diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a0380f4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.e5r-patch merge=union +*.e5r-proj merge=union diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..60049c9 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,165 @@ +name: ci + +# `main` and pull requests, and nothing else. +# +# A branch name in this list is a name that will be wrong later: it was +# `full-rewrite-to-rs`, which is one branch of one period of this repository's +# life, and every other branch went unchecked while that one burned a runner on +# every push. Work in progress is checked when it is proposed, which is what a +# pull request is; a push to a branch nobody has proposed is not yet a claim +# that anything works. +on: + push: + branches: [main] + # Any base branch, so a feature branch proposed against anything is checked. + pull_request: + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -D warnings + +# One in-flight run per ref. Pushing a fix ten seconds after a typo should not +# leave the first run holding a runner; `main` is exempt so a merge is never +# cancelled by the merge behind it. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + # Runners are disposable, so these build the dev profile on purpose. The + # release-only rule in CLAUDE.md is about the maintainer's machine. + check: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all --check + - run: cargo clippy --workspace --all-targets -- -D warnings + + # G2, G3, G4, G6, G7, G9: the whole suite, with the corpus built so the + # oracle-backed gates actually run. Without fixtures they skip silently. + test: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + # The AArch64 std, for the Rust fixture: `build-fixtures.sh` builds it + # for a fixed target so the counts the tests hold do not follow the host. + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-unknown-linux-gnu + - uses: Swatinem/rust-cache@v2 + # Every gate that has no tool skips silently, which is the worst + # possible outcome: a green run that measured nothing. These are what + # the oracle-backed gates need, named so a missing one is visible. + # clang, llvm, binutils the fixture corpus and objdump parity + # llvm-objdump the ARM32 and x86-64 parity oracles + # gcc-aarch64-linux-gnu the `.a64` corpus. This runner is x86-64, so + # without it plain `gcc` writes an x86-64 binary + # into a name claiming AArch64, and the tests + # that read one measure the wrong file + # qemu-user-static both halves of the lifter's hardware oracle, + # and the recorded output of every executed + # AArch64 fixture + # golang-go the Go fixture, which found five decoder bugs + - name: Install the oracles + run: | + sudo apt-get update + sudo apt-get install -y clang llvm binutils qemu-user-static golang-go \ + gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + - name: Build the fixture corpus + run: ./scripts/build-fixtures.sh + - name: Say which oracles are present + run: | + for t in clang llvm-objdump objdump readelf c++filt qemu-x86_64 go rustc; do + printf '%-16s %s\n' "$t" "$(command -v $t || echo MISSING)" + done + # `--no-fail-fast`: without it cargo stops at the first failing test + # binary, so a runner that differs from the maintainer's machine in + # several ways reports them one push at a time. + - run: cargo test --workspace --no-fail-fast -- --nocapture + + # G5: the same bytes must produce the same output at any thread count. + determinism: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + # The AArch64 std, for the Rust fixture: `build-fixtures.sh` builds it + # for a fixed target so the counts the tests hold do not follow the host. + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-unknown-linux-gnu + - uses: Swatinem/rust-cache@v2 + # The cross gcc because this runner is x86-64 and a `.a64` fixture + # has to be AArch64; qemu because several of them are executed. + - run: | + sudo apt-get update + sudo apt-get install -y clang \ + gcc-aarch64-linux-gnu g++-aarch64-linux-gnu qemu-user-static + - run: ./scripts/build-fixtures.sh + - run: cargo build --release + - run: ./scripts/check-determinism.sh + + # G8: what the analysis FOUND, against the floors in scripts/bench-budget.json. + # + # Not a benchmark, and deliberately not run as one. Recall against the symbol + # table and the count of entries no symbol names are the same bytes in, same + # numbers out, on any machine at any thread count, so they gate. Wall time + # and peak memory are facts about a machine, and a shared runner with a noisy + # neighbour is not the machine the ceilings were recorded on -- so timing is + # not measured here, not gated here, and not published from here. It is + # measured by hand on a known machine and written down in docs/benchmarks.md. + # + # One run per binary for the same reason: the fastest of three exists to beat + # down timing noise, and there is no timing to beat down. + recall: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + # The AArch64 std, for the Rust fixture: `build-fixtures.sh` builds it + # for a fixed target so the counts the tests hold do not follow the host. + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-unknown-linux-gnu + - uses: Swatinem/rust-cache@v2 + # The cross gcc because this runner is x86-64 and a `.a64` fixture + # has to be AArch64; qemu because several of them are executed. + - run: | + sudo apt-get update + sudo apt-get install -y clang binutils \ + gcc-aarch64-linux-gnu g++-aarch64-linux-gnu qemu-user-static + - run: ./scripts/build-fixtures.sh + # Release, like determinism above: the recall numbers have to come from + # the code path a user runs. + - run: cargo build --release + - run: RUNS=1 SKIP=rizin OUT=target/recall.json ./scripts/compare-tools.sh + - run: ./scripts/check-bench-budget.py target/recall.json --portable-only --no-timing + + # `install.sh` is what people pipe into a shell, and nothing else in this + # workflow would notice it rotting: it is not built, not linked and not + # imported. Two checks, both cheap. + # + # `sh -n` is the syntax gate. `shellcheck` is the rest of it -- an unquoted + # expansion in an installer run as root is not a style note -- and it is + # pinned to POSIX `sh` because the shebang is `sh`, not bash. + installer: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - run: sh -n install.sh + - run: sudo apt-get update -q && sudo apt-get install -y shellcheck + - run: shellcheck --shell=sh install.sh + # The one thing syntax cannot see: that the script still knows where the + # release puts things. `build-release.sh` names the archive and the + # directory inside it, and the installer reaches into that directory by + # name -- so a change to either that forgot the other is a download that + # unpacks into a path nobody looks at. + - name: The installer agrees with the release layout + run: ./scripts/check-installer.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..29afade --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,232 @@ +name: release + +# Cut by pushing a tag. Everything a user downloads is built here, checksummed +# here and signed here, so the tag is the only thing a maintainer types. +# +# What has actually been run, and what has not. The project's rule is that a +# gate nobody has exercised is a gate that always passes, and a release +# workflow is no different: +# * The `install` and `linux` jobs run commands that were run by hand on the +# maintainer's machine before this file was written. The two musl builds, +# the archive layout and the checksum file are the output of +# scripts/build-release.sh, which is the same script this workflow calls. +# * The `macos` and `windows` jobs are UNVERIFIED. Neither can be run on the +# development machine, which is aarch64 Linux with no macOS SDK and no +# Windows import libraries, and cross-compiling is a different claim from +# cross-testing in any case. The first tag that runs them is the first time +# they have ever run; that is why the release is created as a draft below. +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: "Tag to build and attach to" + required: true + +permissions: + contents: write + # Sigstore keyless signing uses this token as the identity. It is what lets + # the release be signed without a private key in the repository secrets. + id-token: write + +env: + CARGO_TERM_COLOR: always + +jobs: + # M12 promises `cargo install` works. That claim is about a copy of the crate + # with no fixtures/, no tests/ and no scripts/ beside it, so it is checked by + # installing into an empty root and running the binary there, not by reading + # the manifest. Verified locally before this file existed. + install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: cargo install into an empty root + run: cargo install --path crates/e5r-cli --root "$RUNNER_TEMP/inst" --locked + - name: The installed binary runs with nothing else around it + run: | + cd "$RUNNER_TEMP" + ./inst/bin/e5r --version + ./inst/bin/e5r info /bin/ls + ./inst/bin/e5r funcs --json /bin/ls | head -c 200 + + # Static musl, both architectures, linked with the rust-lld that ships in + # every toolchain: no musl-gcc, no cross, no container. Verified on the + # maintainer's machine for both targets. + linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl + - uses: Swatinem/rust-cache@v2 + - run: ./scripts/build-release.sh x86_64-unknown-linux-musl aarch64-unknown-linux-musl + # The runner is x86_64, so one of the two can be run rather than merely + # linked. `file` reports the other, which is all this job can honestly + # claim about it. + - name: Say what came out + run: | + file dist/*.tar.gz + tar -tzf dist/e5r-*-x86_64-unknown-linux-musl.tar.gz + tmp=$(mktemp -d) + tar -xzf dist/e5r-*-x86_64-unknown-linux-musl.tar.gz -C "$tmp" + bin=$(find "$tmp" -name e5r -type f) + file "$bin" + # The build script already fails the job if either archive carries a + # NEEDED or an INTERP. This is the same fact in the form a reader of + # the log recognizes. + ldd "$bin" || true + "$bin" --version + "$bin" info /bin/ls + - uses: actions/upload-artifact@v4 + with: + name: dist-linux + path: dist/*.tar.gz + + # UNVERIFIED. No macOS machine and no SDK here, and Apple's linker cannot be + # had on Linux, so a macOS binary can only be built on a macOS runner. The + # runner is arm64; the x86_64 half is a cross-compile even there and is not + # executed, so this job claims a build and not a test. + macos: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin,x86_64-apple-darwin + - uses: Swatinem/rust-cache@v2 + - run: ./scripts/build-release.sh aarch64-apple-darwin x86_64-apple-darwin + - name: Run the native one + run: | + tmp=$(mktemp -d) + tar -xzf dist/e5r-*-aarch64-apple-darwin.tar.gz -C "$tmp" + bin=$(find "$tmp" -name e5r -type f) + otool -L "$bin" + "$bin" --version + "$bin" info /bin/ls + - uses: actions/upload-artifact@v4 + with: + name: dist-macos + path: dist/*.tar.gz + + # UNVERIFIED. x86_64-pc-windows-msvc needs the MSVC libraries, which exist + # only on a Windows runner; x86_64-pc-windows-gnu was tried on the + # development machine and stops at the mingw-w64 import libraries, which + # rustup does not ship (only crt2.o). The code itself does compile for + # Windows: `cargo check --target x86_64-pc-windows-msvc` passes here, so what + # is untested is the link and the run, not the source. + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + - uses: Swatinem/rust-cache@v2 + - name: Build + shell: pwsh + env: + RUSTFLAGS: -C strip=symbols + run: cargo build --release --target x86_64-pc-windows-msvc -p e5r-cli --bin e5r + - name: Archive + shell: pwsh + run: | + $version = (cargo metadata --no-deps --format-version 1 | ConvertFrom-Json).packages | + Where-Object { $_.name -eq "e5r-cli" } | ForEach-Object { $_.version } + $name = "e5r-$version-x86_64-pc-windows-msvc" + New-Item -ItemType Directory -Force -Path "dist/$name" | Out-Null + Copy-Item "target/x86_64-pc-windows-msvc/release/e5r.exe" "dist/$name/" + Copy-Item README.md, LICENSE, MANUAL.md "dist/$name/" + & "dist/$name/e5r.exe" --version + Compress-Archive -Path "dist/$name" -DestinationPath "dist/$name.zip" + - uses: actions/upload-artifact@v4 + with: + name: dist-windows + path: dist/*.zip + + # One checksum file over every archive, one signature over that file. + # Linux is the floor: if macOS or Windows fails, the release still happens + # with what built, and the notes say which platforms are in it. A missing + # platform should be visible, not a blocked release. + publish: + needs: [install, linux, macos, windows] + if: always() && needs.linux.result == 'success' && needs.install.result == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + - uses: sigstore/cosign-installer@v3 + + - name: Checksums + run: | + cd dist + rm -f SHA256SUMS + sha256sum e5r-* > SHA256SUMS + cat SHA256SUMS + + # Keyless: the signing identity is this workflow's OIDC token, so there + # is no key to store and nothing to rotate. It needs the id-token + # permission above and nothing else. If it fails anyway (a fork, a + # policy, Sigstore being down) the release still goes out with its + # checksums rather than not going out at all, and the notes below say + # whether a signature is present. + - name: Sign the checksum file + id: sign + continue-on-error: true + run: | + cosign sign-blob --yes \ + --output-signature dist/SHA256SUMS.sig \ + --output-certificate dist/SHA256SUMS.pem \ + dist/SHA256SUMS + + - name: Release notes + run: | + tag="${{ github.event.inputs.tag || github.ref_name }}" + repo="${{ github.repository }}" + { + echo "## Downloads" + echo + echo "| File | SHA256 |" + echo "| --- | --- |" + while read -r sum file; do echo "| \`$file\` | \`$sum\` |"; done < dist/SHA256SUMS + echo + echo "The Linux archives are static musl binaries: no dynamic loader," + echo "no glibc version to match, no runtime dependency of any kind." + echo + if [ -f dist/SHA256SUMS.sig ]; then + echo "### Verifying" + echo + echo '```' + echo "sha256sum -c SHA256SUMS" + echo "cosign verify-blob --signature SHA256SUMS.sig --certificate SHA256SUMS.pem \\" + echo " --certificate-identity-regexp '^https://github.com/$repo/' \\" + echo " --certificate-oidc-issuer https://token.actions.githubusercontent.com \\" + echo " SHA256SUMS" + echo '```' + else + echo "Signing did not run for this release; the checksums are unsigned." + fi + echo + echo "Built from \`$tag\`. The macOS and Windows archives are" + echo "cross-built and only smoke-tested on their own runner." + } > notes.md + cat notes.md + + # A draft on purpose while the macOS and Windows jobs are unproven: the + # first tag that runs them is the first time they have ever run, and a + # human should look at the assets before anyone downloads them. Drop + # --draft once a real tag has produced a full set. + - name: Create the release + env: + GH_TOKEN: ${{ github.token }} + run: | + tag="${{ github.event.inputs.tag || github.ref_name }}" + gh release create "$tag" --draft --title "$tag" --notes-file notes.md \ + dist/SHA256SUMS dist/e5r-* $(ls dist/SHA256SUMS.sig dist/SHA256SUMS.pem 2>/dev/null) diff --git a/.gitignore b/.gitignore index 7fbbebb..1199877 100644 --- a/.gitignore +++ b/.gitignore @@ -1,174 +1,31 @@ -# custom -build/ - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo +# Rust +/target/ +**/*.rs.bk +*.pdb + +# Build and analysis artifacts +/fixtures/build/ +/fixtures/corpus/ *.o -*.obj +*.elf +*.exe -# Precompiled Headers -*.gch -*.pch +# Coverage and mutation reports +/lcov.info +/mutants.out*/ +/coverage/ -# Compiled Dynamic libraries -*.so -*.dylib -*.dll +# Fuzzing +/fuzz/artifacts/ +/fuzz/coverage/ -# Fortran module files -*.mod -*.smod +# Machine-local Claude Code settings (wires up scripts/deny-debug-build.py) +.claude/settings.local.json -# Compiled Static libraries -*.lai -*.la -*.a -*.lib +# Editors +.vscode/ +.idea/ +*.swp -# Executables -*.exe -*.out -*.app +# Release archives, built by scripts/build-release.sh. +dist/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b831b62 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# CLAUDE.md + +Scope, milestones and the list of things this project will not build live in +[`ROADMAP.md`](ROADMAP.md). Read that first. + +## This checkout is release-only + +The machine this repository is developed on has 7.5 GB of RAM and the workspace +will grow a SLEIGH compiler, several decoders and a decompiler. A dev-profile +build of that is large enough to be a problem here, and the release build is +already warm, so a debug build is also the slower path. + +Whether a dev build is affordable is a fact about the machine, not about the +project, so the guards are opt-in and gitignored rather than committed settings: + +1. `scripts/no-debug-guard.sh on` parks a regular file at `target/debug`, so + cargo stops with "failed to create directory ... File exists" before it + compiles anything. `off` and `status` do what they say. It works against every + tool, not just Claude Code. The file is under `target/`, which is ignored. +2. `scripts/deny-debug-build.py` is a PreToolUse hook that refuses dev-profile + cargo commands, target-directory redirection, and attempts to remove the + guard. Wire it up in `.claude/settings.local.json`: + + ```json + { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ + { "type": "command", + "command": "python3 \"$CLAUDE_PROJECT_DIR/scripts/deny-debug-build.py\"" } ] } ] } } + ``` + +Both are on in this checkout. Every cargo invocation takes `--release` and the +binary is `./target/release/e5r`. Do not work around a "File exists" failure on +`target/debug`: build release, or turn the guard off deliberately, which is the +owner's call. + +CI runs on disposable runners, so the workflow builds dev profile on purpose. Do +not add `--release` there. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..31a1818 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CLAUDE.md + +Scope, milestones and the list of things this project will not build live in +[`ROADMAP.md`](ROADMAP.md). Read that first. + +## This checkout is release-only + +The machine this repository is developed on has 7.5 GB of RAM and the workspace +will grow a SLEIGH compiler, several decoders and a decompiler. A dev-profile +build of that is large enough to be a problem here, and the release build is +already warm, so a debug build is also the slower path. + +Whether a dev build is affordable is a fact about the machine, not about the +project, so the guards are opt-in and gitignored rather than committed settings: + +1. `scripts/no-debug-guard.sh on` parks a regular file at `target/debug`, so + cargo stops with "failed to create directory ... File exists" before it + compiles anything. `off` and `status` do what they say. It works against every + tool, not just Claude Code. The file is under `target/`, which is ignored. +2. `scripts/deny-debug-build.py` is a PreToolUse hook that refuses dev-profile + cargo commands, target-directory redirection, and attempts to remove the + guard. Wire it up in `.claude/settings.local.json`: + + ```json + { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ + { "type": "command", + "command": "python3 \"$CLAUDE_PROJECT_DIR/scripts/deny-debug-build.py\"" } ] } ] } } + ``` + +Both are on in this checkout. Every cargo invocation takes `--release` and the +binary is `./target/release/e5r`. Do not work around a "File exists" failure on +`target/debug`: build release, or turn the guard off deliberately, which is the +owner's call. + +CI runs on disposable runners, so the workflow builds dev profile on purpose. Do +not add `--release` there. + +That difference matters for more than space. Dev profile panics on an +overflowing `-`; release wraps it and returns a wrong answer quietly. So an +arithmetic bug in a decoder is invisible to every run on this machine and shows +up only on CI -- `move_wide_preferred` underflowed on every 32-bit +`orr Rd, ZR, #imm`, and the local suite was green throughout. A red CI is worth +reading before assuming it is about the runner. diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 7e4643a..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,20 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project("libmydisas" LANGUAGES C CXX) - -# find_package(Threads REQUIRED) - -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -mtune=native -march=native") - -set(SOURCE_DIR "src") -set(SCRIPT_DIR "script") -set(TEST_DIR "test") - -include(GNUInstallDirs) - -include_directories("${PROJECT_SOURCE_DIR}/src") - -add_subdirectory(${SOURCE_DIR}) -add_subdirectory(${SCRIPT_DIR}) - -enable_testing() -add_subdirectory(${TEST_DIR}) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c7dcee8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# Contributing + +## The rule that matters: clean room + +This project reimplements ideas that other reverse engineering tools also +implement. Ideas are not the problem; copied code is. + +**Work from public specifications and from observed behaviour, never from +another tool's source.** In practice: + +- Read the published specification: the ELF gABI and the processor supplements, + the DWARF standard, the PE/COFF specification, the Itanium C++ ABI, the ARM + and Intel architecture reference manuals, the public documentation of the PDB + format. +- Observe what an existing tool *does* as much as you like. Running `objdump` + and comparing its output against ours is how the decoders are tested, and + that is fine: the output is a fact about the input, not a piece of someone's + program. +- Do not read another tool's implementation and then write the same thing here. + If you have read it, say so in the pull request, and expect the reviewer to + ask someone who has not to write that part. +- One exception, deliberately narrow: the SLEIGH *language definitions* under + Ghidra's processor tree are data, and loading them is the point of M2. Their + format is specified. The decompiler's source is not to be consulted. + +If a fixture or a test case comes from another project, say where it came from +and under what licence, in the file that uses it. + +## What a change has to come with + +Every claim this tool makes is measured, and a change that makes a claim has to +bring its measurement. `ROADMAP.md` says what each milestone is gated on; the +short version: + +- **An external oracle wherever one exists.** A decoder is compared against + `objdump` and `llvm-objdump`; a lifter against what a processor produced when + the same program was run; a demangler against `c++filt`; debug information + against `readelf`; a merge against `git merge`. Numbers that only compare the + tool against itself are not evidence. +- **Wrong and missing are different.** A wrong answer is a bug with no + allowance and the test asserts zero of them. A missing answer is a gap with a + floor or a ceiling that only moves one way, and the number stays in + `docs/scorecard.md` even when it is unflattering. +- **A test that cannot fail is not a test.** After writing one, break the code + it covers on purpose and check that it goes red. + +## Style + +- Comments are concise and explain *why*. The code says what it does. A comment + that restates the line above it is noise, and a banner is worse. +- Say what is not known. An analysis that cannot work something out reports + that rather than guessing, and the type system should make the difference + visible: `Option`, an explicit `Unresolved`, a strength on the evidence. +- No emoji, no em dashes. +- `cargo clippy --release --all-targets` leaves no warnings. + +## Building + +This checkout is release-only; `CLAUDE.md` explains why and how the guards +work. Every command takes `--release`: + +``` +CARGO_BUILD_JOBS=4 cargo build --release +CARGO_BUILD_JOBS=4 cargo test --release +bash scripts/build-fixtures.sh # the corpus the gates measure against +bash scripts/check-determinism.sh # the same input must give the same output +``` + +Fixtures are built, not committed. A test whose fixture or tool is missing +returns early rather than failing, so a fresh checkout on a different machine +still runs the suite it can. + +## Reporting something wrong + +A decoding that disagrees with `objdump`, a lifting that disagrees with the +processor, or a recovered fact that is untrue are the most valuable reports +this project can get. Include the bytes, the architecture, and what the other +tool said. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9de2d26 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,498 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "e5r-analysis" +version = "0.1.0" +dependencies = [ + "e5r-arch", + "e5r-core", + "e5r-format", + "e5r-types", + "rayon", + "serde", +] + +[[package]] +name = "e5r-api" +version = "0.1.0" +dependencies = [ + "e5r-analysis", + "e5r-core", + "e5r-db", + "e5r-decomp", + "e5r-format", + "e5r-ir", + "e5r-types", +] + +[[package]] +name = "e5r-arch" +version = "0.1.0" +dependencies = [ + "e5r-core", +] + +[[package]] +name = "e5r-asm" +version = "0.1.0" +dependencies = [ + "e5r-arch", + "e5r-core", +] + +[[package]] +name = "e5r-cli" +version = "0.1.0" +dependencies = [ + "clap", + "e5r-analysis", + "e5r-api", + "e5r-arch", + "e5r-asm", + "e5r-core", + "e5r-db", + "e5r-decomp", + "e5r-diff", + "e5r-format", + "e5r-ir", + "e5r-types", + "memmap2", + "serde", + "serde_json", +] + +[[package]] +name = "e5r-core" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "e5r-db" +version = "0.1.0" +dependencies = [ + "e5r-analysis", + "e5r-arch", + "e5r-core", + "e5r-format", + "e5r-types", + "serde", +] + +[[package]] +name = "e5r-decomp" +version = "0.1.0" +dependencies = [ + "e5r-analysis", + "e5r-api", + "e5r-arch", + "e5r-core", + "e5r-decomp", + "e5r-format", + "e5r-ir", + "serde", +] + +[[package]] +name = "e5r-diff" +version = "0.1.0" +dependencies = [ + "e5r-analysis", + "e5r-arch", + "e5r-core", + "e5r-db", + "e5r-format", + "serde", +] + +[[package]] +name = "e5r-format" +version = "0.1.0" +dependencies = [ + "e5r-core", + "e5r-types", + "serde", +] + +[[package]] +name = "e5r-ir" +version = "0.1.0" +dependencies = [ + "e5r-analysis", + "e5r-arch", + "e5r-core", + "e5r-format", + "e5r-types", + "serde", +] + +[[package]] +name = "e5r-sla" +version = "0.1.0" +dependencies = [ + "e5r-sleigh", +] + +[[package]] +name = "e5r-sleigh" +version = "0.1.0" +dependencies = [ + "e5r-arch", + "e5r-core", +] + +[[package]] +name = "e5r-types" +version = "0.1.0" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..57c5de1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,60 @@ +# e5r — a CLI reverse engineering toolkit. +# +# The library owns every fact; the CLI is a thin client of it. +# Crate boundaries and the dependency direction are documented in ROADMAP.md +# under "Architecture": edges run downward only, and e5r-core depends on +# nothing else in the workspace. +[workspace] +members = ["crates/*"] +resolver = "3" + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +license = "Apache-2.0" +repository = "https://github.com/h5i-dev/e5r" +authors = ["e5r contributors"] +# Publishing metadata. Here rather than in each crate so a crate that becomes +# publishable only has to opt in; e5r-cli is the one that is published today. +homepage = "https://github.com/h5i-dev/e5r" +documentation = "https://docs.rs/e5r-cli" +readme = "README.md" +keywords = ["reverse-engineering", "disassembler", "decompiler", "binary", "elf"] +categories = ["command-line-utilities", "development-tools::debugging"] + +[workspace.dependencies] +e5r-core = { path = "crates/e5r-core", version = "0.1.0" } +e5r-format = { path = "crates/e5r-format", version = "0.1.0" } +e5r-arch = { path = "crates/e5r-arch", version = "0.1.0" } +e5r-asm = { path = "crates/e5r-asm", version = "0.1.0" } +e5r-sla = { path = "crates/e5r-sla", version = "0.1.0" } +e5r-sleigh = { path = "crates/e5r-sleigh", version = "0.1.0" } +e5r-analysis = { path = "crates/e5r-analysis", version = "0.1.0" } +e5r-db = { path = "crates/e5r-db", version = "0.1.0" } +e5r-types = { path = "crates/e5r-types", version = "0.1.0" } +e5r-diff = { path = "crates/e5r-diff", version = "0.1.0" } +e5r-ir = { path = "crates/e5r-ir", version = "0.1.0" } +e5r-decomp = { path = "crates/e5r-decomp", version = "0.1.0" } +e5r-api = { path = "crates/e5r-api", version = "0.1.0" } + +# Kept deliberately short. Loaders and decoders are written here rather than +# pulled in, because provenance tagging and hostile-input behaviour are the +# product, not an implementation detail we can delegate. +memmap2 = "0.9" +rayon = "1.10" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +clap = { version = "4.5", features = ["derive", "wrap_help"] } + +# A dev-profile build lands in target/debug, which this checkout forbids +# (see CLAUDE.md). Release is the only profile used here. +[profile.release] +opt-level = 3 +# Off on purpose: this machine has 7.5 GB of RAM and debug info in a workspace +# this size makes linking the peak-memory step. Profile with a temporary +# override when a flamegraph is actually needed. +debug = false +lto = "thin" +codegen-units = 16 +panic = "unwind" diff --git a/MANUAL.md b/MANUAL.md new file mode 100644 index 0000000..aabf042 --- /dev/null +++ b/MANUAL.md @@ -0,0 +1,485 @@ +# e5r + +Reverse engineering from the command line. Load a binary, recover its +functions, disassemble them, decompile them, and ask questions about what is +there. + +Every command takes `--json` and prints the same information in a form a +script can read. Exit codes are `0` for success, `1` for nothing found, `2` for +a bad command line, and `3` for input that could not be read. + +``` +cargo build --release +./target/release/e5r info /bin/ls +``` + +## Installing + +### A release binary + +Every release attaches one archive per platform, a `SHA256SUMS` file and a +signature over it. Unpack and put `e5r` on your path: + +``` +tar -xzf e5r-0.1.0-x86_64-unknown-linux-musl.tar.gz +install -m755 e5r-0.1.0-x86_64-unknown-linux-musl/e5r /usr/local/bin/ +``` + +Check what you downloaded first. The checksums cover the archives, and the +signature covers the checksum file. Signing is Sigstore keyless, so the +identity is the release workflow itself and there is no key to distribute: + +``` +sha256sum -c SHA256SUMS +cosign verify-blob --signature SHA256SUMS.sig --certificate SHA256SUMS.pem \ + --certificate-identity-regexp '^https://github.com/h5i-dev/e5r/' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + SHA256SUMS +``` + +| Platform | Archive | +| --- | --- | +| Linux x86-64 | `e5r-VERSION-x86_64-unknown-linux-musl.tar.gz` | +| Linux AArch64 | `e5r-VERSION-aarch64-unknown-linux-musl.tar.gz` | +| macOS Apple silicon | `e5r-VERSION-aarch64-apple-darwin.tar.gz` | +| macOS Intel | `e5r-VERSION-x86_64-apple-darwin.tar.gz` | +| Windows x86-64 | `e5r-VERSION-x86_64-pc-windows-msvc.zip` | + +### What the Linux build guarantees + +The Linux archives are static musl binaries. There is no dynamic loader, no +`libc.so` to find and no glibc version to match, so one file runs on any Linux +with the same architecture: a current distribution, an eight-year-old one, +Alpine, a distroless container, or a rescue initramfs. `ldd` on it says it is +not a dynamic executable, and that is the whole claim: + +``` +$ file e5r +e5r: ELF 64-bit LSB pie executable, x86-64, static-pie linked, stripped +``` + +Nothing outside the binary is needed at run time. The fixture corpus under +`fixtures/` is what the test suite measures against; an installed `e5r` never +reads it, and no data file, configuration or cache directory is required to +start. + +### From source + +``` +cargo install --git https://github.com/h5i-dev/e5r e5r-cli +cargo install --path crates/e5r-cli # from a checkout +``` + +The dependency list is deliberately short (`serde` and `serde_json`, `clap`, +`memmap2`, `rayon`, and nothing else), and no crate in the workspace has a +build script or links against a C library. That is what makes cross-compiling +a matter of naming a target rather than assembling a sysroot: the loaders and +decoders are written here rather than pulled in, so nothing in the graph needs +a platform toolchain. + +To build the release archives yourself, for any target whose standard library +rustup has installed: + +``` +scripts/build-release.sh x86_64-unknown-linux-musl aarch64-unknown-linux-musl +``` + +It links the musl targets with the `rust-lld` that comes with the toolchain, so +cross-building the other architecture's static binary needs no musl gcc, no +`cross` and no container. The archives and the `SHA256SUMS` land in `dist/`. +The tarball adds no variation of its own: entries are sorted and ownership and +timestamps are zeroed, so the same binary always packs to the same checksum. + +### install.sh + +``` +curl -fsSL https://raw.githubusercontent.com/h5i-dev/e5r/main/install.sh | sh +``` + +It works out the platform, asks the releases API for the latest tag, downloads +that archive, **verifies it against the release's `SHA256SUMS` and refuses to +install if it does not match**, and puts the binary in `/usr/local/bin` with +`install` rather than `mv`, so a `sudo` install does not leave a user-writable +binary in a root-owned directory. + +Four environment variables, all of them for a case the default does not cover: + +| | | +| --- | --- | +| `E5R_INSTALL_DIR` | somewhere other than `/usr/local/bin` | +| `E5R_VERSION` | a tag other than the latest | +| `E5R_BASE_URL` | a mirror, or a staged release that is not published yet | +| `E5R_SKIP_CHECKSUM` | install without verifying, said out loud on stderr | + +There is no Homebrew formula. There was a draft one, and it was deleted rather +than published: it had never been run against `brew`, and a tap is a second +place a version number has to be right. A script that has been run against a +real archive, and refuses a tampered one, is the smaller promise and the kept +one. + +## What it will and will not tell you + +Every recovered fact carries the evidence for it, and the strength of that +evidence is printed next to it: + +| Strength | Meaning | +| --- | --- | +| `asserted` | A person or an agent wrote it in the annotation log. | +| `proven` | The file says so: a symbol table, debug information, an entry point, an unwind record. | +| `inferred` | Something in the code implies it: a call target, a jump table, an import thunk. | +| `heuristic` | A pattern suggests it: a prologue, a linear sweep, a pointer in data. | + +Where a thing cannot be worked out, it is reported as unknown rather than +guessed at. A function whose control flow could not be followed to the end is +marked incomplete; a jump table whose bound could not be established is left +unresolved; an instruction the lifter does not model appears as unmodelled in +the output rather than being approximated. + +## Looking at a file + +``` +e5r info FILE container, architecture, entry, what the loader noticed +e5r sections FILE sections and where they are mapped +e5r symbols FILE symbols the container names +e5r imports FILE what it needs from elsewhere +e5r exports FILE what it offers +e5r strings FILE strings found in the image, by section +e5r stats FILE counts: functions, blocks, instructions, references +``` + +`info` and the listings above need only the container, so they are instant +whatever the size of the file. + +## Code + +``` +e5r funcs FILE every recovered function and its evidence +e5r disas FILE TARGET disassemble a function, an address, or `all` +e5r xrefs FILE TARGET [--from] references to an address, or from one +e5r decompile FILE TARGET pseudo-C for a function, or for `all` +e5r emulate FILE TARGET [ARGS...] run a function and report what it did +``` + +`TARGET` is an address (`0x401000`, `401000`), a symbol name, or `all`. + +The decompiler's output says what the machine does in C's notation. It does +not claim to be the source. Where the control flow does not fit an `if` or a +`while`, a labelled `goto` appears rather than a shape that is not there, and +the number of them is printed above the function: that is the honest measure +of how well the structuring worked. Where the binary carries debug +information, parameters are named and typed as the compiler recorded them; +where it does not, they are recovered from what the code does with them. + +`emulate` runs the function in the same interpreter the lifters are tested +against. Nothing escapes the process: memory is a copy of the image, a system +call stops the run, and a budget bounds it. + +## Windows + +A PE keeps its debug information in a separate `.pdb`, so it is looked for: at +the path the linker recorded, and beside the binary, which is where the file +usually actually is. + +**A database from the wrong build is refused**, with a note saying so. Symbols +from a different build are worse than no symbols, because they are confidently +wrong, so the identity the image asks for is checked before anything in the +database is believed. + +``` +$ e5r funcs prog.exe +0x140001000 ... proven program database compare_points +0x140001020 ... proven program database walk +``` + +Without it, the same binary yields only its entry point. + +`e5r info` reports `pe.pdb`, `pe.pdb.key` and `pe.pdb.age`, which together +are what a symbol server is indexed by. + +## C++ + +``` +e5r classes prog # the hierarchy +e5r classes prog --members # and every member function, with its `this` +``` + +Classes come from the type information where a binary has it, and from the +virtual tables alone where it does not. Both Itanium and Microsoft layouts are +read. Every claim says what it rests on: a name read out of RTTI or a mangled +symbol is proven, one inferred from what a function writes is not, and the two +are never printed as each other. + +A binary built with `-fno-rtti` degrades and says which it is: + +``` +13 class(es) from 13 table(s); 9 named by symbol, 0 only by type information + (unknown, absent (-fno-rtti): vtables only, no hierarchy) +``` + +`--members` lifts every member function to see what it touches through +`this`, which on a real C++ library takes tens of seconds, so it is off by +default. + +## Structure and types + +``` +e5r shapes FILE TARGET what the pointers a function takes point at +e5r vtables FILE virtual tables and the classes they belong to +``` + +`shapes` reports the offsets a function touches through each pointer it is +given, and the element size when it walks an array of them. It reports what it +saw rather than a conclusion: it does not name fields and it does not average +two disagreeing strides into one. + +## Asking questions + +``` +e5r query FILE 'functions where insns > 100 and name ~ "crypt"' +e5r query FILE 'strings where length >= 20 and text ~ "/etc/"' +e5r query FILE 'xrefs where kind = call and to = 0x401000 limit 20' +``` + +Entities: `functions`, `strings`, `symbols`, `imports`, `exports`, `sections`, +`xrefs`. Operators: `=` `!=` `<` `<=` `>` `>=`, `~` for contains and `!~` for +does not, combined with `and`, `or`, `not` and brackets. Numbers may be +decimal or hexadecimal. A field that does not exist is a mistake in the query +and is reported as one, with the fields there are. + +## Comparing builds + +``` +e5r diff OLD NEW [--all] +``` + +Functions are matched across the two builds by a fingerprint of their +instruction shape rather than by address, so the match survives a relink. +Matched, changed, added and removed are reported with a similarity score. +Given a vulnerable build and a patched one, this points at the change. + +## Naming a stripped binary + +``` +e5r sig FILE create -o LIBRARY a signature for every named function +e5r sig FILE apply LIBRARY name what the library recognizes +``` + +A signature identifies a function by its content rather than its address, so a +library built from a binary with symbols names the same code in one without +them. It refuses every coin flip: two signatures that disagree about a hash +name nothing, a short function is not identified by shape alone, and import +thunks are excluded because they differ only in an offset. + +The file is sorted text, so a signature library reviews in a diff. + +## An interactive session + +``` +$ e5r repl prog +38 function(s) in prog. `help` lists the commands, `quit` leaves. +0x400144> f +0x400144> seek parse_header +0x400180> dec +0x400180> q 'calls to "memcpy" where arg3 is not bounded' +0x400180> quit +``` + +The commands are the ones above, spelled the same way and without the file, so +nothing has to be learned twice. `seek` moves, and the commands that take an +address use where you are when given none. Short forms exist only for the ones +typed a hundred times an hour: `d` `dec` `f` `i` `s` `x` `q` `?`. + +The point is not saved typing. Analyzing a large binary takes seconds, and a +session pays it once: five `funcs` on libcrypto is 1.26s as five invocations +and 0.63s in one session, and the gap widens with the binary. + +A session runs the same code the command line does, so the two cannot disagree +about what a command means. + +## Telling it what you know + +An assertion is not a note on the side: it changes the analysis. Decompile, +see what the engine could not work out, say what it is, decompile again. + +``` +$ e5r decompile prog nestedoffset +uint64_t nestedoffset(uint64_t arg0, uint64_t arg1, uint64_t arg2) +{ + return (uint64_t)((uint64_t)(uint32_t)*(uint32_t *)(arg0 + + ((int64_t)(int32_t)((uint32_t)arg1 + (uint32_t)arg2) << 2) + 12)); +} + +$ e5r annotate prog type nestedoffset \ + "int nestedoffset(struct outer { int header; int pad; int array[8]; } *ptr, int a, int b)" + +$ e5r decompile prog nestedoffset +struct outer { int32_t header; int32_t pad; int32_t array[8]; }; + +int32_t nestedoffset(struct outer *ptr, int32_t a, int32_t b) +{ + return (int32_t)((uint64_t)(uint32_t)*(uint32_t *)((uint64_t)ptr + + ((int64_t)(int32_t)((uint32_t)a + (uint32_t)b) << 2) + 12)); +} +``` + +The binary above has no debug information at all. The declaration is refused +if it does not parse, so a typo is caught when it is typed. + +An assertion outranks what the engine recovered, and where the two disagree +the disagreement is recorded rather than hidden: declare two parameters where +the code reads four argument registers and `--json` reports the other two as +conflicts. That is the point of the strength ladder, not a caveat on it. + +`--json` also carries every variable with its name, type, size, role and +where the machine kept it, so a program driving this can see what there is to +assert: + +``` +$ e5r decompile prog nestedoffset --json + ... + "asserted": true, + "variables": [ + { "name": "ptr", "type": "struct outer *", "role": "parameter", "storage": "register+56" }, + { "name": "a", "type": "int32_t", "role": "parameter", "storage": "register+48" }, + { "name": "b", "type": "int32_t", "role": "parameter", "storage": "register+16" } + ] +``` + +## Annotations + +``` +e5r annotate FILE name TARGET NAME +e5r annotate FILE comment TARGET TEXT +e5r annotate FILE list +e5r annotate FILE undo +``` + +Annotations are written to a log beside the binary, keyed to a content anchor +rather than an address, so the work survives a rebuild. The log is text sorted +by that anchor, so two analysts on separate branches merge without conflict +markers. Add this to `.gitattributes`: + +``` +*.e5r merge=union +``` + +## Containers that are not one program + +An `ar` archive holds objects rather than being one, so it is listed rather +than loaded: picking a member silently would make every later answer about +bytes you did not choose. + +``` +e5r archive libfoo.a # members, sizes and offsets +e5r archive libfoo.a --symbols # and what each one defines +``` + +GNU and BSD archives, long names, both symbol index forms, and thin archives. + +``` +e5r overlay firmware.exe +``` + +reports where the headers stop describing the file, what is appended past that +point, and the entropy of every section with a peak per 4 KB window, which is +how a packed region inside an ordinary-looking section shows up. + +Findings carry a strength and never a verdict. A section that is writable and +executable is a fact; what put it there is not. A packer is named only where a +section carries that packer's own signature. + +## Patching + +An edit is anchored to the code around it and carries the bytes it expects to +find, so a patch written against one build still lands on the right +instruction in the next one, and says how it found it. + +``` +e5r patch prog record 0x401234 --asm "nop" --note "skip the check" -o fix.e5r-patch +e5r patch prog record 0x401234 --bytes 90909090 -o fix.e5r-patch +e5r patch prog record 0x401234 --asm "mov eax, 1" --pad-to 8 -o fix.e5r-patch +e5r patch prog preview fix.e5r-patch # where it lands, what it overwrites +e5r patch prog apply fix.e5r-patch -o prog.patched +e5r patch prog.patched revert fix.e5r-patch -o prog +e5r patch prog merge a.e5r-patch b.e5r-patch -o both.e5r-patch +``` + +`--asm` assembles at the target address, because a branch encodes a +displacement from where it sits. It accepts what `e5r disas` prints, so a +line can be copied out, changed, and assembled back. `--pad-to` fills the rest +with no-ops: an instruction that encodes shorter than the one it replaces +would otherwise leave the bytes after it meaning something they did not mean. + +A set applies as a whole or not at all. Overlapping edits are a conflict +rather than an order-dependent result, and an edit is always the same length +as what it replaces, because a different length would move every byte after it +and invalidate the rest of the set. + +`apply` refuses by default when an edit resolved on its address alone, since +that means the code it was written against is no longer there; `--allow-address-only` +overrides. `revert` does not refuse, because a patched binary no longer holds +the bytes the anchor fingerprinted, which is exactly what applying it did. + +## Saving a session + +``` +e5r project new prog -o prog.e5r-proj --set scan_gaps=true +e5r project add prog.e5r-proj --signatures libc.e5r-sig --patch fix.e5r-patch +e5r project verify prog.e5r-proj +e5r project show prog.e5r-proj +``` + +A project names its binary by content as well as by path. Opening it against a +rebuilt binary says so rather than answering about bytes that are not there, +and a binary that only moved is still the right one. + +## Driving it from a program + +Every command takes `--json`. The schema is named in every document +(`"schema": "e5r/1"`) and addresses are hex strings so nothing is lost to a +float. + +## Shell integration + +``` +e5r completions bash > /etc/bash_completion.d/e5r +e5r completions zsh > ~/.zsh/completions/_e5r +e5r completions fish > ~/.config/fish/completions/e5r.fish +e5r manpage > /usr/local/share/man/man1/e5r.1 +``` + +Both are generated from the command tree, so they cannot describe a command +that does not exist. + +## Common options + +| Option | Effect | +| --- | --- | +| `--json` | Print the same information as JSON. | +| `--base ADDR` | Load a raw image, or rebase a relocatable one, at an address. | +| `--arch NAME` | The architecture of a raw image, which has no header to say. | +| `--threads N` | How much of the analysis to run in parallel. The output does not depend on this. | +| `--color WHEN` | `auto` (a terminal only), `always`, or `never`. `NO_COLOR` is honoured. | +| `--no-pager` | Do not page, even when a terminal is reading. `PAGER` and `E5R_PAGER` choose the pager. | +| `--budget SECONDS` | Stop after this long and report what was finished. | +| `--limit N` | Stop after this many items and report what was finished. | +| `--progress` | Show a counter on stderr. Silent when stderr is not a terminal. | + +A job that runs out of budget prints why on stderr and how much is left, and +the output it already produced is still a valid document: + +``` +$ e5r decompile libstdc++.so.6 all --budget 3 > partial.c +e5r: out of time after 3.5s and 1024 of 5609 function(s); 4585 not done. +Raise --budget or --limit, or narrow the target. +``` + +Colour, paging and width-fitting happen only when a person is reading. Piped +output carries no escape sequences and is never truncated, so a listing taken +today diffs against the same listing taken a week ago. + +## What is not built yet + +SLEIGH languages, i386, Windows-specific entry points, an assembler, and an +interactive session. `ROADMAP.md` is the list, and it says what each milestone +is and what it is measured by. diff --git a/README.md b/README.md index 2479b85..73cc036 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,346 @@ -# MyDisassembler +

+ ci + Apache-2.0 + GitHub stars + release +

-Welcome to MyDisassembler, a project designed to help learners understand X86-64 machine codes, assembly language, ELF format, and disassembly strategies. With this tool, we can explore the intricacies of low-level programming and gain insights into how binaries are structured and executed. +

The Reverse Engineering Toolkit for AI Agents

-You might find [our memo](MEMO.md) helpful if you're new to X86-64 and assembly (like us). +**e5r** is a disassembler, decompiler and binary differ with a command line as +its only front end. Every command speaks JSON, every recovered fact carries the +evidence for it, and every name, type and comment an agent writes lands in a +git-mergeable log. One static Rust binary: no JVM, no project server, no +proprietary database. -## Install + + + + + + +
+ Agent-native
+ JSON on every command, and analysis an agent can correct +
+ Evidence, not assertions
+ Every gate measured against an outside oracle, including a processor +
+ 21×–3,291× faster than rizin
+ 2.4×–8.6× less memory, in our benchmarks +
-To get started with MyDisassembler, follow these simple steps: +**Let agents read binaries the way a human does with IDA — and write what they +learn back into the repository.** -- Clone the Repository: +```bash +# Look at a binary. +e5r info ./a.out # container, architecture, entry, what the loader noticed +e5r funcs ./a.out # every function, with the evidence for each boundary +e5r disas ./a.out main # one function, or a range, or everything +e5r decompile ./a.out main # pseudo-C, with a goto where the shape is not there + +# Ask questions instead of reading output. +e5r xrefs ./a.out 0x4006e8 # who reaches this, and how +e5r strings ./a.out # ASCII, UTF-8 and UTF-16LE, by section +e5r classes ./a.out # C++ hierarchy from the vtables and the RTTI +e5r query ./a.out 'functions where insns > 100 and name ~ "crypt"' + +# Write what you worked out, and merge it like code. +e5r annotate ./a.out name 0x4006e8 parse_header +e5r annotate ./a.out type 0x4006e8 "int parse_header(struct hdr *h, size_t n)" +e5r decompile ./a.out parse_header # the assertion reaches the output + +# Drive it from an agent. +e5r batch ./a.out --command funcs --command strings # several commands, one document +e5r funcs ./a.out --json # every command takes it +``` + +--- + +## 1. Install + +```bash +curl -fsSL https://raw.githubusercontent.com/h5i-dev/e5r/main/install.sh | sh +# cargo install --path crates/e5r-cli # build from source +``` + +One binary, no runtime dependency. The script works out the platform, verifies +the download against the release's `SHA256SUMS`, and refuses to install if it +does not match. [`MANUAL.md`](MANUAL.md) has the environment variables. + +```bash +e5r completions bash > /etc/bash_completion.d/e5r # or zsh, fish, elvish +e5r manpage > ~/.local/share/man/man1/e5r.1 +``` + +--- + +## 2. Use it + +### 2.1. Read a binary + +Everything e5r recovers carries the evidence for it, and the strength of that +evidence is printed next to it, so a boundary from an `.eh_frame` record and one +from a prologue pattern are never the same claim: + +```bash +e5r info ./a.out # container, architecture, entry, warnings +e5r sections ./a.out # and how each maps into memory +e5r funcs ./a.out # address, size, blocks, strength, evidence, name +e5r disas ./a.out main # a function, an address, a range, or `all` +e5r stats ./a.out --json # counts, for a script rather than a reader +``` + +| Strength | Meaning | +| --- | --- | +| `asserted` | A person or an agent wrote it in the annotation log. | +| `proven` | The file says so: a symbol table, debug information, an unwind record. | +| `inferred` | The code implies it: a call target, a jump table, an import thunk. | +| `heuristic` | A pattern suggests it: a prologue, a sweep, a pointer in data. | + +Where a thing cannot be worked out it is reported as unknown, never guessed. + +### 2.2. Decompile + +```bash +e5r decompile ./a.out main # pseudo-C +e5r decompile ./a.out all --json # every complete function, with a position map +e5r shapes ./a.out parse_header # what the pointers it takes appear to point at +e5r emulate ./a.out checksum 1 2 3 # run it in the interpreter and see what comes back +``` + +The output says what the machine does in C's notation; it does not claim to be +the source. Where the control flow does not fit a loop or a branch, a labelled +`goto` appears rather than a shape that is not there, and the count is printed. + +**Every function that can be compiled and run is compiled and run, on every +argument vector, and agrees with the interpreter.** That gate started at 130 +disagreeing functions and is at zero. + +### 2.3. Write analysis back, and merge it + +Names, types and comments are lines in an append-only log keyed to **content +anchors** — a shape hash and a body hash — rather than to addresses, so they +survive a rebuild that moves everything: ```bash -git clone https://github.com:Koukyosyumei/MyDisassembler.git +e5r annotate ./a.out name 0x4006e8 parse_header +e5r annotate ./a.out comment 0x400710 "length is attacker-controlled" +e5r annotate ./a.out list ``` -- Build the Project: +Two analysts merge with `git merge`. Reviewing reverse engineering work becomes +a pull request. Add `*.e5r merge=union` to `.gitattributes`: the fold ignores +line order, so the union of two branches is the correct merge and git already +knows how to compute it. + +### 2.4. Compare two builds ```bash -cd MyDisassembler -./script/build.sh +e5r diff ./old ./new # which functions changed, moved, appeared, vanished +e5r diff ./old ./new --json # and what changed inside each one ``` -## Usage +Matching is by content, not by address, so a rebuild that shifts every function +is not reported as a rewrite of the program. + +### 2.5. Patch, and say why + +```bash +e5r patch ./a.out record 0x4006f0 --asm "nop" --out fix.e5rpatch +e5r patch ./a.out preview fix.e5rpatch # what it would change +e5r patch ./a.out apply fix.e5rpatch --out ./patched # a new file, never in place +``` -Once installed, you can use MyDisassembler to disassemble binary files and delve into their assembly code. Here's how you can use it: +### 2.6. Drive it from an agent ```bash -./build/script/mydisas example/jmp.o +e5r funcs ./a.out --json # every command takes it +e5r batch ./a.out --command funcs --command strings +e5r project new ./a.out --out a.e5rproj # reopen it later without reanalysing ``` -```yaml -section: .text ---- - -40 <_start>: - 40: mov eax 0x00000000 ( b8 0 0 0 0 ) - 45: cmp eax 0x00 ( 83 f8 0 ) - 48: jz 4e ; relative offset = 4 ( 74 4 ) - 4a: jmp 52 ; relative offset = 6 ( eb 6 ) - 4c: jmp 40 <_start> ; relative offset = -14 ( eb f2 ) - -4e : - 4e: push rsp ( 54 ) - 4f: xor eax eax ( 31 c0 ) - 51: ret ( c3 ) - -52 : - 52: push rdi ( 57 ) - 53: xor ecx ecx ( 31 c9 ) - 55: ret ( c3 ) -------------------- -Done! +Every command takes `--json`, and the exit codes are documented: `0` ok, `1` +nothing found, `2` bad usage, `3` bad input. That is the whole agent interface, +deliberately: the CLI is a thin client of the library, so anything it can do is +a function call away, and a second protocol on top would be a second surface to +keep in step with the first. + +--- + +## 3. What works + +| | | +| --- | --- | +| Containers | ELF, PE and COFF, Mach-O (thin and fat), PDB, `ar` archives, raw images | +| Decoders | AArch64, x86-64, i386, ARM32 and Thumb-2 — each at zero disagreements with its oracle | +| Also | any architecture a Ghidra SLEIGH specification covers, through our own runtime | +| Lifting | p-code-style IR and SSA for AArch64, x86-64, i386 and ARM32/Thumb | +| Analysis | functions with provenance, control flow, jump tables, no-return propagation, cross references, strings, data regions | +| Decompiler | expressions, types, structuring, variable naming, C++ classes from vtables and RTTI | +| Names | Itanium C++, Rust (both schemes), MSVC | +| Storage | git-mergeable annotation log keyed to content anchors | +| Surfaces | CLI with JSON on every command, REPL, binary diff, patching | + +Scope, and what is deliberately **not** built, is in +[`ROADMAP.md`](ROADMAP.md), which is the authority on both. + +--- + +## 4. How it is tested + +Every correctness gate is measured against something outside this repository, +so no number here can be produced by writing more assertions about our own +behaviour. The ones that go against us stay in +[`docs/scorecard.md`](docs/scorecard.md). + +| gate | oracle | result | +| --- | --- | --- | +| AArch64 decoding | `objdump -d` | 1,640,904 instructions, 0 wrong | +| x86-64 decoding | `llvm-objdump --x86-asm-syntax=intel` | 0 wrong | +| ARM32 and Thumb-2 | `llvm-objdump-18 -d` | 0 wrong | +| i386 decoding | `llvm-mc`, over a swept encoding space | 235,357 encodings, 0 wrong | +| SLEIGH decoding | `objdump` and `llvm-objdump` | 0 wrong on AArch64, x86-64 and RISC-V | +| Lifting | **a processor**, natively and under `qemu` | every case agrees | +| Decompiled C | **a processor**, again: compile it and run it | 0 functions disagree | +| Function boundaries | DWARF, via `readelf` | 95,697 functions; 0.766 recall blind, 0.937 precision | +| ELF, Mach-O, PDB loading | `readelf`, `llvm-objdump -t`, `llvm-pdbutil` | every section, symbol and record | +| Demangling | `c++filt` | 5,953 libstdc++ names | +| Annotation merge | `git merge` itself | two branches, clean | +| Determinism | itself, at 1, 4 and 10 threads | 153 fixtures, identical output | + +A processor is the oracle wherever one can be: the lifters are checked by +assembling a case, running it, and comparing against our own interpreter, so +"the manual says this instruction sets the carry flag" is never the last word. + +Plus mutation fuzzing of every loader and decoder in the ordinary test run, +which found the one place in the loaders that bypassed the bounds-checked +reader, and a no-panic gate over 1.26 million inputs across every entry point +that takes foreign bytes. + +--- + +## 5. Where it stands against the others + +Ghidra, rizin and IDA are each better than e5r at breadth of architecture and +at accumulated analysis lore. Two measurements, both with every command written +down in [`docs/benchmarks.md`](docs/benchmarks.md) and +[`docs/decbench.md`](docs/decbench.md): + +- **Against rizin**, on the same 16 binaries on the same machine: 21× to 3,291× + faster and 2.4× to 8.6× less memory. Three qualifications belong with that + ratio and are in the table rather than under it — including the one row e5r + loses. +- **Against angr, on DecBench**, a third-party decompiler benchmark: 23.4 union + against angr's 37.0. That is a third behind, and the milestone this project + set itself is Ghidra's published 32.2, which is **not met**. + +The bets are on the axes where the others' design, not their effort, is the +limit: speed, annotations in git, determinism, provenance, and an interface +built for a program rather than for a person. + +--- + +## 6. Build + +The checkout is release-only; see [`CLAUDE.md`](CLAUDE.md). + +```bash +cargo build --release +./scripts/build-fixtures.sh # the corpus the gates measure against +cargo test --release --workspace +./scripts/bench.sh ``` -## Features +--- + +## 7. Documentation + +- [MANUAL.md](MANUAL.md) / `man e5r`: the full command reference +- [docs/tutorial.md](docs/tutorial.md): a stripped binary to a committed annotation log +- [docs/design/](docs/design/): one document per subsystem, and why it is shaped that way +- [docs/scorecard.md](docs/scorecard.md): every measured number, including the bad ones +- [CONTRIBUTING.md](CONTRIBUTING.md): what a change has to measure, and the clean-room rule + +--- + +## 8. FAQ + +
+What is e5r? + +A reverse engineering toolkit: it loads a binary, recovers its functions, +disassembles and decompiles them, and lets you write names, types and comments +back into a log you can commit. It runs locally, is written in Rust, and ships +as one static binary. + +
+ +
+Why another one, when Ghidra and rizin exist? + +Not because they are bad. Because a few things are hard to retrofit: an +annotation store that merges with `git merge` rather than with a project-file +lock, output that is deterministic across thread counts, a provenance record on +every recovered fact, and an interface designed for a program to call rather +than for a person to click. Those are design choices rather than effort, and +they are what e5r is betting on. + +
+ +
+Is the decompiler as good as Hex-Rays? + +No, and the gap is measured rather than estimated: 23.4 union on DecBench +against IDA's published 47.9 — on a different corpus, which +[`docs/decbench.md`](docs/decbench.md) is careful about. What e5r does claim is +narrower and checked: every function it decompiles, that can be compiled and +run, computes what the machine computes. + +
+ +
+What does "agent-native" actually mean here? + +Two things that are properties of the tool rather than a wrapper around it. +Every command emits JSON with a schema and a documented exit code, so an agent +drives it the same way it drives `git`. And an agent's conclusions are +first-class input: an asserted name or type reaches the decompiled output and +the recovered prototype, so read, conclude, correct, re-read is the normal way +to use it rather than a feature bolted on. + +
+ +
+Can I trust what it tells me? + +Look at the evidence column, and at [`docs/scorecard.md`](docs/scorecard.md). +"Wrong" is treated as a bug with no allowance; "not recovered" is a gap with a +number and a floor that only rises. Where the two could be confused, the +scorecard says so — it has a section for what is *not* measured. + +
+ +
+What is not built? + +WASM, .NET, DEX and Java class files; scripting beyond the batch language; and +the decompiler quality needed to clear the DecBench milestone. All of it is in +[`ROADMAP.md`](ROADMAP.md) with the reason, and none of it is claimed here. + +
+ +--- + +## 9. History -- Implemented entirely from scratch in C++ -- Supports both linear sweeping and recursive descent disassembly strategies -- Handles most basic operations with precision -- Capable of parsing ELF headers for deeper analysis +e5r began as a C++ teaching disassembler for x86-64. That code is in the git +history, and its x86-64 and ELF notes are kept at +[`docs/x86-64-notes.md`](docs/x86-64-notes.md). -## Future Improvements +--- -- Expand test coverage for enhanced reliability -- Add support for VEX Prefix -- Incorporate additional instructions, including floating-point operations -- Introduce support for AT&T syntax +## 10. License -Feel free to contribute to MyDisassembler and make it even better! +Apache-2.0. See [LICENSE](LICENSE). diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..380abf4 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,989 @@ +# Roadmap + +Status: in progress, 2026-09-13. This file is the scope authority for e5r: +what it is, what gets built in what order, and what will be refused in review +rather than argued about again. + +## Where it stands + +| Part | State | +| --- | --- | +| workspace, guards, fixtures, CI | built (M0) | +| ELF, PE and COFF, Mach-O, raw | built (M1) | +| AArch64 decoder | built, objdump parity over 395K fixture instructions, 99.67% decoded | +| x86-64 decoder | built, llvm-objdump parity over 4,760 instructions, 100% decoded | +| SLEIGH runtime and compiler | not started (M2) | +| i386, ARM32 and Thumb-2 decoders | not started (M2) | +| functions, CFG, xrefs, strings, jump tables, no-return | built and parallel (M3) | +| IR, lifters, interpreter, SSA, dataflow | built for AArch64 and x86-64, gated against real execution (M4) | +| stack promotion, ABI model, algebraic rules | built (M4) | +| decompiler to pseudo-C | built, output gated on compiling (M6) | +| type model, DWARF, PDB | not started (M5) | +| demanglers | Itanium, Rust both schemes, MSVC names (M5) | +| annotation log, content anchors, git merge | built (M7) | +| CLI with JSON on every command | built (M8) | +| binary diff | built (M10) | +| patch, signatures, emulation, queries | not started (M10) | +| benchmarks and scorecard | built (M11) | +| fuzzing, coverage and mutation tooling | built (M12); packaging not started | + +The measured numbers live in [`docs/scorecard.md`](docs/scorecard.md), +including the ones that go against us. In short: `libc.so.6` analyzes in 0.10s +and 58 MB, finding 3,517 functions with 95.3% analyzed completely, where +`objdump -d` takes 0.26s and only disassembles; both decoders are at zero +disagreements with their oracle; memory use is about ten times objdump's, which +is the axis e5r is worse on. 164 tests, clippy clean. + +e5r is a reverse engineering toolkit with a command line as its only front end. +It loads a binary, recovers functions, disassembles, lifts to an IR, decompiles +to C, diffs two builds, and writes an analyst's findings to a text log that git +can merge. One static Rust binary, no JVM, no project server, no proprietary +database. + +The previous contents of this repository were a C++ teaching disassembler for +x86-64. It is gone from the tree as of M0 and stays readable in git history. The +x86-64 and ELF notes in `MEMO.md` survive the rewrite and move under `docs/`. + +## The bets + +Ghidra, rizin and IDA are each better than e5r will be for years at breadth of +architecture and at accumulated analysis lore. Beating them means picking the +axes where their design, not their effort, is the limit. + +1. **Startup and throughput:** Ghidra pays JVM startup and a project import + before it shows a function. rizin is single-threaded through most of its + analysis. e5r memory-maps the image, analyzes functions in parallel, and + analyzes a function only when something asks for it. The target is first + useful output in under 200 ms on a 100 MB binary, and full analysis of + `/bin/bash` faster than `rizin -A` by a factor we publish and regression-test. + +2. **Annotations in git, not in a database:** Every name, type and comment an + analyst writes is one line in an append-only log, keyed to a content anchor + that survives a rebuild and a rebase. Two analysts working on branches merge + with `git merge` and no conflict markers, because the log folds to the same + state regardless of line order. Review of reverse engineering work becomes a + pull request. Ghidra needs a server for this and IDA needs a plugin. + +3. **Determinism:** Same bytes, same output, any thread count, any machine. This + is what makes the other features possible: diffing two builds, gating a CI + job on an analysis result, and reproducing a bug report all need the tool to + stop guessing differently on Tuesday. + +4. **Proven, inferred, asserted:** Every fact the engine reports carries which of + the three it is and what evidence produced it. A function boundary from an + `.eh_frame` FDE is not the same claim as one from a prologue pattern, and the + output says so. The incumbents flatten both into the same listing. + +5. **Built for automation first:** Structured JSON on every command, stable exit + codes, and a library API that the CLI is a thin client of. Not + a headless mode retrofitted onto a GUI. + +6. **Breadth through SLEIGH, depth by hand:** Writing 40 instruction decoders is + not a good use of the next two years. e5r ships a Rust SLEIGH runtime and + compiler so Ghidra's processor specifications load directly, plus + hand-written decoders for x86-64 and AArch64 where speed matters. Kuna proved + the SLEIGH port is tractable. + +## Architecture + +The library owns every fact. The CLI and any future UI compute +nothing of their own. + +``` +e5r-core addresses, spaces, memory map, ids, error model, provenance +e5r-format ELF, PE/COFF, Mach-O, raw, archives, overlays +e5r-arch decoder trait, hand-written x86/x86-64 and AArch64/ARM +e5r-sleigh .sla runtime plus the SLEIGH compiler (breadth) +e5r-ir p-code-style IR, SSA, dataflow framework +e5r-analysis partition, function discovery, CFG, jump tables, xrefs, strings +e5r-types type model, DWARF, PDB, demanglers, type archives +e5r-decomp regions, structuring, expression rebuild, C emission +e5r-db content anchors, the annotation log, git merge semantics +e5r-diff function matching, build-to-build diff +e5r-patch assembler-backed patch sets +e5r-api stable library surface and the versioned JSON schema +e5r-cli the e5r binary +``` + +Dependency edges run downward only. `e5r-cli` may depend on everything; +`e5r-core` depends on nothing in the workspace. + +## Milestones + +### M0. Clear the ground + +- [x] Copy the release-only build guards from h5i (`scripts/no-debug-guard.sh`, + `scripts/deny-debug-build.py`) and wire the hook in + `.claude/settings.local.json`. This machine has 7.5 GB of RAM; a dev-profile + build of a workspace this size is not affordable. +- [x] Delete `src/`, `test/`, `script/`, `CMakeLists.txt` and the C++ build. Move + `MEMO.md` to `docs/x86-64-notes.md`. +- [x] Cargo workspace, edition 2024, MSRV pinned, `resolver = "3"`. +- [x] `[profile.release]` with `panic = "unwind"`, because the fuzz targets need + it. Debug symbols are off: on 7.5 GB of RAM, linking with them is the + peak-memory step. +- [x] Rewrite `.gitignore` for Rust; drop the Python and C++ sections. +- [x] Rewrite `README.md` for the new tool. The old one documents a C++ teaching + disassembler that no longer exists. +- [x] CI on GitHub Actions: fmt, clippy with `-D warnings`, `cargo deny`, tests. + CI runners are disposable, so CI builds dev profile on purpose. +- [x] `CONTRIBUTING.md` with the clean-room rule: public specifications and + published papers only, no decompiled IDA or disassembled Hex-Rays. +- [x] Test corpus. A `fixtures/` tree of small committed binaries plus a script + that fetches larger ones (coreutils, `libcrypto`, a Go binary, a stripped + C++ binary, a Rust binary, a PE from a public malware corpus, an iOS dylib). + Ground truth comes from DWARF where the fixture has it. +- [x] Bench harness that records wall time, peak RSS and output hash per fixture, + so a regression shows up as a number and not a feeling. +- [x] Coverage and mutation tooling, run on demand rather than in CI: + `cargo llvm-cov --workspace` for per-crate line coverage and + `cargo mutants` for the mutation score, with `mutants.toml` saying what + not to mutate. Neither gates yet — the floors below are not met, and a + floor nobody can pass is a floor everyone learns to skip — and a CI job + that reports a number without gating on it buys nothing: the coverage + run re-ran the whole suite under instrumentation for a summary line, and + the mutation run ended in `|| true`, so it could not fail. They belong in + CI on the day the floors become gates. + +### M1. Containers + +Depth-first: ELF and PE carry the workload, Mach-O follows, everything else waits. + +- [x] ELF32 and ELF64, both endians. Program headers, sections, `symtab`, + `dynsym`, GNU hash, symbol versioning, `.dynamic`, `.init_array`, + `.eh_frame` and `.eh_frame_hdr`, `.note.gnu.build-id`. +- [x] ELF relocations for x86-64 and AArch64, enough to load `.o` files + correctly: the absolute, relative and branch kinds a compiler emits, + including the split immediate an `adrp` carries and the scaled + twelve-bit offsets. Gated by running functions out of the objects and + comparing against what the processor produced. i386 is still to do. +- [x] ELF relocations for i386, enough to load `.o` files and to resolve PLT + entries to names. REL rather than RELA, so the addend comes out of the + bytes being patched; seventeen types applied and every TLS type recorded + as unhandled rather than skipped. 37 relocations compared against + `readelf` entry for entry with zero disagreements, plus 1,828 in + `.rel.debug_*` that were never read before. +- [x] PE and COFF. Both image and object layouts, import and delay-import + descriptors with their IAT slots, exports, the COFF symbol table, the + debug directory's PDB path, and the exception directory + (`RUNTIME_FUNCTION` unwind data, the best function-boundary oracle + Windows offers). Base relocations, resources and load config are still + to do. +- [x] Mach-O, thin and fat, with `LC_FUNCTION_STARTS` for boundaries, the + symbol table, dylib dependencies and the UUID. Chained fixups and the + exports trie are still to do, as is the dyld shared cache. +- [x] Raw blob loading with an explicit base, architecture and entry point, plus + Intel HEX and S-record, for firmware work. +- [x] `ar` archives and loose object files. GNU and BSD flavours, long names, + both symbol index forms and thin archives, measured against `ar t` and + `nm -s`. An archive is opened rather than loaded: it has no architecture, + entry point or memory map, so making one an `Object` would mean picking a + member and making every later answer about bytes the caller never chose. +- [x] Overlay detection, section entropy map, and a packer heuristic that reports + a suspicion with its evidence rather than a verdict. Nine finding kinds, + each with a strength; a packer is named only where a section carries that + packer's own signature. The false-positive gate matters as much as the + true positives: fourteen ordinary binaries, seven linked and seven + relocatable, produce no overlay and no finding at `Inferred` or above. +- [x] Every loader treats its input as hostile. A malformed header returns a typed + error; it never panics and never allocates from an attacker-controlled count. +- [ ] Later, behind a feature flag: WASM, .NET metadata, DEX, Java class files. + +### M2. Instruction decoding + +- [x] x86-64 decoder, table-driven in the Intel manual's own operand notation + so an entry can be diffed against appendix A. Legacy prefixes, REX, the + one-byte, two-byte and three-byte maps, mandatory prefixes, groups, and + SSE through SSE4.2. VEX, EVEX and AVX are not written; 32-bit mode is + not supported, because the opcodes it spends on inc/dec and the BCD + instructions are REX prefixes in long mode. +- [x] AArch64 A64 decoder including Advanced SIMD. SVE is deliberately not + decoded: it is a separate architecture's worth of encodings and the + coverage number records its absence. +- [x] ARM32 and Thumb-2, including interworking and the IT block. 3,568 + instructions over twelve fixture objects against `llvm-objdump-18`, 0 + wrong and 0 undecoded, after a development sweep of roughly 260,000 + random and strided encodings that found about 15,000 disagreements. + NEON, the parallel arithmetic, the saturating and packing instructions + and the exception-return transfers are declined rather than guessed. +- [x] i386, the 32-bit mode of the x86 decoder. The mode rather than the + opcodes, since the table is shared: no REX, so 0x40-0x4f are `inc` and + `dec`; default operand and address size 32; ModRM mod=00 rm=101 is an + absolute displacement rather than rip-relative; eight registers; the + instructions valid only in 32-bit mode and the ones invalid in it. + Measured against `llvm-mc` and `llvm-objdump` over a sweep of 235,357 + candidates, every one- and two-byte opcode crossed with prefix strings + and ModRM shapes, both three-byte maps, all 256 ModRM values of all eight + x87 escapes, and 200,000 random byte piles: 0 wrong, 99.711% decoded. + 100.000% over the compiled fixtures. What it declines is what llvm + declines because it faults on hardware, and what it decodes where llvm + declines is listed with the reason. +- [x] SLEIGH runtime. The `.sla` format is worked out in + [`docs/sla-format.md`](../docs/sla-format.md) and its reader consumes all + 137 files Ghidra ships. The decode engine measures 0 wrong against + objdump on AArch64 (19,710), x86-64 (5,688) and RISC-V 64 (2,567), the + last being an architecture this tool could not decode at all and for + which no code was written: the language definition is data. Sixteen more + decode noise without panic. P-code lifts at 100% for RISC-V, 99.8% for + AArch64 and 98.5% for x86-64, and every gap is SLEIGH's user-defined + operation, which this IR has no opcode for. +- [x] The `.slaspec` front end: all 152 language definitions Ghidra ships + parse, none fail, 133,097 constructors in two seconds, with the bit + patterns reduced to masks that are honest about what they cannot pin + down. 24 real encodings across ten architectures select their own + constructor and no other. +- [x] SLEIGH compiler: `.slaspec` to `.sla`. Partial, and the number says how + partial: all 152 definitions compile and read back with a model that + agrees with the source, and a file we wrote decodes 20,000 encodings + identically to the parsed source on RISC-V and x86-64 and all but 8 on + AArch64. Against Ghidra's own compiler, 2 of 152 match byte for byte, + because the payload is 43% of the reference's: the missing 57% is p-code + templates, whose allocation rule is not established and so is not + guessed. Where the structure is established, 150 of 152 agree on the + space table and 148 on the constructor count. +- [x] Assembler for x86-64 and AArch64, needed by M10 patching. Encoding only, + from public manuals, built around the decoder so it accepts our own + disassembly verbatim: a user can copy a line out of `e5r disas`, change + it, and assemble it back. Round trip over the whole fixture corpus, 0 + wrong on both architectures; byte-identical to `as` and + `aarch64-linux-gnu-as` on every one of 2,535 and 1,272 comparable forms. + A branch whose target does not fit is a typed error naming the range, + never a truncated displacement. SIMD and floating point are refused by + name rather than guessed. +- [x] Text formatting for each architecture, with Intel and AT&T syntax for x86 + and a formatter trait so a caller can render its own. + +### M3. Program model and partition + +- [x] Address spaces and a memory map that survives overlays, `bss`, and a raw + blob loaded at an arbitrary base. +- [x] Function discovery, layered by evidence quality: symbol table, `.eh_frame` + FDEs, PE unwind records, `LC_FUNCTION_STARTS`, Go `pclntab`, ObjC method + lists, Swift metadata, call targets found by recursive descent, then + prologue scanning as the last resort. Each layer tags its provenance. +- [x] Recursive descent with a linear sweep fallback over the gaps, with the two + reconciled rather than concatenated. +- [x] Basic blocks, CFG, tail-call detection, and no-return propagation + through the call graph. Each block records why it ended, which is what + distinguishes a function that returns from one that only ever traps. +- [x] Jump table recovery for the absolute, base-relative and entry-relative + forms, on both architectures. The bound comes from the compare that + guards the switch, matched to the register the table is indexed by; a + compact table of byte or halfword offsets is refused outright when that + compare cannot be found, because every value in such a table yields a + plausible target and scanning cannot honestly bound it. MSVC's two-level + tables are still to do. +- [x] Cross references: code to code, code to data, with the reference type + recorded, including the AArch64 `adrp`/`add` pair. Data to data is still + to do. +- [x] String extraction: ASCII, UTF-8 and UTF-16LE, scanned by section rather + than by segment. Go string headers and Rust slices are still to do. +- [x] Data flow into the data sections: pointers, vtables, jump tables and + literal pools marked as data so the code partition stops at them. Five + proofs, each something observed rather than inferred; a candidate + overlapping a block some function walked is dropped. 55,577 regions over + 154 images, 8.72% of the bytes the gap scan sweeps. It changed no + function anywhere -- no word inside a marked region matches a prologue + pattern -- so it is a guard measured at zero firings, gated by a + hand-built image whose literal-pool word *is* `stp x29, x30, [sp, #-16]!`. +- [x] A jump table whose entries are bytes scaled from a separate `adr` + anchor, which is what gcc emits for a small dense switch at `-O1` and + `-Os` on AArch64: + + ``` + ldrb w1, [x1, w3, uxtw] ; a one-byte entry + adr x3, ; not the table's own address + add x1, x3, w1, sxtb #2 ; sign extended and scaled by four + br x1 + ``` + + Recovery models a four-byte offset from the table's own base and not + this, so `pick` in `em-paths.a64.O1` stays incomplete while the same + source at `-O2` recovers. Found by the emulation gate, which confirms a + recovered table by running the branch for every index: a table that is + not recovered at all is the one thing that gate cannot confirm. The + `ldrh` form, for a switch too large for a byte, is the same shape. +- [x] PLT thunk resolution from the relocation table, so a call through one + prints the imported name. GOT and IAT still to do for the indirect + forms. +- [x] Windows-specific entry points, which are how a PE runs code the entry + point never reaches. TLS callbacks, the `.pdata` exception directory with + the `.xdata` unwind info behind it (frame layout, handler, chained + parent, and the ARM64 packed form), SEH scope tables, base relocations, + and the load config's Control Flow Guard and SafeSEH tables, which are + linker-built lists of real entry points the loader enforces and so carry + their own evidence kind. Measured against `llvm-readobj` entry for entry + over four real linked PE images, x64 and arm64 at O0 and O2, with zero + disagreements. Still to do: the resource directory, the 32-bit `fs:[0]` + handler chain, which is code recognition rather than a directory, and the + `.CRT$XC*` initializer tables, which are a section-name convention. +- [x] Parallel analysis with deterministic output. Functions are independent + units; the work queue order must not reach the result. + +### M4. IR and dataflow + +- [x] A p-code-style IR with explicit varnodes and address spaces. Operations + have no implicit effects, so an instruction that sets four flags becomes + four operations; the register file is byte-addressed, so `w0` overlapping + `x0` is a property of the addresses rather than a rule. AArch64 lifts + 99.52% of the instructions inside recovered functions and x86-64 99.83%, + including the integer SIMD of both and floating point. +- [x] x86-64 lifting with every flag each instruction writes, the wide multiply + and divide its one-operand forms need, and SSE. Gated by running the same + program on both architectures — natively here, under qemu for the other — + and comparing the recorded answers against the interpreted lifting: 59 + calls across five optimization levels on each architecture, floating + point compared as the bits the machine produced. +- [x] SSA construction over the IR, with partial register writes made explicit + rather than papered over: locations are canonical whole registers, a + narrow read becomes a `SubPiece` and a narrow write a masked merge, so + `w0` sitting inside `x0` is a dependency dataflow can see. +- [x] Stack depth tracked through a function, so an access at a known offset + from the entry stack pointer is recognized whichever block it is in. + `alloca` and dynamic frames make the depth unknown, which the analysis + reports rather than guesses. +- [x] Memory promotion, turning stack slots into variables where aliasing + allows. It refuses when any stack address is used as a value or when two + slots overlap without being identical. +- [x] Constant folding, copy propagation and dead code elimination, each run to + a fixed point. On the fixture corpus they take 7,726 lifted operations to + 915, which is what turns IR into something a person can read. +- [x] An algebraic rule pool: known-bits simplification, which collapses the + sub-register merges partial writes produce; local common-subexpression + elimination; and pattern rules that turn flag algebra back into the + comparison it stands for. +- [x] The default calling convention per architecture, which dead code + elimination needs to know what the caller reads after a return and the + decompiler needs to name arguments. Per-function detection of the ones a + compiler invents is still to do. +- [x] Value-set or range analysis, enough to bound a jump table index and to + prove a comparison constant. Interval domain with widening over eight + rounds; 117 results checked, 62 of them bounded. +- [x] Calling convention detection per function. Four answers rather than two: + standard, standard with fewer arguments where the gap in the register + order is the evidence, non-standard with the departures named by + register, and unknown where the lifter did not model everything, because + an unmodelled instruction's reads are not arguments. Gated against + `DW_TAG_call_site_parameter`, which is what the compiler knew: 33 of 37 + recorded argument registers found. +- [x] Prototype recovery: parameter count, storage, return value, varargs. + 186 prototypes recovered with no argument lost. +- [x] Feedback edges. A prototype learned late re-runs the callers' dataflow. The + schedule is explicit and budgeted, not a `while (changed)` around everything. + Callers also settle whether a function returns at all. +- [x] An IR interpreter, landed with the lifter as planned. Ten semantic tests + run real compiled functions through it and compare against the answer + computed independently in Rust: arithmetic at every width, signed and + unsigned division including division by zero, widening, all seven + comparisons, conditional selection, shifts and rotates, nested loops, + and a memory-summing loop. It found two lifter bugs that reading could + not have: signed overflow was always false at 64-bit width, and the + two-operand conditional-select aliases had their condition inverted, + which turned an absolute value into a negation. + +### M5. Types + +- [x] A C type model with structures, unions, arrays, enums, function pointers + and typedefs, which prints a declaration the way C spells it: the + declarator wraps around the name, so a pointer to an array is not a + prefix and a suffix glued together. +- [x] DWARF 4 and 5 consumption: the type graph, function signatures with + named parameters, local variables with frame offsets, and the line + table. Relocations are applied to the debug sections first, because a + relocatable object writes zero where an address goes. Measured against + readelf for names and against the object's own symbol table for + addresses. +- [x] Structure recovery from access patterns: the offsets touched through a + pointer become fields, loop-carried pointers are followed through their + phi so a walk reports its element size, and the result is what was seen + rather than a conclusion. Gated against the debug information: the + recovery runs without looking at it and has to agree. +- [x] A C type model with structures, unions, arrays, enums, function pointers, + bitfields and typedefs, sized per architecture, and a parser that reads + a declaration into it. +- [x] DWARF inlined frames, call sites and location lists, compared against + `readelf` entry for entry: 39 inlined frames with their ranges and call + lines, 16 call sites by argument count, 198 variables with location + lists compared range for range. 230 variables in the fixtures change + storage inside their own body, which a single-location reader gets + wrong for most of the function. +- [x] PDB consumption for Windows binaries, from the public format + documentation. Measured against `llvm-pdbutil` record by record on real + databases built here by clang and lld: 14 procedures, 27 public symbols, + 24 frame-relative locals, 33 register locations by range and register + name, 9 inlined frames, 25 section contributions. It fills the same + structures the DWARF reader does, so nothing above that layer knows + which format the information came from, and a database whose identity + does not match the image is refused with a note rather than believed. +- [x] Demanglers: Itanium C++ at 94% exact parity with `c++filt` over 5,953 + real libstdc++ symbols, Rust in both schemes, and MSVC qualified names. + Swift and ObjC selectors are still to do, and MSVC's type grammar is a + separate job from its names. +- [x] Type archives. A whole translation unit parses into the type model: all 61 + declarations of `elf.h`, with `Elf64_Ehdr` laid out at 64 bytes checked + against the specification rather than against a compiler, and all 91 of + a preprocessed `stdint.h`. Importing Ghidra `.gdt` is still to do. +- [x] Structure recovery fed back into the decompiler. A declared structure + turns an offset into the declared field name, and an inferred one turns + it into `field_`, which is this crate saying what is at that + offset and cannot be mistaken for something a person wrote. +- [x] C++ vtable recovery, by symbol and by scanning, reported with what each + rests on, and gated against a fixture whose virtual dispatch is run in + the interpreter and compared against hardware. +- [x] C++ recovery beyond the tables: RTTI where present, constructor and + destructor identification, `this` pointer typing. Both the Itanium and + Microsoft layouts. Over libstdc++, RTTI names 56 tables no symbol named, + and with symbols cleared it names 156 of 211 that otherwise had only an + address. `-fno-rtti` degrades to the tables alone and says so. It found + a defect no fixture could: the constructor rule accepted a vtable-pointer + store at any offset, so a class that sets a member's pointer was read as + that member's constructor, 114 wrong classes in 335 claims; restricted to + offset zero it is 221 claims and none wrong. +- [x] Go: `pclntab` function names and `moduledata`. 1,299 of 1,299 function + names recovered from a stripped Go binary. Interface tables and the + runtime type descriptors are still to do. +- [x] Rust: the metadata that exists, which is less than people expect. Panic + location strings carry file and line and are worth mining. +- [x] ObjC class and method lists with selectors, and Swift nominal type + descriptors, field descriptors and protocol conformances. The Swift side + is measured against fixtures written from the published ABI, not against + compiler output, because there is no Swift toolchain here; that is a + weaker claim than the DWARF and Go gates and the scorecard says so. + +### M6. Decompiler + +- [x] Region identification over the CFG, producing a region tree distinct from + the block graph: sequences, two-way branches joined at their immediate + post-dominator, natural loops from back edges, and a labelled goto where + the graph has no such shape. +- [x] Structuring into if, while and the loop forms, with break and continue + taken from the loop nesting and the goto count reported as the quality + signal. Switch recovery and the SAILR edit-and-measure loop are still to + do. +- [x] Expression rebuilding from SSA, with operator precedence and cast + insertion that is correct rather than pretty, and the reinterpretations + between a value's bits and the number they stand for written down + wherever the two disagree. +- [x] Variable naming: a value read more than once or produced by a phi becomes + a named local, a promoted stack slot becomes a named variable, and + arguments are named from the calling convention. Merging variables that + share storage across their live ranges is still to do. +- [x] C emission carries every variable with its name, type, size, role and + where the machine kept it, which is what a consumer needs to match + against what the source declared. A token-level position map, so the CLI + can highlight and slice, is still to do. +- [ ] Port Ghidra's 89 decompiler datatests to our format before the M6 gate + opens. 61 cases are ported, representing roughly 48 of the 89; the rest + need processors we do not decode, user-applied data types we have no way + to attach, or assert on Ghidra's own SSA dump. 56 pass and 5 stay + `#[ignore]`d, each naming a real defect, so the list of known defects + lives in the test suite rather than in someone's head. +- [x] Quality gates: goto density per function against a ceiling that only + comes down, and a recompilability check — every function recovered from + the fixture corpus is decompiled into one translation unit that `clang` + has to accept. Density is zero at O0, O1 and Os on both architectures and + 0.24 per function at O2 and O3. +- [ ] M6 does not close until DecBench scores e5r above Ghidra on the + unoptimized set: 32.2 union, 29.3 structure. **Measured 2026-09-13: 23.4 + union, 22.9 structure.** Not met. The gap is structuring: 1.98 gotos per + function against angr's 0.57, and the 124 functions with three or more + score zero on structure, every one of them. +- [ ] Options are toggles, not rewrites. A user who wants low-level output and a + user who wants idiomatic C get the same engine with different switches. + +### M7. The annotation store + +This is the feature that distinguishes e5r from every incumbent, so it gets +designed before it gets coded, and the design lives in +[`docs/design/db.md`](../docs/design/db.md). + +- [x] Content anchors. A function is identified by a fingerprint of its + instruction-shape stream with branch targets excluded, so the identity + survives a rebase and a relink; by an exact body hash for the same-binary + fast path; and by its address as a tiebreak. Resolution reports which of + the three matched. +- [x] Anchors for things that are not functions: a data object, an address inside + a function, a structure field, a call site. +- [x] The log. One assertion per line, append-only, sorted by a sequence number + with a content hash as the deterministic tiebreak. A fold over the log is + the current state, and the fold is order-independent. +- [x] Merge semantics. Two branches that annotate the same binary merge with git + alone. A conflict on the same field of the same anchor resolves by the fold + rule, and the tool can list which assertions lost. +- [x] What is never stored: anything the engine can recompute. Function + boundaries, blocks, xrefs and types inferred from bytes stay out of the + file, so regenerating them cannot conflict. +- [x] Undo and redo as operations on the log. +- [x] Provenance on every assertion: who, when, and optionally why. +- [x] A project file that records the binary hash, the load configuration and the + analysis options, and nothing else. It names the binary by content as well + as by path, so a rebuilt binary is refused and a moved one is a distinct, + usable verdict. +- [x] Tests that run actual `git merge` on diverging annotation branches and + assert the folded result. + +### M8. The command line + +- [x] Non-interactive subcommands that do one thing and exit: `info`, `sections`, + `imports`, `exports`, `funcs`, `disas`, `decompile`, `xrefs`, `strings`, + `search`, `graph`, `diff`, `patch`, `annotate`, `sig`. +- [x] `--json` on every one of them, against a schema that is versioned and + checked in. Breaking the schema is a major version bump. +- [x] Stable exit codes, documented, so a script can branch on them. +- [x] A REPL for the interactive session, with commands that read as words + rather than as a two-character grammar, plus short forms for the ones + typed a hundred times an hour. It runs the command line's own dispatch + against one analysis, so a session and a one-shot invocation cannot + disagree about what a command means, and five commands on libcrypto cost + 0.63s in a session against 1.26s as five invocations. +- [x] Address expressions: symbol names, `main+0x20`, `[rip+0x10]`, section + relative, file offset. +- [x] Paging, color, and a terminal-width-aware listing that stays diffable when + piped. All three are decided once from whether standard output is a + terminal, so piped output is byte-identical to what it was before they + existed. `NO_COLOR`, `PAGER` and `E5R_PAGER` are honoured; `--color` and + `--no-pager` override. JSON is never coloured or paged. +- [x] Shell completion for bash, zsh and fish, and a generated man page, both + generated from the command tree so neither can describe a command that + does not exist. +- [x] Progress reporting on stderr with an estimate. `--progress` covers the + loops the CLI drives, with a rate-based estimate, silent unless stderr is + a terminal. The inside of `analyze()` now reports too: + `Session::with_progress` takes a callback, five stages report through it, + the `Update` allocates nothing, and every report is made from the + sequential merge between parallel batches so nothing contends. Cost is + indistinguishable from noise. + +### M9. Automation and agents + +- [x] `e5r-api`: the library surface the CLI and the tests share, so a + difference between what a test checks and what a user gets is a + difference a user will find. Decompilation, shapes, signatures, + emulation and queries go through it. + Semver from 1.0, with a compatibility test suite. +- [x] ~~MCP server~~. Built in M9 and **removed**: it exposed eight of the + twenty-nine commands, so it was never the complete agent interface it + was advertised as, and everything it did was already reachable through + `--json` and a documented exit code. A second protocol over the same + library is a second surface to keep in step with the first, and the + first one is the one that is complete. +- [x] Cancellable jobs with a budget, so an agent that asks for the decompilation + of a 40,000-function binary gets partial results and a reason. `--budget` + in seconds and `--limit` in items, on `decompile` and `disas`. The clock + starts when the command does, so loading and analysis count against it, + and the reason goes to stderr so a partial document is still a valid + document. +- [x] A batch mode that runs a script of commands and emits one JSON document, + each result carried with the command that produced it. +- [ ] Scripting. Start with the batch language above; add an embedded interpreter + only when a real workflow needs control flow. Python via an extension + module is the likely answer and it is explicitly not in the first year. + +### M10. The differentiators + +- [x] Binary diff. Function matching across two builds by anchor, by name and + by call graph neighborhood, reporting matched, changed, added and removed + with a similarity score, gated on a patched build where the change is + known. +- [x] Diff at instruction granularity inside a changed function, so the answer + is the line that changed rather than the function that contains it. + Patience alignment over a per-instruction shape token that leaves out + exactly what an insertion moves, which is the address a branch or a + pc-relative computation resolves to. An aligned pair whose addresses then + differ is either displaced, which the alignment inside the function and + the function matching outside it can prove, or retargeted, which is a + claim about the program and is never folded into the other. Gated on + three build pairs whose source difference is one line, with the edit list + asserted instruction for instruction: an operator substituted is one + replacement and nothing else, a statement added is four insertions and + one displaced branch, a statement removed is three deletions and one. + `_start`, which the function-level diff calls changed because every byte + of its calls moved, reports no change in it at all. +- [x] Patch sets. An auditable object describing byte edits, previewed before + write, applied to a sibling file by default. A set applies as a whole or + not at all, overlapping edits are a conflict rather than an order-dependent + result, and an edit is always the same length as what it replaces. + Assembling an edit from mnemonics still waits on the assembler. +- [x] Signature matching, in a sorted text format that reviews in a diff. + A library built from a binary with symbols recovers 10 of 11 names in + its stripped copy with none wrong. It refuses every coin flip: two + signatures that disagree about a hash name nothing, a function shorter + than twenty-four instructions is not identified by shape, and import + thunks are excluded because four instructions that differ only in an + offset match across binaries by coincidence. +- [x] Signature matching against published libraries: a corpus built from real + distribution packages, so a statically linked binary stops being 8,000 + anonymous functions. `scripts/build-siglib.sh` takes `.deb`, `.rpm` or + tarball packages, installed packages by name, or bare `.a` files, + extracts the static libraries, and turns every named function in every + archive member into one signature with the member it came from recorded + beside it. Measured on a stripped `-static` build of `hello.c` against + this distribution's own `libc.a`, `libm.a` and `libc_nonshared.a`: 3,228 + signatures from 2,016 archive members recover 497 of the binary's 908 + names, 54.7%, with 0 wrong. The oracle is the unstripped copy's symbol + table, read as the set of names at each address, because a linker puts + `strlen` and `__strlen` on the same byte and either is right. + + The sentence this replaces read "an importer for the a corpus built from + real distribution packages": the format's name was lost in an edit, and + the format was IDA's FLIRT `.sig`. That importer is declined rather than + postponed. Hex-Rays publishes the FLAIR tools and documents the text + `.pat` files they emit; it does not publish the byte layout of the `.sig` + container, and every description of that layout in circulation is + somebody's reverse engineering of the tool, which CONTRIBUTING.md's + clean-room rule forbids reading. Even the documented `.pat` half could + not *match* anything without reimplementing FLIRT's own CRC16 over the + bytes past the leading pattern, and that polynomial is written down in + the same reverse engineering and nowhere else. The archives in a + distribution's development packages carry the same code under the same + names, are what a statically linked binary was built from, and need no + reading of anyone's format. +- [x] Emulation, built on the M4 IR interpreter: `e5r emulate` runs a + function with given arguments and reports what came back and what it + touched. Nothing escapes the process. Gated against the processor by + running the oracle's case table through it. +- [x] Emulation of selected paths, with a memory model and syscall stubs. All + three uses gated against a processor: a decrypted string equal to the + bytes the hardware produced, a masked pointer table yielding exactly its + three real targets after static analysis settled nothing, and five + recovered jump tables confirmed by running the branch for every index. + Nothing escapes the interpreter: the stubs are answers, not actions. +- [x] A query language over the program model, over seven entities with + boolean operators, brackets and JSON on the same command. A field that + does not exist is a typo and says so rather than matching nothing. +- [x] Query over dataflow facts. `calls to "memcpy" where arg3 is not bounded` + answers, and the value is in the negation: `unconstrained` is a claim + about the program, `unknown` is a claim about the analysis, and they are + separate verdicts never printed as each other. Every row carries a + strength. Still to do: interprocedural bounds, so a wrapper one level + deep stops hiding the answer, and relational bounds, so "bounded by the + size of the destination" becomes askable. + +### M11. Performance + +- [x] Published benchmark numbers against `rizin -A` on the fixture corpus, + rerun in CI, with a regression budget. 16 binaries, 21x to 3,291x faster + and 2.4x to 8.6x less memory, with the three qualifications that ratio + needs in the table rather than under it, and the one row e5r loses left + in. `docs/benchmarks.md`, `scripts/bench-budget.json`. Ghidra's headless + analyzer is wired and opt-in behind `GHIDRA_INSTALL_DIR`, but its + decompiler ships x86-64 only, so on this aarch64 host the comparison that + would matter most cannot be made. +- [x] Lazy analysis. A session computes functions, cross references and strings + on first use, memoized, so a command no longer has to be told what to + switch off. `funcs` on libcrypto goes from 0.086s and 55.3 MB to 0.069s + and 50.7 MB. +- [x] An on-disk analysis cache keyed by content hash, format version and the + option bytes each part reads. A hit is 2.7 to 3.1x; a miss costs 25 to + 35% more than no cache at all. A key mismatch is a miss and never a + partial reuse, and a damaged file is a miss with a warning, checked by + corrupting real entries at 1,328 positions with no wrong answer. +- [x] Memory ceiling. Peak resident falls 47.7% on the largest binary here and a + 500 MB one extrapolates to 4.6 to 6.4 GB where every model before put it + at 8.0 to 8.7. Interning the block table by range is most of it: 8.47M + stored blocks were 2.69M distinct ranges, held once per function that + could reach them. Wall time is unchanged and is not claimed as a win. +- [x] Profiling as a habit, with `scripts/flamegraph.sh` checked in and run + rather than merely written. It found that `e5r funcs` spends 12.3% of + its time computing content anchors for every function before printing + anything. + +### M12. Hardening and release + +- [x] Mutation fuzzing of every loader and decoder, running in the ordinary + test suite rather than behind a nightly toolchain, seeded from the real + corpus and bounded by time. It found a panic on its first run: a + five-byte file beginning with the ELF magic, where the class byte was + read by indexing the slice instead of through the reader. A `cargo-fuzz` + setup for longer runs is still to do. +- [x] Resource caps on every attacker-controlled count, with a documented + policy in [`docs/design/limits.md`](../docs/design/limits.md): bound by + the file first, then by the cap, check the cursor advanced, and read + through the reader. Checked by the mutation fuzzer in the ordinary test + suite rather than by review. +- [x] `#![forbid(unsafe_code)]` on every crate except `e5r-cli`, which is + `#![deny(unsafe_code)]` so one audited call can opt in. There is exactly + one exception, the memory map, and its justification is in + [`docs/design/limits.md`](../docs/design/limits.md). +- [x] A no-panic gate on every path that reads foreign bytes, 1.26 million cases + inside a 12-second budget so it runs every time rather than nightly. It + asserts three things and not one: no panic, a per-case ceiling, and that + the object which comes back is self-consistent. It found three defects, + all the same mistake of adding a number the file chose to an address + without checking, and all three are fixed. +- [x] Static musl builds for Linux, both architectures, built and run: 4.4 MB + aarch64 and 5.5 MB x86-64, no interpreter and no shared library, with + the x86-64 one executed under qemu and the aarch64 one producing output + byte-identical to the glibc build. The recipe needs no musl-gcc, no + `cross` and no container, because rust-lld and the self-contained crt + objects ship with rustup. macOS and Windows compile here and cannot be + linked or run here, and the workflow marks both unverified rather than + implying otherwise. +- [x] Packaging. `cargo install` works and is checked by the workflow rather + than asserted: installed into an empty root and run from outside the + checkout, where the absent fixtures break nothing. `cargo package + --workspace` succeeds for all 14 crates. Signing is Sigstore keyless + through the workflow's own identity, so no secret is stored, and it + degrades to unsigned checksums rather than failing the release. + `install.sh` is the install path, and it has been run: against a real + archive served locally, and against a tampered checksum, a missing + `SHA256SUMS` and a missing archive, each of which it refuses. The draft + Homebrew formula it replaced had never been run at all. +- [x] `MANUAL.md`, a design document per subsystem under `docs/design/`, and a + tutorial that takes a reader from a stripped binary to a named, typed, + committed annotation log (`docs/tutorial.md`). + +## Quality gates + +Green before every merge to main. Each one is a command, not a judgment call. + +| Gate | What it checks | +| --- | --- | +| G1 | `cargo fmt --check`, `cargo clippy --release -- -D warnings`, `cargo deny` | +| G2 | `cargo test --release --workspace` | +| G3 | Decoder parity against `objdump` and `iced-x86` on the fuzz corpus | +| G4 | Function boundary precision and recall against DWARF ground truth, per fixture | +| G5 | Determinism: same output hash at 1, 4 and 10 threads, on two runs | +| G6 | No panic and no timeout on the malformed-input corpus | +| G7 | Decompiler goto density and recompilability against the recorded baseline | +| G8 | Benchmark wall time and peak RSS inside the regression budget | +| G9 | Annotation log merge scenarios fold to the expected state | +| G10 | Line and branch coverage at or above the floor for the crate | +| G11 | Mutation score at or above the floor for the crate | +| G12 | DecBench union and recompile rates at or above the last release, per dataset | + +## Test coverage targets + +Ghidra has 1,946 test files and 18,999 `@Test` methods. Copying that number is +the wrong target and it would take a decade. Look at where those tests actually +are: 615 files cover `Features/Base`, 399 cover the framework, and most of the +rest are GUI and integration tests for a UI e5r does not have. The decompiler, +which is the hardest thing in the product, has 15 Java test files, because the +real decompiler tests are 89 XML datatests driving the C++ engine. Processors +have 17. + +So the count is not the measure. Four things are, and the first of them is backed by an +oracle outside this repository, which means its numbers cannot be padded by +writing more assertions about our own behavior. + +### Oracle-backed suites + +| Suite | What the oracle is | Reference point | e5r target | +| --- | --- | --- | --- | +| Decoder parity | `objdump` and `iced-x86` on the same bytes | rizin ships 6,333 named cases and 24,625 lines of assembly vectors | every instruction in the fixture corpus, plus 10^8 randomly generated encodings per architecture, with zero unexplained disagreements | +| Semantic lift | native execution of the same code | Ghidra's PCodeTest compiles 21 C bodies per target and emulates them | the same C bodies plus our own, executed natively and under the e5r emulator, compared on every observable | +| Function boundaries | DWARF and PE unwind records | none published | precision and recall per fixture, tracked per release | +| Decompiler behavior | recorded baseline, reviewed on change | Ghidra 89 datatests, kuna 83 files and 675 assertions | 89 ported cases before the M6 gate opens, then one case per fixed bug, permanently | +| Type recovery | DWARF types in the fixture that has them | none published | agreement rate per type category, tracked per release | +| Annotation merge | `git merge` itself | none | every scenario in the design doc, run against real git | + +The decoder parity suite is the one that has to be enormous and it costs almost +nothing to grow, because the oracle generates the expected answers. A disagreement +is either a bug in our decoder or a known divergence with a written reason, and +the list of written reasons is checked in. + +The semantic lift suite is the part most projects skip, and skipping it is why +lifters quietly produce wrong p-code for years. Ghidra's approach works: compile +a C body for the target, run it natively to get the answer, run it under the +emulator, compare. It is also the only way to test 40 SLEIGH specifications +without 40 experts. + +### Coverage floors + +Line coverage measured with `cargo llvm-cov`, enforced per crate, because a +single workspace number lets a well-tested decoder hide an untested loader. + +| Crate | Line | Branch | +| --- | --- | --- | +| `e5r-format`, `e5r-arch`, `e5r-sleigh` | 90% | 85% | +| `e5r-core`, `e5r-ir`, `e5r-db`, `e5r-types` | 85% | 80% | +| `e5r-analysis`, `e5r-decomp`, `e5r-diff`, `e5r-patch` | 75% | 65% | +| `e5r-cli` | 60% | 50% | + +Parsers and decoders get the high floor because their inputs are hostile and +their failure mode is silent. The decompiler gets a lower one because a +structuring pass has combinatorially many paths and the datatests cover what +matters better than a coverage percentage does. + +### Mutation score + +Coverage says a line ran. It does not say a test would have noticed if the line +were wrong, and on a decoder that distinction is the whole game: a table entry +with the wrong operand size is executed by every test and caught by none. + +`cargo mutants` changes one operator, constant or return value at a time and +reports whether the suite failed. The score is the fraction of mutants killed. + +| Crate | Floor | +| --- | --- | +| `e5r-format`, `e5r-arch`, `e5r-core` | 85% | +| `e5r-ir`, `e5r-db`, `e5r-types` | 75% | +| everything else | 60%, or an exemption with a written reason | + +Full mutation runs are slow, so CI mutates only the diff on a pull request and +the whole workspace on a nightly. Neither Ghidra nor rizin publishes a mutation +score. Publishing ours, and holding a floor, is a claim about test quality that +a test count cannot make. + +### Fuzzing + +Counted by what it finds, not by whether it runs. + +- Every loader, decoder and demangler has a target, run in CI against a corpus + that is checked in and grows with every crash. +- The metric is edge coverage plateau per target, recorded per release. A target + whose coverage stops growing has either been fully explored, which is worth + knowing, or has a harness bug, which is worth fixing. +- A crash found by fuzzing becomes a unit test with the minimized input before + the fix lands. + +### Where the tests live + +- Unit tests next to the code, for anything with a contract. +- `tests/` in each crate for behavior that crosses module boundaries. +- `fixtures/` for the binaries, with a manifest recording the source, the build + flags and the ground truth available for each one. +- `tests/golden/` for the recorded baselines, one file per case, reviewed as part + of the change that moves them. A baseline that moves without a reviewer saying + why is not a baseline. + + +## Scorecard + +The claim is that e5r is better than the incumbents, so it needs numbers that +can prove it wrong. These get recorded in `docs/scorecard.md` from the first +release and rerun every release. + +| Measure | Against | +| --- | --- | +| Time from invocation to first function listing | Ghidra headless, `rizin -A` | +| Full analysis wall time and peak RSS per fixture | same | +| Function boundary F1 on stripped binaries | DWARF ground truth | +| Variable type agreement in decompiled output | DWARF ground truth | +| Goto density per decompiled function | Ghidra decompiler on the same fixture | +| Fraction of decompiled functions that recompile | absolute | +| Byte-identical output across runs and thread counts | absolute | +| Patch-diff: does the changed function rank first | a set of known CVE build pairs | + +Where a number is worse than the incumbent, it stays in the table. A scorecard +that only reports wins is marketing. + +### DecBench + +The scorecard above is ours, which means we choose the fixtures and we grade our +own work. [DecBench](https://decbench.com) is the external check: a public +benchmark that compiles 39 projects at several optimization levels, decompiles +every function with every entrant, and scores the output against the original +source on three axes. 94,575 functions across 803 binaries, with a published +dataset and a leaderboard. + +Its three metrics, none of which we chose: + +| Metric | What it compares | +| --- | --- | +| Structure (GED) | graph edit distance between the source CFG and the decompiled CFG, 0 being isomorphic | +| Types | recovered variable types against DWARF | +| Recompile | recompile the decompiled function with the original toolchain, then compare assembly by Jaccard similarity with linker-dependent operands normalized away | + +Union is the fraction of functions where the entrant is perfect on at least one +of the three. + +Where the field stands, from the 2026-08-29 snapshot. Numbers are percent of +functions, higher is better, on the unoptimized (O0, stripped) set of 34,406 +functions: + +| | Union | Structure | Types | Recompile | +| --- | --- | --- | --- | --- | +| Hex-Rays | 47.9 | 45.7 | 8.7 | 0.8 | +| kuna | 46.8 | 44.8 | 8.1 | 3.0 | +| angr | 45.7 | 41.2 | 12.1 | 0.6 | +| Ghidra | 32.2 | 29.3 | 7.7 | 0.2 | +| Binary Ninja | 28.9 | 24.0 | 10.4 | 0.2 | +| r2dec | 22.1 | 21.5 | 2.3 | 0.1 | + +On plain `-O2` with inlining, everything drops: kuna 31.4, Hex-Rays 30.7, angr +30.3, Ghidra 21.7. On the large-function tail the whole field collapses: kuna +1.6, Binary Ninja 0.8, angr 0.7, Hex-Rays 0.6, Ghidra 0.4. + +Four things follow for this roadmap. + +1. **Ghidra is not the bar,** and beating it is not the achievement it sounds + like. It sits fourth on the default set, 15 points behind Hex-Rays. "Better + than Ghidra" as a decompiler claim means passing 32.2 union and 29.3 structure + on the unoptimized set, and that is the M6 exit criterion. The bar that + matters is Hex-Rays at 47.9. + +2. **Recompilation is wide open.** The best score in the field is kuna at 3.0 and + Ghidra manages 0.2, so fewer than one function in thirty rebuilds to + equivalent assembly anywhere. Output that actually recompiles is worth more to + a patch-diff or a vulnerability workflow than output that reads nicely, and it + is the one axis where a new entrant can lead instead of catching up. M6 + optimizes for it on purpose, which is why the recompilability gate is in G7 + from the start. + +3. **Large functions are the other open axis.** Under 2% for everyone, on a set + of 1,987 functions. A large optimized function is what a researcher actually + hits in real work, and it is what a structuring pass with no budget discipline + gives up on. Scale is already an M11 commitment. + +4. **The LLM results are a scale artifact.** Codex scores 57.2 union and Claude + Code 56.4 on the 250-function sample set, ahead of every traditional + decompiler there. Both score 0.2 on the full 34,406-function set. Per-function + attention from a frontier model wins when someone pays for 250 functions, and + nobody is paying for 94,575. That argues for a fast deterministic engine an + agent drives, which is bet 5. It does not argue that the engine is obsolete. + +Tasks: + +- [x] Use the DecBench dataset as part of the fixture corpus, and G4 with it. + 16 of its 39 projects build here, 592 binaries at three optimization + levels, outside the checkout. Boundary recall and precision are measured + against `readelf`'s DWARF rather than our own reader, over 95,697 + ground-truth functions: 1.000 and 1.000 stripped, and 0.766 and 0.937 + with `.eh_frame` removed as well, which is the number that says what the + analysis can do with nothing but code. See + [`docs/boundaries.md`](../docs/boundaries.md). +- [x] A DecBench backend for e5r, as `scripts/decbench_e5r.py`. Out of tree, + which DecBench's own documentation permits, so the benchmark checkout + stays untouched. Driven by `scripts/decbench.sh`. +- [ ] Submit to the 250-function sample set as soon as M6 produces output at all. + It needs no harness and no open-sourcing: decompile the kit, mail back the + zip. An early bad score is a baseline, not an embarrassment. +- [x] Record the DecBench numbers in `docs/scorecard.md` per release, including + the runs where we lose. The first run is one we lose: 23.4 union against + angr's 37.0 on the same slice, with the method and the defect analysis in + [`docs/decbench.md`](../docs/decbench.md). + +## Not building + +Each of these is a decision. Reopening one needs a reason that did not exist when +it was closed. + +- **A GUI:** Not in the first year, possibly not ever from this repository. The + library API and the JSON schema are designed so someone else can build one. +- **A debugger:** rizin and gdb do this well and it is a different product. The + emulator in M10 runs code paths for analysis; it does not attach to a process. +- **Ghidra script compatibility:** Running Java is the thing this project exists + to avoid. +- **Exotic architectures ahead of depth:** x86-64 and AArch64 on ELF, PE and + Mach-O get finished before anything else gets attention, SLEIGH breadth aside. +- **Reverse engineering the incumbents' internals:** Clean-room: public + specifications, published papers, and the source of permissively licensed + projects used within their licenses. +- **Obfuscation-specific unpackers for named commercial protectors:** The + emulation and query surfaces are general; shipping a bypass for a specific + product is not the goal. + +## Order of work + +M0 and M1 are prerequisites for everything. M2's hand-written x86-64 decoder +comes before the SLEIGH compiler, because the fixture corpus is mostly x86-64 and +because it sets the shape of the decoder trait the SLEIGH runtime has to fit. +M3 and M4 are the long middle. M7 can be built in parallel with M3, since anchors +need only function boundaries and an instruction stream, and getting the storage +format wrong late is expensive. M6 is the hardest and last of the core work. + +The first release worth announcing is M0 through M4 plus M7 and M8: a fast, +deterministic disassembler with git-native annotations and a JSON surface. That +is already a tool a researcher would use. The decompiler is what makes it a +competitor. diff --git a/crates/e5r-analysis/Cargo.toml b/crates/e5r-analysis/Cargo.toml new file mode 100644 index 0000000..5274288 --- /dev/null +++ b/crates/e5r-analysis/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "e5r-analysis" +description = "e5r — function discovery, control flow, cross references, strings." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +e5r-core = { workspace = true } +e5r-format = { workspace = true } +e5r-arch = { workspace = true } +e5r-types = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } diff --git a/crates/e5r-analysis/examples/bench.rs b/crates/e5r-analysis/examples/bench.rs new file mode 100644 index 0000000..ab8754f --- /dev/null +++ b/crates/e5r-analysis/examples/bench.rs @@ -0,0 +1,90 @@ +//! What one command's worth of analysis costs, eagerly and lazily. +//! +//! Not a test. It exists so the M11 numbers are measured with the library the +//! CLI calls rather than inferred from a CLI that has its own per-command +//! workarounds in it. Peak resident set comes from the kernel, and agrees with +//! `/usr/bin/time`. +//! +//! Usage: bench [cache-dir] +//! +//! load parse the header and the symbol table, nothing else +//! funcs-eager what `e5r funcs` costs today: analyze() with strings off +//! funcs-lazy a session asked for functions and nothing else +//! strings-lazy a session asked for strings and nothing else +//! all everything, which is what `e5r stats` needs +//! +//! A cache directory makes the lazy modes read and write it, so running one +//! twice prints a miss and then a hit. + +use std::time::Instant; + +use e5r_analysis::{Cache, ContentHash, Options, Session}; +use e5r_format::LoadOptions; + +/// Peak resident set in kilobytes, as the kernel saw it. +fn peak_rss() -> u64 { + std::fs::read_to_string("/proc/self/status") + .unwrap_or_default() + .lines() + .find(|l| l.starts_with("VmHWM:")) + .and_then(|l| l.split_whitespace().nth(1)) + .and_then(|v| v.parse().ok()) + .unwrap_or(0) +} + +fn main() { + let mut args = std::env::args().skip(1); + let path = args + .next() + .expect("usage: bench [cache-dir]"); + let mode = args.next().unwrap_or_else(|| "all".to_string()); + let cache_dir = args.next(); + + let start = Instant::now(); + let data = std::fs::read(&path).expect("read"); + let object = e5r_format::load(&data, &LoadOptions::default()).expect("load"); + let loaded = start.elapsed(); + + let opts = Options { + strings: mode != "funcs-eager", + ..Options::default() + }; + + let work = Instant::now(); + let note = if mode == "funcs-eager" { + let p = e5r_analysis::analyze(object, &opts); + format!("{} functions, {} xrefs", p.functions.len(), p.xrefs.len()) + } else { + let mut session = Session::new(object, opts); + if let Some(dir) = &cache_dir { + session = session.with_cache(Cache::at(dir), ContentHash::of(&data)); + } + match mode.as_str() { + "load" => "nothing asked for".to_string(), + "funcs-lazy" => { + let f = session.functions(); + let blocks: usize = f.values().map(|f| f.cfg.blocks.len()).sum(); + let insns: u64 = f.values().map(|f| f.cfg.insns() as u64).sum(); + format!("{} functions, {blocks} blocks, {insns} insns", f.len()) + } + "strings-lazy" => format!("{} strings", session.strings().len()), + "all" => { + let s = session.into_program().stats(); + format!( + "{} functions, {} blocks, {} insns, {} xrefs, {} strings", + s.functions, s.blocks, s.insns, s.xrefs, s.strings + ) + } + other => panic!("unknown mode {other}"), + } + }; + let elapsed = work.elapsed(); + + println!( + "{mode:<13} load {:>6.3}s work {:>6.3}s total {:>6.3}s peak {:>7} KB {note}", + loaded.as_secs_f64(), + elapsed.as_secs_f64(), + start.elapsed().as_secs_f64(), + peak_rss(), + ); +} diff --git a/crates/e5r-analysis/examples/demangle.rs b/crates/e5r-analysis/examples/demangle.rs new file mode 100644 index 0000000..944ab5f --- /dev/null +++ b/crates/e5r-analysis/examples/demangle.rs @@ -0,0 +1,61 @@ +//! A filter that demangles the symbol names in whatever is piped through it. +//! +//! `perf` on this machine does not know Rust's v0 scheme, so a profile of e5r +//! reads as `_RNvNtCs..._13e5r_analysis3cfg10build_with`. This crate already +//! depends on the demangler the rest of the tool uses, so the profile can be +//! made readable without installing anything. +//! +//! It belongs in the CLI as `e5r demangle` rather than here. It is an example +//! because the analysis crate is the one `scripts/flamegraph.sh` profiles, and +//! an example needs no new command, no new flag and no other crate's consent. +//! +//! Usage: ... | cargo run --release -p e5r-analysis --example demangle + +use std::io::{BufRead, Write}; + +/// Whether a byte can appear in a mangled symbol. +fn symbolic(b: u8) -> bool { + b.is_ascii_alphanumeric() || b == b'_' || b == b'$' || b == b'.' +} + +/// Replace every mangled name in one line with what it means. +fn rewrite(line: &str, out: &mut String) { + let b = line.as_bytes(); + let mut i = 0; + while i < b.len() { + if (b[i] == b'_' || b[i] == b'?') && (i == 0 || !symbolic(b[i - 1])) { + let mut j = i; + while j < b.len() && symbolic(b[j]) { + j += 1; + } + // A trailing `.llvm.1234` is LLVM's, not part of the name, and the + // demangler rejects the whole symbol if it is left on. + let token = &line[i..j]; + let stem = token.split(".llvm.").next().unwrap_or(token); + if let Some((_, name)) = e5r_types::demangle(stem) { + out.push_str(&name); + i = j; + continue; + } + } + // One character rather than one byte: `i` is always on a boundary, + // because everything a mangled name is made of is ASCII. + let ch = line[i..].chars().next().unwrap_or('\u{fffd}'); + out.push(ch); + i += ch.len_utf8(); + } +} + +fn main() { + let stdin = std::io::stdin(); + let stdout = std::io::stdout(); + let mut w = std::io::BufWriter::new(stdout.lock()); + let mut buf = String::new(); + for line in stdin.lock().lines().map_while(Result::ok) { + buf.clear(); + rewrite(&line, &mut buf); + if writeln!(w, "{buf}").is_err() { + return; + } + } +} diff --git a/crates/e5r-analysis/examples/phases.rs b/crates/e5r-analysis/examples/phases.rs new file mode 100644 index 0000000..ec3a43d --- /dev/null +++ b/crates/e5r-analysis/examples/phases.rs @@ -0,0 +1,292 @@ +//! Where the memory goes, stage by stage and structure by structure. +//! +//! Not a test and not wired into the CLI: it exists so the memory numbers in +//! docs/scorecard.md and in an M11 report are measured rather than guessed. +//! +//! A session is what makes this one run rather than four. Each part is forced +//! in turn and nothing is dropped, so the resident set after a part minus the +//! resident set before it is exactly what that part retains, and the peak +//! along the way is what it needed transiently to get there. An eager +//! `analyze` could only be priced by running it once per option, which prices +//! four analyses rather than four stages. +//! +//! The accounting at the end is the same quantity counted a second way, from +//! the sizes of what is actually stored. Two independent numbers that agree +//! are evidence; one number is a claim. +//! +//! Usage: cargo run --release -p e5r-analysis --example phases -- [no-scan] + +use std::collections::BTreeMap; + +use e5r_analysis::program::{Function, Options}; +use e5r_analysis::{Cfg, Session}; +use e5r_core::Addr; +use e5r_format::{LoadOptions, Object}; + +/// Current and peak resident set, in kilobytes, from the kernel. +fn rss() -> (u64, u64) { + let s = std::fs::read_to_string("/proc/self/status").unwrap_or_default(); + let field = |name: &str| -> u64 { + s.lines() + .find(|l| l.starts_with(name)) + .and_then(|l| l.split_whitespace().nth(1)) + .and_then(|v| v.parse().ok()) + .unwrap_or(0) + }; + (field("VmRSS:"), field("VmHWM:")) +} + +/// Print one stage, and return the resident set so the caller can difference +/// it against the next. +fn mark(label: &str, prev: u64, note: String) -> u64 { + let (now, peak) = rss(); + println!( + "{label:<18} rss {:>8} KB (+{:>8} KB) peak {:>8} KB {note}", + now, + now.saturating_sub(prev), + peak + ); + now +} + +/// Rust's B-tree holds between 5 and 11 elements per node, plus a parent +/// pointer, an index and a length. Nodes are counted as three-quarters full, +/// which is what a map built by ascending insertion settles at; the number is +/// an estimate and is labelled as one. +fn btree_bytes(elements: usize, key: usize, value: usize) -> usize { + if elements == 0 { + return 0; + } + let per_node = 16 + 11 * (key + value); + let nodes = elements.div_ceil(8).max(1); + nodes * per_node +} + +/// The accounting: bytes held, broken down by what holds them. +fn account(functions: &BTreeMap) { + let map = btree_bytes(functions.len(), size_of::(), size_of::()); + let mut names = 0usize; + let mut blocks = 0usize; + let mut succ = 0usize; + let mut calls = 0usize; + let mut tables = 0usize; + let mut targets = 0usize; + let mut provenance = 0usize; + let mut nblocks = 0usize; + let mut nsucc = 0usize; + let mut ncalls = 0usize; + let mut insns = 0u64; + for f in functions.values() { + names += f.name.as_ref().map_or(0, |n| n.capacity()); + provenance += f.provenance.corroborating.capacity() * size_of::(); + nblocks += f.cfg.blocks.len(); + // Four bytes per stored block: the block itself is in the shared + // table, counted once below. + blocks += f.cfg.blocks.len() * size_of::() + 32; + for b in f.cfg.blocks.values() { + nsucc += b.successors.len(); + } + ncalls += f.cfg.calls.len(); + calls += f.cfg.calls.capacity() * size_of::(); + tables += f.cfg.tables.capacity() * size_of::(); + for t in &f.cfg.tables { + targets += t.targets.capacity() * size_of::(); + } + insns += f.cfg.insns() as u64; + } + // The shared block table, counted once rather than once per function that + // names a block. An entry is the block plus the address it starts at. + let table = functions + .values() + .next() + .map(|f| f.cfg.blocks.table()) + .expect("a program with no functions has nothing to account for"); + let distinct = table.len(); + let mut table_bytes = distinct * (size_of::() + size_of::()); + for b in table.blocks() { + // What the allocator hands out, not what the list holds: glibc rounds + // a request up to a 16-byte multiple with an 8-byte header and never + // gives out less than 32 bytes, so one or two successors cost 32. + let want = b.successors.capacity() * size_of::(); + succ += if want == 0 { + 0 + } else { + (want + 8).next_multiple_of(16).max(32) + }; + } + table_bytes += succ; + let total = map + names + blocks + table_bytes + calls + tables + targets + provenance; + let kb = |n: usize| n / 1024; + println!(); + println!("accounted bytes, by what holds them"); + println!( + " function map {:>9} KB {} functions at {} B each, in B-tree nodes", + kb(map), + functions.len(), + size_of::() + ); + println!( + " block ids {:>9} KB {nblocks} stored blocks at 4 B each", + kb(blocks) + ); + println!( + " block table {:>9} KB {distinct} distinct blocks at {} B each, plus {} KB of \ + successor lists for {nsucc} edges", + kb(table_bytes), + size_of::() + size_of::(), + kb(succ) + ); + println!( + " call lists {:>9} KB {ncalls} call sites", + kb(calls) + ); + println!( + " jump tables {:>9} KB plus {} KB of targets", + kb(tables), + kb(targets) + ); + println!(" function names {:>9} KB", kb(names)); + println!( + " provenance {:>9} KB corroborating evidence", + kb(provenance) + ); + println!(" ---"); + println!(" total {:>9} KB", kb(total)); + println!(); + println!( + "per function {} B, per block {} B, per instruction {} B", + total / functions.len().max(1), + total / nblocks.max(1), + total as u64 / insns.max(1) + ); + println!( + "{insns} instructions are decoded and thrown away; one decoded Insn is {} B, so keeping \ + them would cost {} MB", + size_of::(), + insns * size_of::() as u64 / (1024 * 1024) + ); + + // How much of what is stored is the same code stored twice. Functions + // recovered from independent evidence can cover the same bytes, and a + // block map is the largest thing this crate holds, so the answer decides + // whether the memory ceiling is a question of representation or of + // discovery. A sorted vector rather than a set, because a set of eight + // million ranges costs more than the thing being measured. + let mut ranges: Vec<(u64, u64)> = Vec::with_capacity(nblocks); + for f in functions.values() { + for b in f.cfg.blocks.values() { + ranges.push((b.range.start().get(), b.range.end().get())); + } + } + ranges.sort_unstable(); + let distinct = { + let mut n = 0usize; + let mut last = None; + for r in &ranges { + if Some(*r) != last { + n += 1; + last = Some(*r); + } + } + n + }; + // Union of the ranges, which is the code actually reached. + let mut covered = 0u64; + let mut end = 0u64; + for (s, e) in &ranges { + let from = (*s).max(end); + if *e > from { + covered += *e - from; + end = *e; + } + } + println!( + "{nblocks} blocks are {distinct} distinct ranges covering {} KB of code, so {:.1}x of what \ + is stored is the same bytes held by more than one function", + covered / 1024, + nblocks as f64 / distinct.max(1) as f64 + ); +} + +fn main() { + let path = std::env::args().nth(1).expect("usage: phases "); + let mut at = mark("start", 0, String::new()); + + // Read rather than mapped, unlike the CLI, so this line is the whole file + // where `e5r` pays only for the pages it touches. Everything after it is + // the same either way, which is the part being measured. + let data = std::fs::read(&path).expect("read"); + at = mark("file read", at, format!("{} KB of file", data.len() / 1024)); + + let object: Object = e5r_format::load(&data, &LoadOptions::default()).expect("load"); + at = mark( + "load", + at, + format!( + "{} symbols, {} hints", + object.symbols.len(), + object.function_hints.len() + ), + ); + + // One session, each part forced in turn: nothing is dropped, so every + // difference below is what that part keeps. + // A second argument switches gap scanning off, because a prologue scan + // invents functions that overlap the ones real evidence found, and the + // block duplication printed at the end is the number that says whether + // that is where the memory goes. + let opts = Options { + scan_gaps: std::env::args().nth(2).as_deref() != Some("no-scan"), + ..Options::default() + }; + let session = Session::new(object, opts); + + let functions = session.functions(); + let nblocks: usize = functions.values().map(|f| f.cfg.blocks.len()).sum(); + let insns: u64 = functions.values().map(|f| f.cfg.insns() as u64).sum(); + at = mark( + "functions + cfg", + at, + format!( + "{} functions, {nblocks} blocks, {insns} insns, {} noreturn", + functions.len(), + session.noreturn().len() + ), + ); + + at = mark("xrefs", at, format!("{} xrefs", session.xrefs().len())); + + let bytes: usize = session.strings().iter().map(|f| f.text.len()).sum(); + mark( + "strings", + at, + format!( + "{} strings, {} KB of text", + session.strings().len(), + bytes / 1024 + ), + ); + + account(session.functions()); + + let max_insns = functions.values().map(|f| f.cfg.insns()).max().unwrap_or(0); + let max_blocks = functions + .values() + .map(|f| f.cfg.blocks.len()) + .max() + .unwrap_or(0); + println!( + "largest function {max_insns} insns, {max_blocks} blocks, so decoding it into a list \ + costs {} KB on whichever thread has it", + max_insns as usize * size_of::() / 1024 + ); + println!( + "sizeof Function {}, Block {}, Cfg {}, Xref {}, Found {}, JumpTable {}", + size_of::(), + size_of::(), + size_of::(), + size_of::(), + size_of::(), + size_of::(), + ); +} diff --git a/crates/e5r-analysis/src/cache.rs b/crates/e5r-analysis/src/cache.rs new file mode 100644 index 0000000..ea6487b --- /dev/null +++ b/crates/e5r-analysis/src/cache.rs @@ -0,0 +1,466 @@ +//! An on-disk cache of analysis results, keyed by content and options. +//! +//! Derived data, so it lives in the user's cache directory and never beside +//! the binary: `$XDG_CACHE_HOME/e5r` when that is set, `~/.cache/e5r` +//! otherwise. Nothing here is authoritative. Two rules outrank speed: +//! +//! 1. A key that does not match is a miss. There is no partial reuse, no +//! "close enough", and no reading of one part of an entry whose other part +//! was written by a different build. +//! 2. A corrupt or truncated file is a miss with a warning. It is never a +//! panic and never a wrong answer, because a cache that can lie is worse +//! than no cache at all. +//! +//! Each part of the analysis is a separate entry under its own key, so a run +//! that asked only for functions leaves a functions entry, and a later run +//! that also wants strings reuses the first and computes the second. The key +//! of a part covers exactly the inputs that determine it: the file's content +//! hash, the format version, and the options that part reads. Thread count is +//! deliberately absent, because the answer may not depend on it (gate G5). +//! +//! # The file format +//! +//! Hand rolled rather than serde, because the decoder has to survive hostile +//! bytes and every length in it needs to be checked against what is left of +//! the file. All integers are little endian. +//! +//! ```text +//! magic 8 bytes, "E5RCACHE" +//! version u32, FORMAT_VERSION +//! part u8, which analysis part this is +//! key 16 bytes, the part key +//! length u64, payload bytes +//! payload length bytes +//! checksum u64 over everything before it +//! ``` +//! +//! The payload encodings are documented on the functions that write them. + +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +/// Bumped whenever a payload encoding changes. An old entry then fails the +/// version check and is a miss, which is the point: a stale decode that +/// happens to parse is exactly the wrong answer this cache must not give. +pub const FORMAT_VERSION: u32 = 1; + +const MAGIC: &[u8; 8] = b"E5RCACHE"; +const HEADER_LEN: usize = 8 + 4 + 1 + 16 + 8; + +/// Which analysis result an entry holds. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Part { + /// Functions, their graphs, and the no-return set. + Functions, + /// The cross reference index. + Xrefs, + /// Extracted strings. + Strings, +} + +impl Part { + /// The byte that identifies the part inside a file. + fn code(self) -> u8 { + match self { + Part::Functions => 1, + Part::Xrefs => 2, + Part::Strings => 3, + } + } + + /// The filename prefix, so a person can see what is in the directory. + fn name(self) -> &'static str { + match self { + Part::Functions => "funcs", + Part::Xrefs => "xrefs", + Part::Strings => "strings", + } + } +} + +/// A 128-bit hash of a file's bytes, the first half of every part key. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ContentHash([u8; 16]); + +impl ContentHash { + /// Hash a whole file image. + pub fn of(data: &[u8]) -> ContentHash { + ContentHash(hash128(data)) + } + + /// Fold in anything else that changed what was loaded. + /// + /// The object cannot say what load options produced it, and a run with + /// `.eh_frame` reading turned off finds different functions in the same + /// bytes. A caller that varies the load must mix that variation in here, + /// or the two loads share a key and the second gets the first's answer. + pub fn with(self, extra: &[u8]) -> ContentHash { + let mut buf = Vec::with_capacity(16 + extra.len()); + buf.extend_from_slice(&self.0); + buf.extend_from_slice(extra); + ContentHash(hash128(&buf)) + } + + /// The raw bytes, for a caller that wants to key something of its own. + pub fn bytes(&self) -> [u8; 16] { + self.0 + } +} + +/// The key of one cache entry: content, format version and the options that +/// part depends on, mixed into 128 bits. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Key([u8; 16]); + +impl Key { + /// Derive a part key from the content hash and the bytes that describe the + /// options that part reads. + pub fn derive(content: &ContentHash, part: Part, opts: &[u8]) -> Key { + let mut buf = Vec::with_capacity(16 + 4 + 1 + opts.len()); + buf.extend_from_slice(&content.0); + buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes()); + buf.push(part.code()); + buf.extend_from_slice(opts); + Key(hash128(&buf)) + } + + /// Lowercase hex, which is what the filename uses. + pub fn hex(&self) -> String { + let mut s = String::with_capacity(32); + for b in self.0 { + s.push(char::from_digit((b >> 4) as u32, 16).unwrap_or('0')); + s.push(char::from_digit((b & 0xf) as u32, 16).unwrap_or('0')); + } + s + } +} + +/// What a lookup found. +#[derive(Debug)] +pub enum Lookup { + /// A payload whose key, version and checksum all matched. + Hit(Vec), + /// Nothing usable, and nothing wrong: no file, or a file for another key. + Miss, + /// A file that exists and does not hold what it claims to. The caller + /// treats this as a miss and reports the message. + Corrupt(String), +} + +/// A directory of cache entries. +#[derive(Debug, Clone)] +pub struct Cache { + dir: PathBuf, +} + +impl Cache { + /// The conventional location, or `None` when neither `XDG_CACHE_HOME` nor + /// `HOME` says where it is. Does not create anything: a cache that cannot + /// be found is a miss, not an error. + pub fn discover() -> Option { + let base = match std::env::var_os("XDG_CACHE_HOME") { + Some(v) if !v.is_empty() => PathBuf::from(v), + _ => PathBuf::from(std::env::var_os("HOME")?).join(".cache"), + }; + Some(Cache { + dir: base.join("e5r").join("analysis"), + }) + } + + /// A cache in a directory the caller names, for tests and for `--cache-dir`. + pub fn at(dir: impl Into) -> Cache { + Cache { dir: dir.into() } + } + + /// Where entries are written. + pub fn dir(&self) -> &Path { + &self.dir + } + + /// The file one entry lives in. + fn path(&self, part: Part, key: &Key) -> PathBuf { + self.dir.join(format!("{}-{}.bin", part.name(), key.hex())) + } + + /// Read one entry. + /// + /// Every failure short of a decoded, checksummed payload is a miss. The + /// distinction between [`Lookup::Miss`] and [`Lookup::Corrupt`] is only + /// about whether a person should be told. + pub fn get(&self, part: Part, key: &Key) -> Lookup { + let path = self.path(part, key); + let bytes = match fs::read(&path) { + Ok(b) => b, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Lookup::Miss, + Err(e) => return Lookup::Corrupt(format!("cache read failed for {path:?}: {e}")), + }; + match decode_entry(&bytes, part, key) { + Ok(Some(payload)) => Lookup::Hit(payload), + Ok(None) => Lookup::Miss, + Err(why) => Lookup::Corrupt(format!("ignoring damaged cache file {path:?}: {why}")), + } + } + + /// Write one entry, replacing whatever was there. + /// + /// Written to a temporary name and renamed, so a reader never sees half a + /// file and an interrupted write leaves the previous entry intact. The + /// temporary carries the process id because two e5r runs on the same + /// binary are a normal thing to do. + pub fn put(&self, part: Part, key: &Key, payload: &[u8]) -> io::Result<()> { + fs::create_dir_all(&self.dir)?; + let final_path = self.path(part, key); + let tmp = self.dir.join(format!( + "{}-{}.{}.tmp", + part.name(), + key.hex(), + std::process::id() + )); + let mut buf = Vec::with_capacity(HEADER_LEN + payload.len() + 8); + buf.extend_from_slice(MAGIC); + buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes()); + buf.push(part.code()); + buf.extend_from_slice(&key.0); + buf.extend_from_slice(&(payload.len() as u64).to_le_bytes()); + buf.extend_from_slice(payload); + let sum = hash64(&buf); + buf.extend_from_slice(&sum.to_le_bytes()); + match fs::write(&tmp, &buf).and_then(|()| fs::rename(&tmp, &final_path)) { + Ok(()) => Ok(()), + Err(e) => { + let _ = fs::remove_file(&tmp); + Err(e) + } + } + } + + /// Delete one entry. Used when a corrupt file is found, so the next run + /// does not report the same damage again. + pub fn remove(&self, part: Part, key: &Key) { + let _ = fs::remove_file(self.path(part, key)); + } +} + +/// Check the header and the checksum and hand back the payload. +/// +/// `Ok(None)` is a file for a different key or version, which is ordinary. +/// `Err` is a file that claims to be this entry and is not intact. +fn decode_entry(bytes: &[u8], part: Part, key: &Key) -> Result>, String> { + if bytes.len() < HEADER_LEN + 8 { + return Err(format!("{} bytes, shorter than a header", bytes.len())); + } + if &bytes[..8] != MAGIC { + return Err("bad magic".to_string()); + } + let version = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]); + if version != FORMAT_VERSION { + return Ok(None); + } + if bytes[12] != part.code() { + return Ok(None); + } + if bytes[13..29] != key.0 { + return Ok(None); + } + let mut len = [0u8; 8]; + len.copy_from_slice(&bytes[29..37]); + let len = u64::from_le_bytes(len); + // The length is attacker-controlled in the sense that matters here: a + // damaged file can claim any size, so it is checked against what the file + // actually holds before anything is allocated. + let Ok(len) = usize::try_from(len) else { + return Err("payload length does not fit in memory".to_string()); + }; + let Some(end) = HEADER_LEN.checked_add(len) else { + return Err("payload length overflows".to_string()); + }; + if bytes.len() != end + 8 { + return Err(format!( + "claims {len} payload bytes, file holds {}", + bytes.len().saturating_sub(HEADER_LEN + 8) + )); + } + let mut sum = [0u8; 8]; + sum.copy_from_slice(&bytes[end..end + 8]); + if u64::from_le_bytes(sum) != hash64(&bytes[..end]) { + return Err("checksum mismatch".to_string()); + } + Ok(Some(bytes[HEADER_LEN..end].to_vec())) +} + +/// A 64-bit mixing hash over eight bytes at a time. +/// +/// Not cryptographic and not claimed to be: it keys a local cache and detects +/// damage, and both jobs need speed over an image that can be hundreds of +/// megabytes more than they need collision resistance against an adversary. +/// The multiply-xor-shift shape is the usual one; a byte-at-a-time FNV is +/// several times slower on a file this size, which is measurable on the very +/// binaries the cache exists for. +fn hash64_seeded(data: &[u8], seed: u64) -> u64 { + const PRIME: u64 = 0x9e37_79b9_7f4a_7c15; + let mut h = seed ^ (data.len() as u64).wrapping_mul(PRIME); + let mut chunks = data.chunks_exact(8); + for c in &mut chunks { + let w = u64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]); + h ^= w.wrapping_mul(PRIME).rotate_left(31); + h = h + .wrapping_mul(PRIME) + .rotate_left(27) + .wrapping_add(0x165667b1); + } + let mut tail = 0u64; + for (i, b) in chunks.remainder().iter().enumerate() { + tail |= (*b as u64) << (i * 8); + } + h ^= tail.wrapping_mul(PRIME); + h ^= h >> 33; + h = h.wrapping_mul(0xff51_afd7_ed55_8ccd); + h ^= h >> 29; + h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53); + h ^ (h >> 32) +} + +/// The checksum, and half of a key. +fn hash64(data: &[u8]) -> u64 { + hash64_seeded(data, 0x517c_c1b7_2722_0a95) +} + +/// Two independent 64-bit hashes, so an accidental collision needs both to +/// agree. A local cache does not need more than that. +fn hash128(data: &[u8]) -> [u8; 16] { + let a = hash64_seeded(data, 0x243f_6a88_85a3_08d3); + let b = hash64_seeded(data, 0x13198a2e03707344); + let mut out = [0u8; 16]; + out[..8].copy_from_slice(&a.to_le_bytes()); + out[8..].copy_from_slice(&b.to_le_bytes()); + out +} + +/// A little-endian writer. Every encoder in this crate builds on it. +#[derive(Debug, Default)] +pub(crate) struct Enc { + buf: Vec, +} + +impl Enc { + /// A writer with room reserved for what the caller expects to write. + pub(crate) fn with_capacity(n: usize) -> Enc { + Enc { + buf: Vec::with_capacity(n), + } + } + + pub(crate) fn u8(&mut self, v: u8) { + self.buf.push(v); + } + + pub(crate) fn u32(&mut self, v: u32) { + self.buf.extend_from_slice(&v.to_le_bytes()); + } + + pub(crate) fn u64(&mut self, v: u64) { + self.buf.extend_from_slice(&v.to_le_bytes()); + } + + pub(crate) fn bool(&mut self, v: bool) { + self.buf.push(v as u8); + } + + /// A length-prefixed string. + pub(crate) fn str(&mut self, s: &str) { + self.u32(s.len() as u32); + self.buf.extend_from_slice(s.as_bytes()); + } + + /// An optional string: a flag, then the string when there is one. + pub(crate) fn opt_str(&mut self, s: Option<&str>) { + match s { + Some(s) => { + self.u8(1); + self.str(s); + } + None => self.u8(0), + } + } + + pub(crate) fn finish(self) -> Vec { + self.buf + } +} + +/// A little-endian reader over bytes that may be damaged. +/// +/// Every read is checked and returns `None` past the end, so a decoder built +/// on it propagates a short read instead of panicking. Nothing here indexes a +/// slice directly, which is the property the corruption test exists to hold. +#[derive(Debug)] +pub(crate) struct Dec<'a> { + buf: &'a [u8], + at: usize, +} + +impl<'a> Dec<'a> { + pub(crate) fn new(buf: &'a [u8]) -> Dec<'a> { + Dec { buf, at: 0 } + } + + /// Bytes not yet read, which is the ceiling on any count the file claims. + pub(crate) fn remaining(&self) -> usize { + self.buf.len().saturating_sub(self.at) + } + + fn take(&mut self, n: usize) -> Option<&'a [u8]> { + let end = self.at.checked_add(n)?; + let out = self.buf.get(self.at..end)?; + self.at = end; + Some(out) + } + + pub(crate) fn u8(&mut self) -> Option { + self.take(1).map(|b| b[0]) + } + + pub(crate) fn u32(&mut self) -> Option { + let b = self.take(4)?; + Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) + } + + pub(crate) fn u64(&mut self) -> Option { + let b = self.take(8)?; + Some(u64::from_le_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) + } + + pub(crate) fn bool(&mut self) -> Option { + match self.u8()? { + 0 => Some(false), + 1 => Some(true), + // Anything else is damage, not a boolean. + _ => None, + } + } + + pub(crate) fn str(&mut self) -> Option { + let n = self.u32()? as usize; + let b = self.take(n)?; + String::from_utf8(b.to_vec()).ok() + } + + pub(crate) fn opt_str(&mut self) -> Option> { + match self.u8()? { + 0 => Some(None), + 1 => Some(Some(self.str()?)), + _ => None, + } + } + + /// A count the file claims, refused when it is larger than the bytes left + /// could possibly encode. Without this a damaged length reserves gigabytes + /// before the read that would have failed. + pub(crate) fn count(&mut self, min_element_bytes: usize) -> Option { + let n = usize::try_from(self.u64()?).ok()?; + (n.saturating_mul(min_element_bytes.max(1)) <= self.remaining()).then_some(n) + } +} diff --git a/crates/e5r-analysis/src/cfg.rs b/crates/e5r-analysis/src/cfg.rs new file mode 100644 index 0000000..c3ab9bb --- /dev/null +++ b/crates/e5r-analysis/src/cfg.rs @@ -0,0 +1,861 @@ +//! Basic blocks and the control flow graph of one function. +//! +//! Built by recursive descent from the entry: decode, follow every direct +//! successor, stop at a return, a trap, or an unresolved indirect branch. +//! Blocks split when a later branch lands inside one already walked, which is +//! why the walk records instruction starts rather than assuming them. + +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::Arc; + +use e5r_arch::{Flow, Insn}; +use e5r_core::{Addr, AddrRange, Arch, Caps, MemoryMap}; + +use crate::data::DataMap; +use crate::jumptable::{self, JumpTable}; + +/// Why a block ended, which is what tells a returning function from one that +/// never comes back. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Terminator { + /// A branch, conditional or not, or a fall-through into another block. + Flow, + /// A return to the caller. + Return, + /// A tail call: the callee returns on this function's behalf. + TailCall, + /// A call to a function that never returns. + NoReturnCall, + /// A breakpoint or an undefined instruction. + Trap, + /// An indirect branch nothing resolved, or bytes that did not decode. + Unresolved, +} + +/// One straight-line run of instructions. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Block { + /// Addresses the block covers. + pub range: AddrRange, + /// Where control can go next, sorted and deduplicated. + pub successors: Vec, + /// How many instructions it holds. + pub insns: u32, + /// True when the block ends somewhere analysis could not follow. + pub unresolved: bool, + /// Why it ended. + pub terminator: Terminator, +} + +/// Where one block sits in the program-wide table. +pub type BlockId = u32; + +/// A block and the address it starts at, which is the key every consumer knows +/// it by. Kept beside the block because a consumer iterating a function wants +/// `(&Addr, &Block)` and an `AddrRange` cannot lend out its start. +#[derive(Debug, Clone, PartialEq, Eq)] +struct Entry { + at: Addr, + block: Block, +} + +/// Every distinct basic block of one program, held once. +/// +/// A C++ binary's shared landing pads and cold paths are branch targets that +/// no symbol names, so every function that can reach one absorbs it +/// independently: on libLLVM, 8.47M blocks stored per function are 2.69M +/// distinct ones, and a block map held per function was 82% of the live bytes. +/// Functions name a block by id instead, which is 4 bytes against 84, and it +/// makes a C and a C++ binary cost the same per byte of code. +/// +/// Chunked because the table grows while functions already built hold a +/// reference to it: a chunk is sealed once and never moves, and a snapshot is +/// the list of chunks that existed when it was taken. An id means the same +/// entry in every snapshot that is long enough to hold it. +#[derive(Debug, Default)] +pub struct BlockTable { + chunks: Vec>>, + /// The first id in each chunk, so an id finds its chunk by binary search. + starts: Vec, +} + +impl BlockTable { + fn entry(&self, id: BlockId) -> Option<&Entry> { + let c = self.starts.partition_point(|s| *s <= id).checked_sub(1)?; + self.chunks[c].get((id - self.starts[c]) as usize) + } + + /// How many distinct blocks it holds. + pub fn len(&self) -> usize { + self.chunks.iter().map(|c| c.len()).sum() + } + + /// True when it holds none. + pub fn is_empty(&self) -> bool { + self.chunks.iter().all(|c| c.is_empty()) + } + + /// Every distinct block, once each, in the order they were first seen. + pub fn blocks(&self) -> impl Iterator + '_ { + self.chunks.iter().flat_map(|c| c.iter().map(|e| &e.block)) + } +} + +/// The blocks of one function: ids into a table shared with every other +/// function, in address order. +/// +/// Reads like the `BTreeMap` it replaces, which is what every +/// consumer in the workspace already asks for. +#[derive(Clone, Default)] +pub struct Blocks { + table: Arc, + ids: Vec, +} + +impl Blocks { + /// How many blocks the function has. + pub fn len(&self) -> usize { + self.ids.len() + } + + /// True when the walk produced none. + pub fn is_empty(&self) -> bool { + self.ids.is_empty() + } + + /// The block starting exactly at `at`. + pub fn get(&self, at: &Addr) -> Option<&Block> { + let pos = self + .ids + .partition_point(|id| self.table.entry(*id).is_some_and(|e| e.at < *at)); + let e = self.table.entry(*self.ids.get(pos)?)?; + (e.at == *at).then_some(&e.block) + } + + /// True when a block starts exactly at `at`. + pub fn contains_key(&self, at: &Addr) -> bool { + self.get(at).is_some() + } + + /// Blocks with their start addresses, in address order. + pub fn iter(&self) -> BlocksIter<'_> { + BlocksIter { + table: &self.table, + ids: self.ids.iter(), + } + } + + /// The blocks, in address order. + pub fn values(&self) -> impl Iterator + '_ { + self.iter().map(|(_, b)| b) + } + + /// The start addresses, in order. + pub fn keys(&self) -> impl Iterator + '_ { + self.iter().map(|(a, _)| a) + } + + /// The table these blocks are held in, which is shared with every other + /// function of the same program. + pub fn table(&self) -> &BlockTable { + &self.table + } +} + +impl std::fmt::Debug for Blocks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + +impl<'a> IntoIterator for &'a Blocks { + type Item = (&'a Addr, &'a Block); + type IntoIter = BlocksIter<'a>; + + fn into_iter(self) -> BlocksIter<'a> { + self.iter() + } +} + +/// One function's blocks, in address order. +pub struct BlocksIter<'a> { + table: &'a BlockTable, + ids: std::slice::Iter<'a, BlockId>, +} + +impl<'a> Iterator for BlocksIter<'a> { + type Item = (&'a Addr, &'a Block); + + fn next(&mut self) -> Option<(&'a Addr, &'a Block)> { + loop { + let id = *self.ids.next()?; + // An id with no entry cannot happen: a function only ever holds + // ids from a snapshot that already contains them. Skipped rather + // than unwrapped so a future caller cannot turn it into a panic. + if let Some(e) = self.table.entry(id) { + return Some((&e.at, &e.block)); + } + } + } + + fn size_hint(&self) -> (usize, Option) { + (0, Some(self.ids.len())) + } +} + +/// Empty slot in the interner's index. +const NO_ID: BlockId = BlockId::MAX; + +/// Builds a [`BlockTable`], collapsing blocks that are identical. +/// +/// Interning happens as each batch of walked functions is merged rather than +/// afterwards, because a pass over finished functions would have to hold the +/// duplicated form and the interned one at once, which is the peak the whole +/// exercise is trying to avoid. +/// +/// The index is open addressing over ids rather than a `HashMap` keyed by the +/// block: a map would store every distinct block a second time as its own key, +/// which costs more than it saves. +pub(crate) struct BlockInterner { + chunks: Vec>>, + starts: Vec, + /// Entries interned since the last seal. + open: Vec, + /// The first id in `open`. + sealed: BlockId, + slots: Vec, + filled: usize, + /// Handed to a function whose table is not published yet, so installing + /// one costs no allocation. + empty: Arc, +} + +impl Default for BlockInterner { + fn default() -> BlockInterner { + BlockInterner { + chunks: Vec::new(), + starts: Vec::new(), + open: Vec::new(), + sealed: 0, + slots: vec![NO_ID; 1024], + filled: 0, + empty: Arc::new(BlockTable::default()), + } + } +} + +impl BlockInterner { + fn entry(&self, id: BlockId) -> Option<&Entry> { + if id >= self.sealed { + return self.open.get((id - self.sealed) as usize); + } + let c = self.starts.partition_point(|s| *s <= id).checked_sub(1)?; + self.chunks[c].get((id - self.starts[c]) as usize) + } + + fn intern(&mut self, at: Addr, block: Block) -> BlockId { + if (self.filled + 1) * 4 >= self.slots.len() * 3 { + self.grow(); + } + let mask = self.slots.len() - 1; + let mut i = hash_block(at, &block) as usize & mask; + while self.slots[i] != NO_ID { + let id = self.slots[i]; + if self + .entry(id) + .is_some_and(|e| e.at == at && e.block == block) + { + return id; + } + i = (i + 1) & mask; + } + let id = self.sealed + self.open.len() as BlockId; + self.open.push(Entry { at, block }); + self.slots[i] = id; + self.filled += 1; + id + } + + fn grow(&mut self) { + let mut slots = vec![NO_ID; self.slots.len() * 2]; + let mask = slots.len() - 1; + for old in std::mem::take(&mut self.slots) { + if old == NO_ID { + continue; + } + let Some(e) = self.entry(old) else { continue }; + let mut i = hash_block(e.at, &e.block) as usize & mask; + while slots[i] != NO_ID { + i = (i + 1) & mask; + } + slots[i] = old; + } + self.slots = slots; + } + + /// Intern one walked function's blocks. The table it names is not + /// published yet; [`BlockInterner::publish`] does that for a whole batch. + pub(crate) fn install(&mut self, raw: RawCfg) -> Cfg { + let ids = raw + .blocks + .into_iter() + .map(|(at, b)| self.intern(at, b)) + .collect(); + Cfg { + entry: raw.entry, + blocks: Blocks { + table: self.empty.clone(), + ids, + }, + calls: raw.calls, + has_indirect: raw.has_indirect, + tables: raw.tables, + halt: raw.halt, + } + } + + /// Seal what has been interned and give it to these functions. + /// + /// Every function installed since the last call must be published before + /// anything reads its blocks. + pub(crate) fn publish<'a>(&mut self, cfgs: impl Iterator) { + let table = self.seal(); + for c in cfgs { + c.blocks.table = table.clone(); + } + } + + fn seal(&mut self) -> Arc { + if !self.open.is_empty() { + let mut chunk = std::mem::take(&mut self.open); + // Sealed chunks are kept for the life of the program, so the + // capacity a doubling push left over is worth one copy to return. + chunk.shrink_to_fit(); + self.starts.push(self.sealed); + self.sealed += chunk.len() as BlockId; + self.chunks.push(Arc::new(chunk)); + } + Arc::new(BlockTable { + chunks: self.chunks.clone(), + starts: self.starts.clone(), + }) + } +} + +/// A block's identity, for the interner's index. +/// +/// FNV rather than the default hasher: this runs once per stored block, eight +/// million times on a large C++ binary, and SipHash over a block costs more +/// than the lookup it keys. +fn hash_block(at: Addr, b: &Block) -> u64 { + let mut h: u64 = 0xcbf2_9ce4_8422_2325; + let mut mix = |v: u64| { + h ^= v; + h = h.wrapping_mul(0x100_0000_01b3); + }; + mix(at.get()); + mix(b.range.end().get()); + mix(b.insns as u64); + mix(b.unresolved as u64 | ((b.terminator as u64) << 1)); + for s in &b.successors { + mix(s.get()); + } + h +} + +/// Why a walk stopped before it ran out of work. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Halt { + /// Everything reachable was walked. + Complete, + /// An instruction cap was reached. + InstructionCap, + /// A block cap was reached. + BlockCap, + /// Bytes ran out, or an encoding was not decodable. + Undecodable, +} + +impl Halt { + /// What to call it in a report. + pub fn as_str(self) -> &'static str { + match self { + Halt::Complete => "complete", + Halt::InstructionCap => "instruction cap", + Halt::BlockCap => "block cap", + Halt::Undecodable => "undecodable", + } + } +} + +/// The control flow graph of one function. +#[derive(Debug, Clone)] +pub struct Cfg { + /// Where the function starts. + pub entry: Addr, + /// Blocks by start address. + pub blocks: Blocks, + /// Direct call targets found inside, sorted and deduplicated. + pub calls: Vec, + /// Addresses reached by an indirect branch we could not resolve. + pub has_indirect: bool, + /// Jump tables resolved inside this function. + pub tables: Vec, + /// Why the walk ended. + pub halt: Halt, +} + +impl Cfg { + /// The span from the lowest to the highest byte the function covers. + /// + /// A hull, not a claim of contiguity: a compiler can put a cold path far + /// from the hot one, and the hull says so rather than hiding it. + pub fn hull(&self) -> AddrRange { + let lo = self.blocks.keys().next().copied().unwrap_or(self.entry); + let hi = self + .blocks + .values() + .map(|b| b.range.end()) + .max() + .unwrap_or(lo); + AddrRange::new(lo, hi).unwrap_or(AddrRange::empty_at(lo)) + } + + /// Total bytes actually covered by blocks, which is under the hull when the + /// function is split. + pub fn covered_bytes(&self) -> u64 { + self.blocks.values().map(|b| b.range.len()).sum() + } + + /// Instructions in the whole function. + pub fn insns(&self) -> u32 { + self.blocks.values().map(|b| b.insns).sum() + } + + /// True when the function was walked to the end with nothing unresolved. + pub fn is_complete(&self) -> bool { + self.halt == Halt::Complete && !self.has_indirect + } +} + +/// A walked function before its blocks are interned. +/// +/// The walk runs on every thread at once and interning is one shared table, so +/// the two are separate steps: a thread produces this, and the merge that +/// follows turns it into a [`Cfg`]. +pub(crate) struct RawCfg { + pub(crate) entry: Addr, + /// Blocks with their start addresses, in address order. + pub(crate) blocks: Vec<(Addr, Block)>, + pub(crate) calls: Vec, + pub(crate) has_indirect: bool, + pub(crate) tables: Vec, + pub(crate) halt: Halt, +} + +/// Walk one function from `entry`. +/// +/// `stop_at` holds the entries of other known functions: reaching one means a +/// tail call, not a continuation, which is the single most common way a +/// disassembler runs two functions together. +pub fn build( + mem: &MemoryMap, + arch: &Arch, + entry: Addr, + stop_at: &BTreeSet, + caps: &Caps, +) -> Cfg { + build_with(mem, arch, entry, stop_at, &BTreeSet::new(), caps) +} + +/// As [`build`], told which functions never return. +/// +/// A call to one of those ends the block: continuing past it walks into +/// whatever the compiler put next, which is usually a literal pool. +/// +/// One function on its own gets a block table of its own. Discovery does not +/// go through here: it shares one table across the whole program, which is +/// where the interning pays. +pub fn build_with( + mem: &MemoryMap, + arch: &Arch, + entry: Addr, + stop_at: &BTreeSet, + noreturn: &BTreeSet, + caps: &Caps, +) -> Cfg { + let raw = build_raw( + mem, + arch, + entry, + stop_at, + noreturn, + caps, + &DataMap::default(), + ); + let mut interner = BlockInterner::default(); + let mut cfg = interner.install(raw); + interner.publish(std::iter::once(&mut cfg)); + cfg +} + +/// The walk itself, leaving the blocks for a caller to intern. +pub(crate) fn build_raw( + mem: &MemoryMap, + arch: &Arch, + entry: Addr, + stop_at: &BTreeSet, + noreturn: &BTreeSet, + caps: &Caps, + data: &DataMap, +) -> RawCfg { + // The executable range the entry sits in bounds every jump table target: + // a switch does not branch into another section. + let section = mem + .segment_at(entry) + .map(|s| s.range) + .unwrap_or(AddrRange::empty_at(entry)); + let mut starts: BTreeSet = BTreeSet::new(); + let mut work: Vec = vec![entry]; + let mut seen: BTreeSet = BTreeSet::new(); + let mut calls: BTreeSet = BTreeSet::new(); + let mut ends: BTreeMap, u32, bool, Terminator)> = BTreeMap::new(); + let mut has_indirect = false; + let mut insn_budget = caps.function_insns; + let mut halt = Halt::Complete; + let mut tables: Vec = Vec::new(); + // Kept per block so an indirect branch can be resolved against the blocks + // that lead to it: the compare that bounds a switch is almost always in a + // predecessor, not in the block that ends with the branch. + let mut bodies: BTreeMap> = BTreeMap::new(); + + while let Some(start) = work.pop() { + if !seen.insert(start) { + continue; + } + if ends.len() as u64 >= caps.function_blocks { + halt = Halt::BlockCap; + break; + } + starts.insert(start); + + let mut at = start; + let mut count = 0u32; + // One past the last instruction decoded, which is where the block ends. + let mut end = start; + // Kept so an indirect branch can be resolved against what led to it. + let mut body: Vec = Vec::new(); + let mut term = Terminator::Flow; + // Thumb's IT block makes the next few instructions conditional with + // nothing in their own halfwords saying so, so the state has to be + // carried from one decode to the next. It starts empty at every block: + // an IT block is a straight run of at most four instructions, and + // branching into the middle of one is unpredictable, so no block ever + // begins inside another's. + let mut it = e5r_arch::arm::ItState::default(); + let (succs, unresolved) = loop { + if insn_budget == 0 { + halt = Halt::InstructionCap; + term = Terminator::Unresolved; + break (Vec::new(), true); + } + // A branch into the middle of this run means the run ends here and + // a new block starts, so stop as soon as we reach a known start. + if at != start && starts.contains(&at) { + break (vec![at], false); + } + // Bytes something proved are data. Decoding them produces + // instructions that were never executed and a block that runs on + // into whatever follows, so the walk stops here and says it did + // not finish. Empty for the evidence-led rounds, which run before + // anything has been proved. + if !data.is_empty() && data.contains(at) { + halt = Halt::Undecodable; + term = Terminator::Unresolved; + break (Vec::new(), true); + } + let Some(window) = mem.decode_window(at, arch.max_insn_len()) else { + halt = Halt::Undecodable; + term = Terminator::Unresolved; + break (Vec::new(), true); + }; + let Some(insn) = decode_stateful(arch, window, at, &mut it) else { + halt = Halt::Undecodable; + term = Terminator::Unresolved; + break (Vec::new(), true); + }; + insn_budget -= 1; + count += 1; + body.push(insn); + let next = insn.next(); + end = next; + + match insn.flow { + Flow::Next => { + at = next; + continue; + } + Flow::Call(t) => { + calls.insert(t); + if noreturn.contains(&t) { + term = Terminator::NoReturnCall; + break (Vec::new(), false); + } + at = next; + continue; + } + Flow::IndirectCall | Flow::Syscall => { + at = next; + continue; + } + Flow::Return => { + term = Terminator::Return; + break (Vec::new(), false); + } + Flow::Trap => { + term = Terminator::Trap; + break (Vec::new(), false); + } + Flow::IndirectBranch => { + // A switch: read the table rather than giving up. + let context = with_predecessors(&bodies, &ends, start, &body); + match jumptable::recover(&context, mem, section, caps, arch.insn_alignment()) { + Some(t) => { + let targets = t.targets.clone(); + tables.push(t); + break (targets, false); + } + None => { + has_indirect = true; + term = Terminator::Unresolved; + break (Vec::new(), true); + } + } + } + Flow::Branch(t) => { + // A branch to another function's entry is a tail call. + if stop_at.contains(&t) && t != entry { + calls.insert(t); + term = if noreturn.contains(&t) { + Terminator::NoReturnCall + } else { + Terminator::TailCall + }; + break (Vec::new(), false); + } + break (vec![t], false); + } + Flow::CondBranch(t) => { + if stop_at.contains(&t) && t != entry { + calls.insert(t); + break (vec![next], false); + } + break (vec![t, next], false); + } + } + }; + + let mut s = succs.clone(); + s.sort_unstable(); + s.dedup(); + ends.insert(start, (end, s.clone(), count, unresolved, term)); + bodies.insert(start, body); + + for t in s { + if !seen.contains(&t) && mem.is_executable(t) { + work.push(t); + } + } + } + + // Split blocks whose range covers another block's start. A conditional + // branch backwards into a run already walked is the usual cause. + // A vector rather than a map: `ends` is a `BTreeMap`, so this fills in + // address order already, and the blocks are about to be interned anyway. + let mut blocks: Vec<(Addr, Block)> = Vec::with_capacity(ends.len()); + for (start, (end, succs, insns, unresolved, term)) in &ends { + // An empty block has nothing to cut, and an excluded range whose + // bounds are equal is a panic rather than an empty iterator. + let cut = if *end > *start { + starts + .range(( + std::ops::Bound::Excluded(*start), + std::ops::Bound::Excluded(*end), + )) + .next() + .copied() + } else { + None + }; + let (real_end, real_succs, real_unresolved, real_term) = match cut { + Some(c) => (c, vec![c], false, Terminator::Flow), + None => (*end, succs.clone(), *unresolved, *term), + }; + let Some(range) = AddrRange::new(*start, real_end) else { + continue; + }; + if range.is_empty() { + continue; + } + blocks.push(( + *start, + Block { + range, + successors: real_succs, + insns: if cut.is_some() { 0 } else { *insns }, + unresolved: real_unresolved, + terminator: real_term, + }, + )); + } + // Recount instructions in any block that was cut. + for (start, b) in blocks.iter_mut() { + if b.insns == 0 { + b.insns = count_insns(mem, arch, *start, b.range.end()); + } + } + + tables.sort_by_key(|t| t.at); + RawCfg { + entry, + blocks, + calls: calls.into_iter().collect(), + has_indirect, + tables, + halt, + } +} + +/// The instructions of `block` preceded by those of the blocks that reach it. +/// +/// Three levels is enough in practice: a compiler puts the bound check +/// immediately before the table setup, and going deeper mostly adds unrelated +/// comparisons that could size a table wrongly. +fn with_predecessors( + bodies: &BTreeMap>, + ends: &BTreeMap, u32, bool, Terminator)>, + block: Addr, + body: &[Insn], +) -> Vec { + let mut chain: Vec = Vec::new(); + let mut frontier = vec![block]; + for _ in 0..3 { + let mut next = Vec::new(); + for target in &frontier { + for (start, (_, succs, _, _, _)) in ends { + if succs.contains(target) && !chain.contains(start) && *start != block { + chain.push(*start); + next.push(*start); + } + } + } + if next.is_empty() { + break; + } + frontier = next; + } + + let mut out: Vec = chain + .iter() + .filter_map(|a| bodies.get(a)) + .flatten() + .copied() + .collect(); + out.sort_by_key(|i| i.addr); + out.extend_from_slice(body); + out +} + +/// How many instructions fit between two addresses. +fn count_insns(mem: &MemoryMap, arch: &Arch, start: Addr, end: Addr) -> u32 { + let mut at = start; + let mut n = 0; + while at < end { + let Some(w) = mem.decode_window(at, arch.max_insn_len()) else { + break; + }; + let Some(i) = e5r_arch::decode(arch, w, at) else { + break; + }; + at = i.next(); + n += 1; + } + n +} + +/// Decode every instruction of a function and hand each one straight to `f`, +/// without building a list of them. +/// +/// Returns false when an instruction arrived at a lower address than the one +/// before it, which can only happen if two blocks of the function overlap. A +/// caller that needs address order then has to fall back to [`instructions`], +/// which sorts. No binary in the corpus does this, and the check is one +/// comparison per instruction against a whole function's worth of decoded +/// instructions held in memory: 224 bytes each, and the largest function in +/// `libcrypto.so.3` has 4,510 of them, on every thread at once. +pub fn for_each_instruction( + mem: &MemoryMap, + arch: &Arch, + cfg: &Cfg, + mut f: impl FnMut(Insn), +) -> bool { + let mut ordered = true; + let mut last: Option = None; + for b in cfg.blocks.values() { + let mut at = b.range.start(); + while at < b.range.end() { + let Some(w) = mem.decode_window(at, arch.max_insn_len()) else { + break; + }; + let Some(i) = e5r_arch::decode(arch, w, at) else { + break; + }; + at = i.next(); + ordered &= last.is_none_or(|p| p <= i.addr); + last = Some(i.addr); + f(i); + } + } + ordered +} + +/// Decode every instruction of a function in address order. +/// +/// Walks the blocks rather than the whole hull, so bytes between a function's +/// split parts are not mistaken for its code. +pub fn instructions(mem: &MemoryMap, arch: &Arch, cfg: &Cfg) -> Vec { + let mut out = Vec::with_capacity(cfg.insns() as usize); + for b in cfg.blocks.values() { + let mut at = b.range.start(); + while at < b.range.end() { + let Some(w) = mem.decode_window(at, arch.max_insn_len()) else { + break; + }; + let Some(i) = e5r_arch::decode(arch, w, at) else { + break; + }; + at = i.next(); + out.push(i); + } + } + out.sort_by_key(|i| i.addr); + out +} + +/// Decode one instruction, carrying the state a run of them needs. +/// +/// Only Thumb has any: every other instruction set this decodes is a function +/// of its own bytes and its address. +fn decode_stateful( + arch: &Arch, + window: &[u8], + at: Addr, + it: &mut e5r_arch::arm::ItState, +) -> Option { + if *arch == Arch::Thumb { + let (insn, next) = e5r_arch::arm::decode_thumb(window, at, *it)?; + *it = next; + return Some(insn); + } + e5r_arch::decode(arch, window, at) +} diff --git a/crates/e5r-analysis/src/codec.rs b/crates/e5r-analysis/src/codec.rs new file mode 100644 index 0000000..f02fa27 --- /dev/null +++ b/crates/e5r-analysis/src/codec.rs @@ -0,0 +1,469 @@ +//! Turning analysis results into cache payloads and back. +//! +//! Fixed-width little-endian fields, no self-description, no schema in the +//! file: the version in the entry header says what the layout is, and a +//! mismatch is a miss rather than a negotiation. Enum values are mapped +//! through an explicit match in both directions, so adding a variant upstream +//! fails the build here instead of silently changing what an old cache file +//! means. +//! +//! Every decoder returns `Option` and every count is checked against the bytes +//! remaining before anything is allocated. A damaged file makes the decode +//! return `None`; it never panics and never returns a half-built answer. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_core::{Addr, AddrRange, Evidence, Provenance}; + +use crate::cache::{Dec, Enc}; +use crate::cfg::{Block, BlockInterner, Cfg, Halt, RawCfg, Terminator}; +use crate::jumptable::{JumpTable, TableKind}; +use crate::program::Function; +use crate::strings::{Encoding, Found}; +use crate::xref::{Xref, XrefIndex, XrefKind}; + +/// Smallest number of bytes one encoded item of each kind can occupy, used to +/// reject a count that the rest of the file could not possibly hold. +const MIN_FUNCTION: usize = 8 + 1 + 16 + 2 + 8 + 8 + 4 + 1 + 4; +const MIN_BLOCK: usize = 8 + 8 + 4 + 1 + 1 + 8; +const MIN_XREF: usize = 8 + 8 + 1; +const MIN_STRING: usize = 8 + 8 + 1 + 4; +const MIN_TABLE: usize = 8 + 8 + 8 + 1 + 8 + 1 + 8 + 1; + +/// Functions, their graphs, the round count and the no-return set. +pub(crate) fn encode_functions( + functions: &BTreeMap, + noreturn: &BTreeSet, + rounds: usize, +) -> Vec { + // Sized from what is actually there rather than from a guess per function: + // on a 139 MB binary the payload is 350 MB, and letting a Vec double its + // way there copies most of that several times over on a machine that is + // already short of memory. The per-item figures are the encoded sizes + // below, rounded up. + let blocks: usize = functions.values().map(|f| f.cfg.blocks.len()).sum(); + let successors: usize = functions + .values() + .flat_map(|f| f.cfg.blocks.values()) + .map(|b| b.successors.len()) + .sum(); + let calls: usize = functions.values().map(|f| f.cfg.calls.len()).sum(); + let mut e = Enc::with_capacity( + 64 + functions.len() * 64 + blocks * 26 + successors * 8 + calls * 8 + noreturn.len() * 8, + ); + e.u64(rounds as u64); + e.u64(functions.len() as u64); + for f in functions.values() { + e.u64(f.entry.get()); + e.opt_str(f.name.as_deref()); + e.u64(f.range.start().get()); + e.u64(f.range.end().get()); + encode_provenance(&mut e, &f.provenance); + encode_cfg(&mut e, &f.cfg); + } + e.u64(noreturn.len() as u64); + for a in noreturn { + e.u64(a.get()); + } + e.finish() +} + +/// The inverse of [`encode_functions`]. `None` on any damage. +#[allow(clippy::type_complexity)] +pub(crate) fn decode_functions( + bytes: &[u8], +) -> Option<(BTreeMap, BTreeSet, usize)> { + let mut d = Dec::new(bytes); + let rounds = usize::try_from(d.u64()?).ok()?; + let n = d.count(MIN_FUNCTION)?; + let mut functions = BTreeMap::new(); + // A cache entry is read back into the same one-table-per-program shape the + // analysis builds, so a hit costs what a miss would have kept. + let mut interner = BlockInterner::default(); + for _ in 0..n { + let entry = Addr::new(d.u64()?); + let name = d.opt_str()?; + let range = AddrRange::new(Addr::new(d.u64()?), Addr::new(d.u64()?))?; + let provenance = decode_provenance(&mut d)?; + let cfg = interner.install(decode_cfg(&mut d)?); + functions.insert( + entry, + Function { + entry, + name, + range, + cfg, + provenance, + }, + ); + } + interner.publish(functions.values_mut().map(|f| &mut f.cfg)); + let n = d.count(8)?; + let mut noreturn = BTreeSet::new(); + for _ in 0..n { + noreturn.insert(Addr::new(d.u64()?)); + } + // A file with bytes left over is a file written by something else. + (d.remaining() == 0).then_some((functions, noreturn, rounds)) +} + +fn encode_provenance(e: &mut Enc, p: &Provenance) { + e.u8(evidence_code(p.best)); + e.u8(p.corroborating.len().min(u8::MAX as usize) as u8); + for ev in p.corroborating.iter().take(u8::MAX as usize) { + e.u8(evidence_code(*ev)); + } +} + +fn decode_provenance(d: &mut Dec<'_>) -> Option { + let best = evidence_from(d.u8()?)?; + let n = d.u8()? as usize; + if n > d.remaining() { + return None; + } + let mut corroborating = Vec::with_capacity(n); + for _ in 0..n { + corroborating.push(evidence_from(d.u8()?)?); + } + Some(Provenance { + best, + corroborating, + }) +} + +fn encode_cfg(e: &mut Enc, c: &Cfg) { + e.u64(c.entry.get()); + e.u64(c.blocks.len() as u64); + for b in c.blocks.values() { + e.u64(b.range.start().get()); + e.u64(b.range.end().get()); + e.u32(b.insns); + e.u8(terminator_code(b.terminator)); + e.bool(b.unresolved); + e.u32(b.successors.len() as u32); + for s in &b.successors { + e.u64(s.get()); + } + } + e.u32(c.calls.len() as u32); + for a in &c.calls { + e.u64(a.get()); + } + e.bool(c.has_indirect); + e.u8(halt_code(c.halt)); + e.u32(c.tables.len() as u32); + for t in &c.tables { + encode_table(e, t); + } +} + +fn decode_cfg(d: &mut Dec<'_>) -> Option { + let entry = Addr::new(d.u64()?); + let n = d.count(MIN_BLOCK)?; + let mut blocks = Vec::with_capacity(n); + for _ in 0..n { + let range = AddrRange::new(Addr::new(d.u64()?), Addr::new(d.u64()?))?; + let insns = d.u32()?; + let terminator = terminator_from(d.u8()?)?; + let unresolved = d.bool()?; + let nsucc = d.u32()? as usize; + if nsucc.saturating_mul(8) > d.remaining() { + return None; + } + let mut successors = Vec::with_capacity(nsucc); + for _ in 0..nsucc { + successors.push(Addr::new(d.u64()?)); + } + blocks.push(( + range.start(), + Block { + range, + successors, + insns, + unresolved, + terminator, + }, + )); + } + let ncalls = d.u32()? as usize; + if ncalls.saturating_mul(8) > d.remaining() { + return None; + } + let mut calls = Vec::with_capacity(ncalls); + for _ in 0..ncalls { + calls.push(Addr::new(d.u64()?)); + } + let has_indirect = d.bool()?; + let halt = halt_from(d.u8()?)?; + let ntables = d.u32()? as usize; + if ntables.saturating_mul(MIN_TABLE) > d.remaining() { + return None; + } + let mut tables = Vec::with_capacity(ntables); + for _ in 0..ntables { + tables.push(decode_table(d)?); + } + Some(RawCfg { + entry, + blocks, + calls, + has_indirect, + tables, + halt, + }) +} + +fn encode_table(e: &mut Enc, t: &JumpTable) { + e.u64(t.at.get()); + e.u64(t.table.get()); + e.u64(t.entry_size); + e.u8(table_kind_code(t.kind)); + e.u64(t.base.get()); + e.u8(t.shift); + e.bool(t.bounded_by_scan); + e.u64(t.targets.len() as u64); + for a in &t.targets { + e.u64(a.get()); + } +} + +fn decode_table(d: &mut Dec<'_>) -> Option { + let at = Addr::new(d.u64()?); + let table = Addr::new(d.u64()?); + let entry_size = d.u64()?; + let kind = table_kind_from(d.u8()?)?; + let base = Addr::new(d.u64()?); + let shift = d.u8()?; + let bounded_by_scan = d.bool()?; + let n = d.count(8)?; + let mut targets = Vec::with_capacity(n); + for _ in 0..n { + targets.push(Addr::new(d.u64()?)); + } + Some(JumpTable { + at, + table, + entry_size, + kind, + base, + shift, + targets, + bounded_by_scan, + }) +} + +/// The cross reference index, in `by_from` order. +pub(crate) fn encode_xrefs(index: &XrefIndex) -> Vec { + let all = index.all(); + let mut e = Enc::with_capacity(8 + all.len() * MIN_XREF); + e.u64(all.len() as u64); + for x in all { + e.u64(x.from.get()); + e.u64(x.to.get()); + e.u8(xref_kind_code(x.kind)); + } + e.finish() +} + +/// The inverse of [`encode_xrefs`]. `None` on any damage. +pub(crate) fn decode_xrefs(bytes: &[u8]) -> Option { + let mut d = Dec::new(bytes); + let n = d.count(MIN_XREF)?; + let mut refs = Vec::with_capacity(n); + for _ in 0..n { + refs.push(Xref { + from: Addr::new(d.u64()?), + to: Addr::new(d.u64()?), + kind: xref_kind_from(d.u8()?)?, + }); + } + (d.remaining() == 0).then(|| XrefIndex::build(refs)) +} + +/// Extracted strings, in the order the scan reported them. +pub(crate) fn encode_strings(found: &[Found]) -> Vec { + let mut e = Enc::with_capacity(8 + found.len() * 48); + e.u64(found.len() as u64); + for f in found { + e.u64(f.addr.get()); + e.u64(f.len); + e.u8(encoding_code(f.encoding)); + e.str(&f.text); + } + e.finish() +} + +/// The inverse of [`encode_strings`]. `None` on any damage. +pub(crate) fn decode_strings(bytes: &[u8]) -> Option> { + let mut d = Dec::new(bytes); + let n = d.count(MIN_STRING)?; + let mut out = Vec::with_capacity(n); + for _ in 0..n { + out.push(Found { + addr: Addr::new(d.u64()?), + len: d.u64()?, + encoding: encoding_from(d.u8()?)?, + text: d.str()?, + }); + } + (d.remaining() == 0).then_some(out) +} + +/// Evidence is written as a number, so the mapping is spelled out rather than +/// taken from the declaration order of the enum: a variant inserted upstream +/// must not change what an existing cache file says. +fn evidence_code(e: Evidence) -> u8 { + match e { + Evidence::SymbolTable => 1, + Evidence::DebugInfo => 2, + Evidence::CodeSymbol => 3, + Evidence::DynamicSymbol => 4, + Evidence::EntryPoint => 5, + Evidence::InitArray => 6, + Evidence::EhFrame => 7, + Evidence::PeUnwind => 8, + Evidence::GuardTable => 9, + Evidence::MachFunctionStarts => 10, + Evidence::GoPclntab => 11, + Evidence::ObjcMetadata => 12, + Evidence::SwiftMetadata => 13, + Evidence::RustPanic => 14, + Evidence::Pdb => 15, + Evidence::Export => 16, + Evidence::ImportThunk => 17, + Evidence::CallTarget => 18, + Evidence::BranchTarget => 19, + Evidence::JumpTable => 20, + Evidence::ProloguePattern => 21, + Evidence::LinearSweep => 22, + Evidence::DataPointer => 23, + Evidence::Annotation => 24, + } +} + +fn evidence_from(v: u8) -> Option { + Some(match v { + 1 => Evidence::SymbolTable, + 2 => Evidence::DebugInfo, + 3 => Evidence::CodeSymbol, + 4 => Evidence::DynamicSymbol, + 5 => Evidence::EntryPoint, + 6 => Evidence::InitArray, + 7 => Evidence::EhFrame, + 8 => Evidence::PeUnwind, + 9 => Evidence::GuardTable, + 10 => Evidence::MachFunctionStarts, + 11 => Evidence::GoPclntab, + 12 => Evidence::ObjcMetadata, + 13 => Evidence::SwiftMetadata, + 14 => Evidence::RustPanic, + 15 => Evidence::Pdb, + 16 => Evidence::Export, + 17 => Evidence::ImportThunk, + 18 => Evidence::CallTarget, + 19 => Evidence::BranchTarget, + 20 => Evidence::JumpTable, + 21 => Evidence::ProloguePattern, + 22 => Evidence::LinearSweep, + 23 => Evidence::DataPointer, + 24 => Evidence::Annotation, + _ => return None, + }) +} + +fn terminator_code(t: Terminator) -> u8 { + match t { + Terminator::Flow => 1, + Terminator::Return => 2, + Terminator::TailCall => 3, + Terminator::NoReturnCall => 4, + Terminator::Trap => 5, + Terminator::Unresolved => 6, + } +} + +fn terminator_from(v: u8) -> Option { + Some(match v { + 1 => Terminator::Flow, + 2 => Terminator::Return, + 3 => Terminator::TailCall, + 4 => Terminator::NoReturnCall, + 5 => Terminator::Trap, + 6 => Terminator::Unresolved, + _ => return None, + }) +} + +fn halt_code(h: Halt) -> u8 { + match h { + Halt::Complete => 1, + Halt::InstructionCap => 2, + Halt::BlockCap => 3, + Halt::Undecodable => 4, + } +} + +fn halt_from(v: u8) -> Option { + Some(match v { + 1 => Halt::Complete, + 2 => Halt::InstructionCap, + 3 => Halt::BlockCap, + 4 => Halt::Undecodable, + _ => return None, + }) +} + +fn table_kind_code(k: TableKind) -> u8 { + match k { + TableKind::Absolute => 1, + TableKind::RelativeToBase => 2, + TableKind::RelativeToEntry => 3, + } +} + +fn table_kind_from(v: u8) -> Option { + Some(match v { + 1 => TableKind::Absolute, + 2 => TableKind::RelativeToBase, + 3 => TableKind::RelativeToEntry, + _ => return None, + }) +} + +fn xref_kind_code(k: XrefKind) -> u8 { + match k { + XrefKind::Call => 1, + XrefKind::Branch => 2, + XrefKind::Data => 3, + XrefKind::Read => 4, + XrefKind::Write => 5, + } +} + +fn xref_kind_from(v: u8) -> Option { + Some(match v { + 1 => XrefKind::Call, + 2 => XrefKind::Branch, + 3 => XrefKind::Data, + 4 => XrefKind::Read, + 5 => XrefKind::Write, + _ => return None, + }) +} + +fn encoding_code(e: Encoding) -> u8 { + match e { + Encoding::Ascii => 1, + Encoding::Utf8 => 2, + Encoding::Utf16 => 3, + } +} + +fn encoding_from(v: u8) -> Option { + Some(match v { + 1 => Encoding::Ascii, + 2 => Encoding::Utf8, + 3 => Encoding::Utf16, + _ => return None, + }) +} diff --git a/crates/e5r-analysis/src/data.rs b/crates/e5r-analysis/src/data.rs new file mode 100644 index 0000000..b53a1a9 --- /dev/null +++ b/crates/e5r-analysis/src/data.rs @@ -0,0 +1,551 @@ +//! Regions that are provably data, and the proof for each one. +//! +//! The code partition has two ways to reach an address: recursive descent from +//! evidence, which only ever follows a real control edge, and the linear sweep +//! over the gaps, which looks at bytes nobody claimed and asks whether they +//! open a function. The sweep is where a literal pool or a jump table gets +//! decoded as instructions, because four bytes of a pool are as likely to +//! match `sub sp, sp, #imm` as any other four bytes are. Marking what is data +//! and stopping the sweep at it is the fix. +//! +//! The rule the whole module is written to is **provably data, not probably +//! data**. A region marked as data that is really code loses a function with +//! no diagnostic anywhere, which is the worst thing this subsystem can +//! produce, so every region carries the [`Proof`] that put it there and +//! anything short of a proof is not marked at all. Two consequences: +//! +//! * A candidate that overlaps a block some function already walked is dropped +//! rather than believed. Instructions were decoded there and control reached +//! them, which outranks anything here. +//! * Nothing is inferred from shape. A run of bytes that looks like a pointer +//! table is not marked; a run of bytes an instruction reads, a symbol +//! declares, or a recovered jump table was read out of, is. +//! +//! What is still missing is relocation targets. A relocation says the linker +//! wrote a value at an address, which is as strong as evidence gets, but +//! `e5r_format::Object` does not carry the relocations it applied, so this +//! crate cannot see them. + +use std::collections::BTreeMap; + +use e5r_arch::{Insn, Operand}; +use e5r_core::{Addr, AddrRange, Arch, MemoryMap}; +use e5r_format::Object; +use rayon::prelude::*; +use serde::Serialize; + +use crate::cfg; +use crate::program::Function; +use crate::progress::{Sink, Stage}; + +/// Why a region is known to hold data. +/// +/// Every variant is something observed rather than something inferred: an +/// instruction that reads the bytes, a container that declares them, or a +/// table recovery that consumed them. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Proof { + /// The entries of a recovered jump table, with the indirect branch they + /// resolve. Every byte in the range was read as a table entry and yielded + /// an aligned, executable, in-section target. + JumpTable { + /// The branch the table resolves. + branch: Addr, + }, + /// The slots of a virtual table the container's own symbol table names and + /// sizes. `_ZTV` under the Itanium ABI, `??_7` under Microsoft's. + Vtable, + /// An object the container's symbol table declares, with a size. The + /// container is saying this is not code. + DeclaredObject, + /// A word an instruction reads through a pc-relative operand: an AArch64 + /// literal pool entry, or the x86-64 `[rip + disp]` form. The instruction + /// that reads it is the proof, and its address is recorded. + LiteralPool { + /// The instruction that reads these bytes. + read_by: Addr, + }, + /// A run of pointer-sized words, in a section the container marks + /// non-executable, each holding the address of something inside a declared + /// section. + /// + /// No target address is carried. A run of pointers is thousands of words + /// on a real binary and one region per word would cost more than the whole + /// block table does; the words are still there to be read. + Pointer, +} + +impl Proof { + /// A short name for reporting. + pub fn label(&self) -> &'static str { + match self { + Proof::JumpTable { .. } => "jump table", + Proof::Vtable => "vtable", + Proof::DeclaredObject => "declared object", + Proof::LiteralPool { .. } => "literal pool", + Proof::Pointer => "pointer", + } + } +} + +/// One region of data and what proves it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub struct Region { + /// The bytes. + pub range: AddrRange, + /// What put it here. + pub proof: Proof, +} + +/// Every region of a program that is known to hold data. +/// +/// Regions may overlap, because two different proofs can cover the same bytes +/// and throwing one away would throw away a reason. Containment questions go +/// through a separate merged list, so asking whether an address is data is a +/// binary search and does not care how many proofs agree. +#[derive(Debug, Clone, Default)] +pub struct DataMap { + regions: Vec, + /// The same bytes, merged into a disjoint sorted list. + covered: Vec, + refused: usize, +} + +impl DataMap { + /// Every region with its proof, sorted by address then by proof. + pub fn regions(&self) -> &[Region] { + &self.regions + } + + /// How many regions there are. + pub fn len(&self) -> usize { + self.regions.len() + } + + /// Candidates dropped because a block some function actually walked + /// covered them. + /// + /// Zero is the expected answer. A nonzero count is a disagreement between + /// a proof here and the code partition, and one of the two is wrong; it is + /// reported rather than resolved, because resolving it silently either way + /// is how a function or a data region disappears without a trace. + pub fn refused(&self) -> usize { + self.refused + } + + /// True when nothing is marked. + pub fn is_empty(&self) -> bool { + self.regions.is_empty() + } + + /// True when this address is inside a region that is known to be data. + pub fn contains(&self, at: Addr) -> bool { + self.run_at(at).is_some() + } + + /// The first proof covering this address, when there is one. + /// + /// Linear, unlike [`DataMap::contains`]: it is for explaining one address + /// to a person, not for a scan. + pub fn proof_at(&self, at: Addr) -> Option<&Region> { + self.regions.iter().find(|r| r.range.contains(at)) + } + + /// One past the last byte of the unbroken data run containing `at`, so a + /// scan can step over the whole of it instead of one unit at a time. + pub fn end_of_run(&self, at: Addr) -> Option { + self.run_at(at).map(|r| r.end()) + } + + /// Total bytes marked. + pub fn bytes(&self) -> u64 { + self.covered.iter().map(|r| r.len()).sum() + } + + /// Bytes marked inside a range, which is how a report says what fraction + /// of `.text` this accounts for. + pub fn bytes_in(&self, span: AddrRange) -> u64 { + self.covered + .iter() + .filter_map(|r| AddrRange::new(r.start().max(span.start()), r.end().min(span.end()))) + .map(|r| r.len()) + .sum() + } + + fn run_at(&self, at: Addr) -> Option { + let i = self.covered.partition_point(|r| r.end() <= at); + self.covered.get(i).copied().filter(|r| r.contains(at)) + } + + /// Assemble from candidate regions, refusing anything that overlaps code + /// that was actually walked. + /// + /// `code` must be sorted and disjoint, as [`crate::program::merge_ranges`] + /// returns. Overlap is the one case where the decision is not close: a + /// block is instructions that control reached, and no proof here outranks + /// that. + fn assemble(mut candidates: Vec, code: &[AddrRange]) -> DataMap { + let before = candidates.len(); + candidates.retain(|r| !r.range.is_empty() && !overlaps(r.range, code)); + let refused = before - candidates.len(); + // Sorted and deduplicated so the answer does not depend on which + // thread produced which region, which is gate G5. + candidates.sort_unstable_by_key(|r| (r.range.start(), r.range.end(), r.proof)); + candidates.dedup(); + + let mut covered: Vec = Vec::with_capacity(candidates.len()); + for r in &candidates { + match covered.last_mut() { + Some(last) if r.range.start() <= last.end() => { + if r.range.end() > last.end() { + *last = AddrRange::new(last.start(), r.range.end()).unwrap_or(*last); + } + } + _ => covered.push(r.range), + } + } + DataMap { + regions: candidates, + covered, + refused, + } + } +} + +/// True when `r` overlaps any of a sorted disjoint list. +fn overlaps(r: AddrRange, sorted: &[AddrRange]) -> bool { + let i = sorted.partition_point(|c| c.end() <= r.start()); + sorted.get(i).is_some_and(|c| c.start() < r.end()) +} + +/// What a build should look for. +/// +/// Split because the two callers want different things. The partition asks +/// before the gap scan and only cares about executable bytes, where scanning a +/// literal pool would invent a function; the published map is for consumers +/// and reports on the data sections too. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Sources { + /// Walk the data sections looking for runs of pointers. + pub(crate) pointers: bool, +} + +/// Everything provably data in this program. +pub(crate) fn build( + object: &Object, + functions: &BTreeMap, + sources: Sources, + sink: Sink<'_>, +) -> DataMap { + // Four sources, so four steps. Each is one pass and none of them is + // interruptible part way, which is why the unit is the source rather than + // the region. + let steps = Some(if sources.pointers { 4 } else { 3 }); + sink.report(Stage::Data, 1, 0, steps); + let mut candidates = declared_objects(object); + sink.report(Stage::Data, 1, 1, steps); + candidates.extend(jump_table_entries(functions)); + sink.report(Stage::Data, 1, 2, steps); + candidates.extend(literal_pools(object, functions)); + sink.report(Stage::Data, 1, 3, steps); + if sources.pointers { + candidates.extend(pointer_runs(object)); + sink.report(Stage::Data, 1, 4, steps); + } + let code = crate::program::merge_ranges( + functions + .values() + .flat_map(|f| f.cfg.blocks.values().map(|b| b.range)), + ); + DataMap::assemble(candidates, &code) +} + +/// Data the container's own symbol table declares, with its declared size. +/// +/// A symbol of object kind with a size is the container saying, in the file, +/// that these bytes are not instructions. It is the only proof available for a +/// vtable from inside this crate: the scanning recovery that finds unnamed +/// ones lives above us, and a scan is an inference anyway. +fn declared_objects(object: &Object) -> Vec { + let mem = &object.memory; + object + .symbols + .iter() + .filter(|s| { + s.kind == e5r_format::SymbolKind::Object + && s.size > 0 + && s.addr != Addr::ZERO + && mem.is_mapped(s.addr) + }) + .filter_map(|s| { + let range = AddrRange::sized(s.addr, s.size)?; + // The declared size can run past what is mapped in a malformed or + // unusual file; the claim is only about bytes that exist. + let end = range.end().min(mem.bounds()?.end()); + let range = AddrRange::new(range.start(), end)?; + let proof = if is_vtable_name(&s.name) { + Proof::Vtable + } else { + Proof::DeclaredObject + }; + Some(Region { range, proof }) + }) + .collect() +} + +/// Whether a mangled name is a virtual table. +fn is_vtable_name(name: &str) -> bool { + // Itanium: `_ZTV`, plus the construction tables a class with + // virtual bases carries. Microsoft: `??_7@@6B@`. + name.starts_with("_ZTV") || name.starts_with("_ZTT") || name.starts_with("??_7") +} + +/// The entries every recovered jump table was read out of. +fn jump_table_entries(functions: &BTreeMap) -> Vec { + functions + .values() + .flat_map(|f| f.cfg.tables.iter()) + .filter_map(|t| { + let len = t.entry_size.checked_mul(t.targets.len() as u64)?; + Some(Region { + range: AddrRange::sized(t.table, len)?, + proof: Proof::JumpTable { branch: t.at }, + }) + }) + .collect() +} + +/// Words an instruction reads through a pc-relative operand. +/// +/// One streaming pass over the instructions of every function, on the same +/// threads the rest of analysis runs on. Streamed rather than decoded into a +/// list first, for the reason [`crate::program::build_xrefs`] gives: a decoded +/// instruction is much larger than what it contributes. +fn literal_pools(object: &Object, functions: &BTreeMap) -> Vec { + let mem = &object.memory; + let arch = object.arch.clone(); + let per_thread: Vec> = functions + .values() + .collect::>() + .par_iter() + .fold(Vec::new, |mut out: Vec, f| { + cfg::for_each_instruction(mem, &arch, &f.cfg, |i| { + if let Some(r) = literal_read(&i, &arch, mem) { + out.push(r); + } + }); + out + }) + .collect(); + per_thread.concat() +} + +/// The bytes one instruction reads as data, when it names them pc-relatively. +fn literal_read(i: &Insn, arch: &Arch, mem: &MemoryMap) -> Option { + let (at, size) = match arch { + // A64 has one pc-relative load and the decoder resolves its target to + // an `Addr` operand. `adr` and `adrp` carry an `Addr` too and read + // nothing, so the mnemonic has to say which this is. + Arch::AArch64 => { + let size = match i.mnemonic { + // The register width is the transfer width, except for the + // sign-extending form, which writes 64 bits from 32. + "ldrsw" => 4, + "ldr" => match i.operands().first() { + Some(Operand::Reg(r)) => r.width.bytes(), + _ => return None, + }, + _ => return None, + }; + match i.operands().get(1) { + Some(Operand::Addr(a)) => (*a, size), + _ => return None, + } + } + // Everywhere else, a memory operand whose base is the program counter. + // A zero transfer size is an address computation such as `lea`, which + // says nothing about what is there. + _ => i.operands().iter().find_map(|o| match o { + Operand::Mem(m) if m.size > 0 => Some((m.pc_target(i.end())?, m.size)), + _ => None, + })?, + }; + if !mem.is_mapped(at) { + return None; + } + Some(Region { + range: AddrRange::sized(at, size)?, + proof: Proof::LiteralPool { read_by: i.addr }, + }) +} + +/// Runs of pointer-sized words in the data sections. +/// +/// Only in sections the container marks non-executable and that have bytes in +/// the file, so nothing here can cost a function: the partition never looks +/// there. What it buys is a map of the pointer tables an analyst and a later +/// data-to-data cross reference pass both want, and the claim it makes is +/// weaker than the other four, which is why it is its own proof. +fn pointer_runs(object: &Object) -> Vec { + let width = object.bits.bytes(); + if !(2..=8).contains(&width) { + return Vec::new(); + } + let little = object.endian == e5r_core::Endian::Little; + let mem = &object.memory; + // Merged first, so "points into a known section" is one binary search per + // word. It runs over every pointer-aligned word of every data section, and + // a linear walk of the section list there is the difference between a pass + // that costs nothing and one that shows up in the wall time. + let known = crate::program::merge_ranges( + object + .sections + .iter() + .filter(|s| !s.range.is_empty()) + .map(|s| s.range), + ); + + let mut out: Vec = Vec::new(); + for s in &object.sections { + if s.exec || s.file_size == 0 || s.range.is_empty() { + continue; + } + let start = Addr(s.range.start().get().next_multiple_of(width)); + // Borrowed once rather than read word by word: `read_ptr` finds the + // segment again for every call, which on a megabyte of `.data.rel.ro` + // is a million lookups for the same segment. + let len = s.range.end().get().saturating_sub(start.get()) & !(width - 1); + let Some(bytes) = mem.slice(start, len) else { + continue; + }; + // A run in progress, so a table of a thousand pointers is one region + // rather than a thousand. + let mut run: Option<(Addr, Addr)> = None; + for (n, word) in bytes.chunks_exact(width as usize).enumerate() { + let at = start.wrapping_offset((n as u64 * width) as i64); + let value = word_value(word, little); + let points_in = value != 0 && in_any(Addr(value), &known); + let next = at.wrapping_offset(width as i64); + match (&mut run, points_in) { + (Some((_, end)), true) => *end = next, + (None, true) => run = Some((at, next)), + (Some(_), false) => { + if let Some((lo, hi)) = run.take() + && let Some(range) = AddrRange::new(lo, hi) + { + out.push(Region { + range, + proof: Proof::Pointer, + }); + } + } + (None, false) => {} + } + } + if let Some((lo, hi)) = run + && let Some(range) = AddrRange::new(lo, hi) + { + out.push(Region { + range, + proof: Proof::Pointer, + }); + } + } + out +} + +/// One word of a data section, in the image's byte order. +fn word_value(word: &[u8], little: bool) -> u64 { + let mut v = 0u64; + if little { + for b in word.iter().rev() { + v = (v << 8) | *b as u64; + } + } else { + for b in word { + v = (v << 8) | *b as u64; + } + } + v +} + +/// True when an address falls inside one of a sorted disjoint list of ranges. +fn in_any(at: Addr, sorted: &[AddrRange]) -> bool { + let i = sorted.partition_point(|r| r.end() <= at); + sorted.get(i).is_some_and(|r| r.contains(at)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn r(lo: u64, hi: u64, proof: Proof) -> Region { + Region { + range: AddrRange::new(Addr(lo), Addr(hi)).unwrap(), + proof, + } + } + + #[test] + fn a_region_that_overlaps_a_walked_block_is_refused() { + // The one rule that matters: instructions control reached outrank + // every proof in this module, because marking them data loses the + // function silently. + let code = [AddrRange::new(Addr(0x1000), Addr(0x1020)).unwrap()]; + let map = DataMap::assemble( + vec![ + r(0x1010, 0x1018, Proof::DeclaredObject), + r(0x1020, 0x1030, Proof::DeclaredObject), + ], + &code, + ); + assert_eq!(map.len(), 1); + assert_eq!(map.regions()[0].range.start(), Addr(0x1020)); + assert!(!map.contains(Addr(0x1010))); + assert!(map.contains(Addr(0x1028))); + } + + #[test] + fn overlapping_proofs_are_both_kept_and_counted_once() { + let map = DataMap::assemble( + vec![ + r(0x2000, 0x2010, Proof::DeclaredObject), + r(0x2000, 0x2008, Proof::JumpTable { branch: Addr(0x40) }), + ], + &[], + ); + assert_eq!(map.len(), 2, "both reasons are worth keeping"); + assert_eq!(map.bytes(), 0x10, "the bytes are counted once"); + assert_eq!(map.end_of_run(Addr(0x2004)), Some(Addr(0x2010))); + } + + #[test] + fn a_run_skips_in_one_step() { + // What the gap scan needs: adjacent regions are one run, so a pool of + // a hundred words costs one step rather than a hundred. + let map = DataMap::assemble( + vec![ + r(0x3000, 0x3008, Proof::LiteralPool { read_by: Addr(1) }), + r(0x3008, 0x3010, Proof::LiteralPool { read_by: Addr(2) }), + r(0x3020, 0x3028, Proof::Pointer), + ], + &[], + ); + assert_eq!(map.end_of_run(Addr(0x3000)), Some(Addr(0x3010))); + assert_eq!(map.end_of_run(Addr(0x3018)), None); + assert_eq!(map.bytes(), 0x18); + assert_eq!( + map.bytes_in(AddrRange::new(Addr(0x3004), Addr(0x3024)).unwrap()), + 0x0c + 0x04 + ); + } + + #[test] + fn a_vtable_symbol_is_told_from_an_ordinary_object() { + assert!(is_vtable_name("_ZTV7Derived")); + assert!(is_vtable_name("??_7Shape@@6B@")); + assert!(!is_vtable_name("_ZTS7Derived")); + assert!(!is_vtable_name("some_table")); + } +} diff --git a/crates/e5r-analysis/src/jumptable.rs b/crates/e5r-analysis/src/jumptable.rs new file mode 100644 index 0000000..67d32c7 --- /dev/null +++ b/crates/e5r-analysis/src/jumptable.rs @@ -0,0 +1,1125 @@ +//! Jump table recovery. +//! +//! A `switch` compiles to an indirect branch through a table, and a +//! disassembler that cannot read the table stops at the branch. That is most of +//! the difference between a function that analyzes completely and one that does +//! not. +//! +//! Recovery simulates the instructions leading to the branch with a small value +//! model, then reads the table. The hard part is knowing where the table ends. +//! Compilers emit compact tables of byte or halfword offsets scaled by the +//! instruction size, and *every* byte value in such a table yields a plausible +//! target, so scanning until an entry looks wrong never stops. The bound has to +//! come from the compare that guards the switch, and the compare has to be +//! matched to the register the table is indexed by. Without that match a stray +//! comparison elsewhere in the function would size the table. Following the +//! moves between the compared register and the indexed one is needed on both +//! architectures, not just on x86: x86 copies the switch value into the +//! register the table is indexed by, and gcc on AArch64 does the same and then +//! reuses the index register for the `adr` anchor, two instructions after the +//! load. So the alias chain is captured where the table is read rather than +//! read back at the branch, where it has already been overwritten. + +use std::collections::BTreeMap; + +use e5r_arch::{Flow, Insn, Mem, Operand, Reg, RegClass}; +use e5r_core::{Addr, AddrRange, Caps, MemoryMap}; +use serde::Serialize; + +/// How a table entry becomes a target address. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TableKind { + /// The entry is the target. + Absolute, + /// The entry is an offset from a single fixed address. + RelativeToBase, + /// The entry is an offset from its own address in the table, which is what + /// a compiler emits when it folds the index into the pointer first. + RelativeToEntry, +} + +/// A recovered table. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct JumpTable { + /// The indirect branch it resolves. + pub at: Addr, + /// Where the entries live. + pub table: Addr, + /// Bytes per entry. + pub entry_size: u64, + /// What the entries mean. + pub kind: TableKind, + /// Where offsets are measured from, for [`TableKind::RelativeToBase`]. + pub base: Addr, + /// How far each entry is shifted before it is added. + pub shift: u8, + /// The targets, in table order. + pub targets: Vec, + /// True when the size came from scanning rather than from a compare. + pub bounded_by_scan: bool, +} + +/// A register, identified across the simulation. +type RegKey = (u8, u8); + +fn key(r: Reg) -> Option { + match r.class { + RegClass::Gpr => Some((0, r.num)), + RegClass::Sp => Some((1, 31)), + _ => None, + } +} + +/// What a register holds, as far as recovery needs to know. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Val { + /// Anything. + Unknown, + /// Another register's unknown value, carried by a move. x86 copies the + /// switch value into the register the table is indexed by, so the compare + /// that bounds the switch names a register the branch never mentions. + Alias(RegKey), + /// A known address the code formed. + Const(u64), + /// `base + index * scale`: a pointer to one element of a table. + Elem { + table: u64, + scale: u64, + index: RegKey, + }, + /// A value loaded from a table. + Entry { + table: u64, + size: u64, + signed: bool, + /// The register the table was indexed by, which is what the guarding + /// compare has to name for its bound to apply. + index: Option, + /// Stride between entries. + scale: u64, + /// True when the load went through an element pointer, so each entry's + /// offset is measured from its own address rather than from one base. + from_elem: bool, + }, + /// An entry added to a base, which is the branch target. + Target { + table: u64, + size: u64, + signed: bool, + index: Option, + scale: u64, + from_elem: bool, + base: u64, + shift: u8, + }, +} + +#[derive(Default)] +struct Regs { + registers: BTreeMap, + /// Stack slots, keyed by the register the address was formed from and the + /// displacement. An unoptimized compiler spills the switch index and + /// reloads it, so without this the index is unknown by the time the branch + /// reads it and the table cannot be bounded. + slots: BTreeMap<(RegKey, i64), Val>, + /// The registers the index value lived in at the moment a table read named + /// that index, newest first. + /// + /// The bound is looked for at the branch, but the chain has to be read + /// where the load is: gcc's `-O1` AArch64 switch indexes the table by `w3` + /// and then writes the `adr` anchor into `x3` two instructions later, which + /// erases the move that carried the compared value there. + index_chains: BTreeMap>, +} + +impl Regs { + fn get(&self, r: Reg) -> Val { + key(r) + .and_then(|k| self.registers.get(&k).copied()) + .unwrap_or(Val::Unknown) + } + + fn set(&mut self, r: Reg, v: Val) { + if let Some(k) = key(r) { + // An alias names this register, so writing it makes every alias to + // it stale. + self.registers.retain(|_, held| *held != Val::Alias(k)); + self.registers.insert(k, v); + // Anything spilled from this register is now stale: the slot holds + // what was written, not what the register holds next. + self.slots.retain(|(base, _), _| *base != k); + } + } + + /// The slot a memory operand names, when it is a simple base and + /// displacement. + fn slot(&self, m: &Mem) -> Option<(RegKey, i64)> { + if m.index.is_some() || m.mode != e5r_arch::AddrMode::Offset { + return None; + } + Some((key(m.base?)?, m.disp)) + } + + fn load_slot(&self, m: &Mem) -> Option { + self.slots.get(&self.slot(m)?).copied() + } + + fn store_slot(&mut self, m: &Mem, v: Val) { + if let Some(k) = self.slot(m) { + self.slots.insert(k, v); + } + } +} + +/// The largest table believed without a bound from a compare. +/// +/// Low on purpose. An unbounded compact table always looks valid, so a large +/// unbounded result is a sign the recovery is wrong rather than that the switch +/// is big. +const SCAN_CAP: u64 = 256; + +/// Try to resolve the indirect branch that ends `insns`. +/// +/// `insns` is the instruction context in address order, ending with the branch; +/// giving it the predecessor blocks as well as the branch's own block is what +/// lets the guarding compare be found. `section` bounds every target and +/// `alignment` is the architecture's instruction alignment. +pub fn recover( + insns: &[Insn], + mem: &MemoryMap, + section: AddrRange, + caps: &Caps, + alignment: u64, +) -> Option { + let alignment = alignment.max(1); + let last = insns.last()?; + if !matches!(last.flow, Flow::IndirectBranch) { + return None; + } + + // Everything before the branch: its operand is a read, and running it + // through the model would clobber the register the table was built in. + let mut regs = Regs::default(); + for i in &insns[..insns.len() - 1] { + step(i, &mut regs); + } + + let shape = match last.operands().first()? { + Operand::Reg(r) => regs.get(*r), + // `jmp qword ptr [rip + table + rax*8]`: the table is the operand. + Operand::Mem(m) if m.seg.is_none() => { + let table = if m.is_pc_relative() { + m.pc_target(last.end())?.get() + } else { + match m.base.map(|b| regs.get(b)) { + Some(Val::Const(c)) => c.wrapping_add(m.disp as u64), + // No base register at all: `jmp [0x2001b8 + rax*8]`, the + // absolute form a non-PIE x86-64 build emits. The + // displacement is the whole table address. + None if m.disp > 0 => m.disp as u64, + _ => return None, + } + }; + let (ix, _, scale) = m.index?; + Val::Entry { + table, + size: 1u64 << scale, + signed: false, + index: key(ix), + scale: 1u64 << scale, + from_elem: false, + } + } + _ => return None, + }; + + let (table, size, signed, index, scale, from_elem, kind, base, shift) = match shape { + Val::Target { + table, + size, + signed, + index, + scale, + from_elem, + base, + shift, + } => { + let kind = if from_elem { + TableKind::RelativeToEntry + } else { + TableKind::RelativeToBase + }; + ( + table, size, signed, index, scale, from_elem, kind, base, shift, + ) + } + // A table of whole pointers needs no arithmetic after the load. + Val::Entry { + table, + size, + signed, + index, + scale, + from_elem, + } if size == 8 => ( + table, + size, + signed, + index, + scale, + from_elem, + TableKind::Absolute, + 0, + 0, + ), + _ => return None, + }; + + if size == 0 || scale == 0 || !mem.is_mapped(Addr(table)) { + return None; + } + + // The compare that guards the switch, matched to the index register or to + // whichever register the index was copied from. The chain recorded at the + // table read wins over the one the registers show now, because the code + // between the read and the branch may have written the index register. + let bound = index.and_then(|ix| { + let chain = regs + .index_chains + .get(&ix) + .cloned() + .unwrap_or_else(|| aliases(ix, ®s)); + bound_for(&chain, &insns[..insns.len() - 1]) + }); + // A compact table of byte or halfword offsets cannot be bounded by + // scanning: every value in range yields a plausible target, so the scan + // would stop wherever the following bytes happened to look wrong. Without + // the guard there is no honest answer, so there is no answer. + if bound.is_none() && size <= 2 && kind != TableKind::Absolute { + return None; + } + let limit = match bound { + Some(n) => n.min(caps.jump_table_entries), + None => SCAN_CAP.min(caps.jump_table_entries), + }; + + let mut targets = Vec::new(); + for n in 0..limit { + let at = match table.checked_add(n * scale) { + Some(a) => Addr(a), + None => break, + }; + let Ok(raw) = mem.read_ptr(at, size, true) else { + break; + }; + let value = if signed { + sign_extend(raw, size) + } else { + raw as i64 + }; + let from = if from_elem { at.get() } else { base }; + let target = match kind { + TableKind::Absolute => Addr(raw), + _ => Addr(from.wrapping_add((value << shift) as u64)), + }; + if !section.contains(target) || !mem.is_executable(target) || target.get() % alignment != 0 + { + break; + } + targets.push(target); + } + + // Two entries is the smallest thing worth calling a switch, and an + // unbounded result that ran to the cap is a recovery that went wrong. + if targets.len() < 2 || (bound.is_none() && targets.len() as u64 >= limit) { + return None; + } + + Some(JumpTable { + at: last.addr, + table: Addr(table), + entry_size: size, + kind, + base: Addr(base), + shift, + targets, + bounded_by_scan: bound.is_none(), + }) +} + +/// Every register the index value has been held in, newest first. +/// +/// Without this, `cmp edi, 10 ; mov eax, edi ; jmp [table + rax*8]` has no +/// bound: the compare names `edi` and the branch names `rax`. An unbounded +/// absolute table then scans into whatever follows it, which for a compiler +/// that packs several tables together is the next switch's entries. +fn aliases(index: RegKey, regs: &Regs) -> Vec { + let mut chain = vec![index]; + let mut at = index; + while let Some(Val::Alias(next)) = regs.registers.get(&at) { + if chain.contains(next) { + break; + } + chain.push(*next); + at = *next; + } + chain +} + +/// The switch bound from the compare that guards it. +/// +/// `cmp , #n` means at most `n + 1` entries. The register has to be one +/// the index value lives in, or an unrelated comparison would size the table. +/// The nearest such compare wins, since an outer range check on the same value +/// is looser than the one right above the branch. +fn bound_for(index: &[RegKey], body: &[Insn]) -> Option { + (0..body.len()).rev().find_map(|n| { + let i = &body[n]; + // A subtraction whose only purpose is the flags it sets: an + // unoptimized compiler writes `sub rax, 11` and branches on it rather + // than comparing, and the difference itself is thrown away. The + // branch immediately after is what says so. + let comparison = match i.mnemonic { + "cmp" | "subs" => true, + "sub" => body + .get(n + 1) + .is_some_and(|next| matches!(next.flow, Flow::CondBranch(_))), + _ => false, + }; + if !comparison { + return None; + } + let Some(Operand::Reg(r)) = i.operands().first() else { + return None; + }; + if !key(*r).is_some_and(|k| index.contains(&k)) { + return None; + } + match i.operands().get(1) { + Some(Operand::Imm(v)) if *v >= 0 && *v < 1 << 20 => Some(*v as u64 + 1), + Some(Operand::UImm(v)) if *v < 1 << 20 => Some(*v + 1), + _ => None, + } + }) +} + +fn sign_extend(v: u64, bytes: u64) -> i64 { + match bytes { + 1 => v as u8 as i8 as i64, + 2 => v as u16 as i16 as i64, + 4 => v as u32 as i32 as i64, + _ => v as i64, + } +} + +/// Advance the value model by one instruction. +fn step(i: &Insn, regs: &mut Regs) { + // These name a register first but write only flags. Treating the first + // operand as a destination would discard the index value at the exact + // instruction that bounds it. + if matches!(i.mnemonic, "cmp" | "cmn" | "test" | "tst") { + return; + } + let ops = i.operands(); + let dest = match ops.first() { + Some(Operand::Reg(r)) => Some(*r), + _ => None, + }; + + let value = match i.mnemonic { + // Address formation: AArch64 in two steps, x86 in one lea. + "adrp" | "adr" => match ops.get(1) { + Some(Operand::Addr(a)) => Some(Val::Const(a.get())), + _ => None, + }, + "lea" => match ops.get(1) { + Some(Operand::Mem(m)) => { + m.pc_target(i.end()) + .map(|t| Val::Const(t.get())) + .or_else(|| match m.base.map(|b| regs.get(b)) { + Some(Val::Const(c)) => Some(Val::Const(c.wrapping_add(m.disp as u64))), + _ => None, + }) + } + _ => None, + }, + "add" => Some(add_value(ops, regs)), + "ldr" | "ldrb" | "ldrh" | "ldrsw" | "ldrsb" | "ldrsh" | "mov" | "movsxd" | "movzx" + | "movsx" => Some(load_value(i, ops, regs)), + _ => None, + }; + + if let Some(d) = dest { + let v = value.unwrap_or(Val::Unknown); + // Captured before the write, because `set` invalidates the aliases + // that name the register being written. + if let Val::Entry { + index: Some(ix), .. + } = v + { + let chain = aliases(ix, regs); + regs.index_chains.insert(ix, chain); + } + regs.set(d, v); + return; + } + // A spill: the destination is memory and the source a register whose value + // the model knows. + if let (Some(Operand::Mem(m)), Some(Operand::Reg(source))) = (ops.first(), ops.get(1)) { + if matches!(i.mnemonic, "mov" | "str" | "stur") { + let v = regs.get(*source); + regs.store_slot(m, v); + } + } +} + +/// The value of an `add` whose parts the model knows. +fn add_value(ops: &[Operand], regs: &Regs) -> Val { + // Three operands on AArch64, two on x86 where the destination is also the + // first source. Reading the two-operand form as if it had three loses the + // value entirely, which is how a switch stays unresolved. + let (a, b) = if ops.len() >= 3 { + ( + operand_value(ops.get(1), regs), + operand_value(ops.get(2), regs), + ) + } else { + ( + operand_value(ops.first(), regs), + operand_value(ops.get(1), regs), + ) + }; + let shift = shift_of(ops); + + match (a, b) { + (Val::Const(x), Val::Const(y)) => Val::Const(x.wrapping_add(y)), + + // A known address plus an unknown scaled register: a pointer to one + // element of a table. + (Val::Const(table), Val::Unknown) | (Val::Unknown, Val::Const(table)) => { + match scaled_index(ops, regs) { + Some(index) if shift > 0 => Val::Elem { + table, + scale: 1u64 << shift, + index, + }, + _ => Val::Unknown, + } + } + + // An entry added to a base: the branch target. + ( + Val::Entry { + table, + size, + signed, + index, + scale, + from_elem, + }, + Val::Const(base), + ) + | ( + Val::Const(base), + Val::Entry { + table, + size, + signed, + index, + scale, + from_elem, + }, + ) => Val::Target { + table, + size, + // The extend on the add decides the sign, not the load: GCC reads + // a table with `ldrb` and adds it with `sxtb`, so the byte is + // signed even though the load was not. + signed: signed_extend(ops).unwrap_or(signed), + index, + scale, + from_elem, + base, + shift, + }, + + // An entry added to the element pointer it came from. + ( + Val::Entry { + table, + size, + signed, + index, + scale, + .. + }, + Val::Elem { .. }, + ) + | ( + Val::Elem { .. }, + Val::Entry { + table, + size, + signed, + index, + scale, + .. + }, + ) => Val::Target { + table, + size, + signed, + index, + scale, + from_elem: true, + base: 0, + shift, + }, + + _ => Val::Unknown, + } +} + +/// Whether an `add`'s extend sign-extends its operand, when it has one. +fn signed_extend(ops: &[Operand]) -> Option { + use e5r_arch::Extend::*; + ops.iter().find_map(|o| match o { + Operand::Extended(_, e, _) => Some(matches!(e, Sxtb | Sxth | Sxtw | Sxtx)), + _ => None, + }) +} + +/// The register an `add`'s shifted or extended operand names. +fn scaled_index(ops: &[Operand], _regs: &Regs) -> Option { + ops.iter().find_map(|o| match o { + Operand::Shifted(r, _, _) | Operand::Extended(r, _, _) => key(*r), + _ => None, + }) +} + +/// The scaling an `add` applies to its shifted or extended operand. +fn shift_of(ops: &[Operand]) -> u8 { + ops.iter() + .find_map(|o| match o { + Operand::Shifted(_, _, n) | Operand::Extended(_, _, n) => Some(*n), + _ => None, + }) + .unwrap_or(0) +} + +fn operand_value(op: Option<&Operand>, regs: &Regs) -> Val { + let v = match op { + Some(Operand::Reg(r)) => regs.get(*r), + Some(Operand::Shifted(r, _, _)) | Some(Operand::Extended(r, _, _)) => regs.get(*r), + Some(Operand::Imm(v)) => Val::Const(*v as u64), + Some(Operand::UImm(v)) => Val::Const(*v), + _ => Val::Unknown, + }; + // An alias says which register a value came from, not what it is; + // arithmetic on it knows no more than it does about anything unknown. + match v { + Val::Alias(_) => Val::Unknown, + v => v, + } +} + +/// The value a load produces, when it reads something the model recognizes. +fn load_value(i: &Insn, ops: &[Operand], regs: &Regs) -> Val { + let Some(Operand::Mem(m)) = ops.get(1) else { + // A register move carries the value along, and when there is no value + // it carries the source register's identity instead. + return match ops.get(1) { + Some(Operand::Reg(src)) => match regs.get(*src) { + Val::Unknown => key(*src).map_or(Val::Unknown, Val::Alias), + v => v, + }, + _ => Val::Unknown, + }; + }; + + // A reload of something this function spilled: the value is what went in. + if let Some(v) = regs.load_slot(m) { + return v; + } + + let signed = matches!(i.mnemonic, "ldrsw" | "ldrsb" | "ldrsh" | "movsxd" | "movsx"); + let size = if m.size == 0 { 8 } else { m.size }; + let base = m.base.map(|b| regs.get(b)); + + match (base, m.index) { + // An indexed read from a known address: a table read. + (Some(Val::Const(table)), Some((ix, _, scale))) => Val::Entry { + table: table.wrapping_add(m.disp as u64), + size, + signed, + index: key(ix), + scale: if scale == 0 { size } else { 1u64 << scale }, + from_elem: false, + }, + // A read through an element pointer: the offset is from the element. + ( + Some(Val::Elem { + table, + scale, + index, + }), + None, + ) => Val::Entry { + table: table.wrapping_add(m.disp as u64), + size, + signed, + index: Some(index), + scale, + from_elem: true, + }, + _ => Val::Unknown, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use e5r_arch::{AddrMode, Extend, Mem, Width}; + use e5r_core::{Perms, Segment}; + + const CODE: Addr = Addr(0x40_0000); + const CODE_LEN: u64 = 0x1000; + const TABLE_OFF: u64 = 0x800; + + /// One executable section of `nop`s with a table inside it. + fn map_with(entries: &[i64], entry_size: usize) -> MemoryMap { + let mut bytes: Vec = + std::iter::repeat_n([0x1f, 0x20, 0x03, 0xd5], (CODE_LEN / 4) as usize) + .flatten() + .collect(); + let mut at = TABLE_OFF as usize; + for e in entries { + bytes[at..at + entry_size].copy_from_slice(&e.to_le_bytes()[..entry_size]); + at += entry_size; + } + let mut m = MemoryMap::new(); + m.add( + Segment::new( + AddrRange::sized(CODE, CODE_LEN).unwrap(), + Perms::RX, + ".text", + 0, + bytes, + ) + .unwrap(), + ); + m + } + + fn table_addr() -> Addr { + Addr(CODE.get() + TABLE_OFF) + } + + fn section() -> AddrRange { + AddrRange::sized(CODE, CODE_LEN).unwrap() + } + + fn gpr(n: u8) -> Reg { + Reg::gpr(n, Width::W64) + } + + fn adrp(at: u64, d: u8, v: u64) -> Insn { + let mut i = Insn::new(Addr(at), 4, "adrp", Flow::Next); + i.push(Operand::Reg(gpr(d))); + i.push(Operand::Addr(Addr(v))); + i + } + + fn cmp_imm(at: u64, r: u8, v: i64) -> Insn { + let mut i = Insn::new(Addr(at), 4, "cmp", Flow::Next); + i.push(Operand::Reg(gpr(r))); + i.push(Operand::Imm(v)); + i + } + + /// `ldrb wd, [xbase, xindex]`, the compact-table read. + fn ldr_indexed(at: u64, d: u8, base: u8, index: u8, size: u64, signed: bool) -> Insn { + let name = match (size, signed) { + (1, false) => "ldrb", + (2, false) => "ldrh", + (4, true) => "ldrsw", + _ => "ldr", + }; + let mut i = Insn::new(Addr(at), 4, name, Flow::Next); + i.push(Operand::Reg(gpr(d))); + i.push(Operand::Mem(Mem { + seg: None, + base: Some(gpr(base)), + index: Some((gpr(index), Extend::Lsl, 0)), + disp: 0, + mode: AddrMode::Offset, + size, + })); + i + } + + /// `add xd, xa, wb, sxtw #shift`. + fn add_scaled(at: u64, d: u8, a: u8, b: u8, shift: u8) -> Insn { + let mut i = Insn::new(Addr(at), 4, "add", Flow::Next); + i.push(Operand::Reg(gpr(d))); + i.push(Operand::Reg(gpr(a))); + i.push(Operand::Extended(gpr(b), Extend::Sxtw, shift)); + i + } + + /// `adr xd, #imm`, which is how gcc forms the anchor a compact table's + /// offsets are measured from. + fn adr(at: u64, d: u8, v: u64) -> Insn { + let mut i = Insn::new(Addr(at), 4, "adr", Flow::Next); + i.push(Operand::Reg(gpr(d))); + i.push(Operand::Addr(Addr(v))); + i + } + + /// `mov xd, xs`. + fn mov_reg(at: u64, d: u8, s: u8) -> Insn { + let mut i = Insn::new(Addr(at), 4, "mov", Flow::Next); + i.push(Operand::Reg(gpr(d))); + i.push(Operand::Reg(gpr(s))); + i + } + + fn br(at: u64, r: u8) -> Insn { + let mut i = Insn::new(Addr(at), 4, "br", Flow::IndirectBranch); + i.push(Operand::Reg(gpr(r))); + i + } + + /// The GCC compact form: byte offsets scaled by four from a fixed base. + fn compact_switch(bound: Option) -> Vec { + let mut v = Vec::new(); + if let Some(n) = bound { + v.push(cmp_imm(0x40_0000, 2, n)); + } + v.push(adrp(0x40_0004, 1, table_addr().get())); + v.push(ldr_indexed(0x40_0008, 0, 1, 2, 1, false)); + v.push(adrp(0x40_000c, 3, CODE.get())); + v.push(add_scaled(0x40_0010, 3, 3, 0, 2)); + v.push(br(0x40_0014, 3)); + v + } + + #[test] + fn a_compact_table_needs_its_guard_to_be_sized() { + // Every byte value yields a plausible target, so without the compare + // there is no honest way to know where the table ends. Recovery must + // refuse rather than invent 256 successors. + let mem = map_with(&[1, 2, 3, 4], 1); + assert!(recover(&compact_switch(None), &mem, section(), &Caps::default(), 4).is_none()); + + let t = recover( + &compact_switch(Some(3)), + &mem, + section(), + &Caps::default(), + 4, + ) + .expect("guarded table"); + assert_eq!(t.targets.len(), 4); + assert!(!t.bounded_by_scan); + assert_eq!(t.kind, TableKind::RelativeToBase); + assert_eq!(t.shift, 2); + assert_eq!(t.targets[0], Addr(CODE.get() + 4)); + assert_eq!(t.targets[3], Addr(CODE.get() + 16)); + } + + #[test] + fn the_guard_has_to_name_the_index_register() { + // A comparison of some other register must not size the table. + let mem = map_with(&[1, 2, 3, 4], 1); + let mut insns = compact_switch(None); + insns.insert(0, cmp_imm(0x3f_fffc, 9, 3)); + assert!(recover(&insns, &mem, section(), &Caps::default(), 4).is_none()); + } + + #[test] + fn an_entry_outside_the_section_ends_the_table() { + // The guard says eight, but the fifth entry points outside the code. + let mem = map_with(&[1, 2, 3, 4, 0x4000, 0, 0, 0], 2); + let mut insns = compact_switch(Some(7)); + insns[2] = ldr_indexed(0x40_0008, 0, 1, 2, 2, false); + let t = recover(&insns, &mem, section(), &Caps::default(), 4).unwrap(); + assert_eq!(t.targets.len(), 4); + } + + #[test] + fn misaligned_targets_are_refused() { + // Offsets that do not land on an instruction boundary are not targets. + let mem = map_with(&[1, 3, 5, 7], 1); + let t = recover( + &compact_switch(Some(3)), + &mem, + section(), + &Caps::default(), + 4, + ); + // 1 << 2 == 4, aligned; 3 << 2 == 12, aligned. All fine at shift 2. + assert!(t.is_some()); + + let unaligned = map_with(&[1, 2, 3, 4], 1); + let mut insns = compact_switch(Some(3)); + insns[4] = add_scaled(0x40_0010, 3, 3, 0, 0); + let t = recover(&insns, &unaligned, section(), &Caps::default(), 4); + // Offsets 1, 2, 3 unscaled are not multiples of four, so nothing is + // believed and there is no table. + assert!(t.is_none()); + } + + #[test] + fn a_branch_through_an_unknown_register_recovers_nothing() { + let mem = map_with(&[1, 2], 1); + assert!(recover(&[br(0x40_0000, 3)], &mem, section(), &Caps::default(), 4).is_none()); + } + + #[test] + fn one_entry_is_not_a_switch() { + let mem = map_with(&[1], 1); + let t = recover( + &compact_switch(Some(0)), + &mem, + section(), + &Caps::default(), + 4, + ); + assert!(t.is_none()); + } + + /// `jmp qword ptr [disp + rax*8]`, with no base register at all. + /// + /// A non-PIE x86-64 build addresses its table absolutely, and clang packs + /// the tables of adjacent switches together, so an unbounded scan runs + /// straight into the next switch's entries and every one of them looks + /// like a valid target. The bound is the only thing that stops it, and the + /// compare names the register the index was copied *from*. + fn x86_absolute_switch(copy: bool) -> Vec { + // With `copy`, the guard names rdi and the branch indexes by rax; + // without it both name rax and no alias is involved. + let guarded = if copy { 7 } else { 0 }; + let mut cmp = Insn::new(Addr(0x40_0000), 3, "cmp", Flow::Next); + cmp.push(Operand::Reg(Reg::gpr(guarded, Width::W32))); + cmp.push(Operand::Imm(3)); + + let mut mov = Insn::new(Addr(0x40_0003), 2, "mov", Flow::Next); + mov.push(Operand::Reg(Reg::gpr(0, Width::W32))); + mov.push(Operand::Reg(Reg::gpr(guarded, Width::W32))); + + let mut jmp = Insn::new(Addr(0x40_0005), 7, "jmp", Flow::IndirectBranch); + jmp.push(Operand::Mem(Mem { + seg: None, + base: None, + index: Some((gpr(0), Extend::Lsl, 3)), + disp: table_addr().get() as i64, + mode: AddrMode::Offset, + size: 8, + })); + vec![cmp, mov, jmp] + } + + #[test] + fn an_absolute_table_with_no_base_register_is_read() { + // Eight entries are present but the guard says four, and the four past + // the guard are the next switch's table. Believing them would invent + // successors. + let entries: Vec = (0..8).map(|n| CODE.get() as i64 + 4 * n).collect(); + let mem = map_with(&entries, 8); + let t = recover( + &x86_absolute_switch(true), + &mem, + section(), + &Caps::default(), + 1, + ) + .expect("absolute table"); + assert_eq!(t.kind, TableKind::Absolute); + assert_eq!(t.table, table_addr()); + assert_eq!(t.entry_size, 8); + assert!(!t.bounded_by_scan); + assert_eq!(t.targets.len(), 4); + assert_eq!(t.targets[0], CODE); + assert_eq!(t.targets[3], Addr(CODE.get() + 12)); + } + + #[test] + fn a_move_carries_the_bound_to_the_register_the_branch_indexes_by() { + // The same code with the compare naming the index register directly + // must recover the same four cases: the move is the only difference, + // so anything else means the bound was found by accident. + let entries: Vec = (0..8).map(|n| CODE.get() as i64 + 4 * n).collect(); + let mem = map_with(&entries, 8); + let direct = recover( + &x86_absolute_switch(false), + &mem, + section(), + &Caps::default(), + 1, + ); + let copied = recover( + &x86_absolute_switch(true), + &mem, + section(), + &Caps::default(), + 1, + ); + assert_eq!(direct, copied); + + // A compare of a register the index never came from still must not + // size the table, and without a bound the scan runs to all eight. + let mut stray = x86_absolute_switch(true); + stray[0] = cmp_imm(0x40_0000, 9, 3); + let t = recover(&stray, &mem, section(), &Caps::default(), 1).expect("unbounded scan"); + assert!(t.bounded_by_scan); + assert_eq!(t.targets.len(), 8); + } + + #[test] + fn a_table_of_offsets_from_its_own_base_is_recognized() { + // lea rcx, [rip + table] ; movsxd rax, [rcx + rax*4] ; add rax, rcx + // The entries are signed 32-bit displacements from the table address, + // which is what a PIE x86-64 build emits. + let entries: Vec = (0..4).map(|n| -(TABLE_OFF as i64) + 4 * n).collect(); + let mem = map_with(&entries, 4); + + let mut lea = Insn::new(Addr(0x40_0000), 7, "lea", Flow::Next); + lea.push(Operand::Reg(gpr(1))); + lea.push(Operand::Mem(Mem { + seg: None, + base: Some(Reg { + class: RegClass::Pc, + num: 0, + width: Width::W64, + }), + index: None, + disp: table_addr().get() as i64 - 0x40_0007, + mode: AddrMode::Offset, + size: 0, + })); + + let mut load = Insn::new(Addr(0x40_0007), 4, "movsxd", Flow::Next); + load.push(Operand::Reg(gpr(0))); + load.push(Operand::Mem(Mem { + seg: None, + base: Some(gpr(1)), + index: Some((gpr(0), Extend::Lsl, 2)), + disp: 0, + mode: AddrMode::Offset, + size: 4, + })); + + let mut add = Insn::new(Addr(0x40_000b), 3, "add", Flow::Next); + add.push(Operand::Reg(gpr(0))); + add.push(Operand::Reg(gpr(1))); + + let mut jmp = Insn::new(Addr(0x40_000e), 2, "jmp", Flow::IndirectBranch); + jmp.push(Operand::Reg(gpr(0))); + + let insns = vec![cmp_imm(0x3f_fffc, 0, 3), lea, load, add, jmp]; + let t = recover(&insns, &mem, section(), &Caps::default(), 1).expect("offset table"); + assert_eq!(t.kind, TableKind::RelativeToBase); + assert_eq!(t.base, table_addr()); + assert_eq!(t.entry_size, 4); + assert_eq!(t.targets.len(), 4); + assert_eq!(t.targets[0], CODE); + assert_eq!(t.targets[3], Addr(CODE.get() + 12)); + } + + /// The gcc `-O1` and `-Os` shape: the anchor is written into the very + /// register the table was indexed by. + /// + /// cmp w2, #3 + /// mov x3, x2 + /// adrp x1, table + /// ldrb w1, [x1, x3] + /// adr x3, anchor ; x3 stops holding the index here + /// add x1, x3, w1, sxtb #2 + /// br x1 + /// + /// The guard names `w2` and the load names `w3`, so the bound is only + /// reachable through the move, and the move is only visible before the + /// `adr` overwrites it. Reading the alias chain at the branch finds + /// nothing and refuses the table, which is a whole switch lost. + fn anchored_switch(reuse_index: bool) -> Vec { + let anchor = if reuse_index { 3 } else { 4 }; + vec![ + cmp_imm(0x40_0000, 2, 3), + mov_reg(0x40_0004, 3, 2), + adrp(0x40_0008, 1, table_addr().get()), + ldr_indexed(0x40_000c, 1, 1, 3, 1, false), + adr(0x40_0010, anchor, CODE.get()), + add_scaled(0x40_0014, 1, anchor, 1, 2), + br(0x40_0018, 1), + ] + } + + #[test] + fn the_anchor_may_land_in_the_index_register() { + let mem = map_with(&[0, 1, 2, 3], 1); + let spare = recover( + &anchored_switch(false), + &mem, + section(), + &Caps::default(), + 4, + ) + .expect("anchor in a spare register"); + let reused = recover(&anchored_switch(true), &mem, section(), &Caps::default(), 4) + .expect("anchor in the index register"); + + // Which register the anchor went to is not a property of the switch, + // so the two have to agree entry for entry. + assert_eq!(spare, reused); + assert_eq!(reused.kind, TableKind::RelativeToBase); + assert_eq!(reused.base, CODE); + assert_eq!(reused.entry_size, 1); + assert_eq!(reused.shift, 2); + assert!(!reused.bounded_by_scan); + assert_eq!(reused.targets.len(), 4); + assert_eq!(reused.targets[0], CODE); + assert_eq!(reused.targets[3], Addr(CODE.get() + 12)); + } + + #[test] + fn an_element_relative_table_is_recognized() { + // add x6, x6, x14, lsl #2 ; ldr w7, [x6] ; add x6, x6, w7, sxtw ; br x6 + // Offsets are measured from each entry's own address. + let mem = map_with(&[-0x800, -0x7fc, -0x7f8, -0x7f4], 4); + let mut add_index = Insn::new(Addr(0x40_0004), 4, "add", Flow::Next); + add_index.push(Operand::Reg(gpr(6))); + add_index.push(Operand::Reg(gpr(6))); + add_index.push(Operand::Shifted(gpr(14), e5r_arch::Shift::Lsl, 2)); + + let mut load = Insn::new(Addr(0x40_0008), 4, "ldrsw", Flow::Next); + load.push(Operand::Reg(gpr(7))); + load.push(Operand::Mem(Mem { + seg: None, + base: Some(gpr(6)), + index: None, + disp: 0, + mode: AddrMode::Offset, + size: 4, + })); + + let mut add_entry = Insn::new(Addr(0x40_000c), 4, "add", Flow::Next); + add_entry.push(Operand::Reg(gpr(6))); + add_entry.push(Operand::Reg(gpr(6))); + add_entry.push(Operand::Extended(gpr(7), Extend::Sxtw, 0)); + + let insns = vec![ + cmp_imm(0x40_0000, 14, 3), + adrp(0x40_0000, 6, table_addr().get()), + add_index, + load, + add_entry, + br(0x40_0010, 6), + ]; + let t = recover(&insns, &mem, section(), &Caps::default(), 4).expect("element-relative"); + assert_eq!(t.kind, TableKind::RelativeToEntry); + assert_eq!(t.targets.len(), 4); + assert_eq!(t.targets[0], Addr(CODE.get())); + } +} diff --git a/crates/e5r-analysis/src/lib.rs b/crates/e5r-analysis/src/lib.rs new file mode 100644 index 0000000..d597e0f --- /dev/null +++ b/crates/e5r-analysis/src/lib.rs @@ -0,0 +1,36 @@ +//! Function discovery, control flow, cross references, and strings. +//! +//! Analysis is parallel and deterministic: each round walks its pending +//! entries across threads, then merges in sorted order, so the answer never +//! depends on which thread finished first. +//! +//! Two ways in. [`Session`] owns a loaded object and computes each part the +//! first time it is asked for, so a caller that wants function names never +//! pays for a string scan; with a [`Cache`] attached it reads those parts back +//! from disk instead of recomputing them. [`analyze`] is the eager shape, a +//! session with every part forced, and is what most of the workspace calls. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod cache; +pub mod cfg; +mod codec; +pub mod data; +pub mod jumptable; +pub mod noreturn; +pub mod program; +pub mod progress; +pub mod session; +pub mod strings; +pub mod xref; + +pub use cache::{Cache, ContentHash, Part}; +pub use cfg::{Block, Cfg, Halt, Terminator}; +pub use data::{DataMap, Proof, Region}; +pub use jumptable::{JumpTable, TableKind}; +pub use program::{Function, Options, Program, Stats, analyze}; +pub use progress::{Sink, Stage, Update}; +pub use session::Session; +pub use strings::Found; +pub use xref::{Xref, XrefIndex, XrefKind}; diff --git a/crates/e5r-analysis/src/noreturn.rs b/crates/e5r-analysis/src/noreturn.rs new file mode 100644 index 0000000..437eb6a --- /dev/null +++ b/crates/e5r-analysis/src/noreturn.rs @@ -0,0 +1,164 @@ +//! Which functions never come back. +//! +//! A call to `abort` is the end of a basic block. Treating it as an ordinary +//! call makes the walk continue into whatever the compiler put next, which is +//! usually a literal pool or the next function, and the result is a function +//! that appears to contain code it does not. +//! +//! Two sources. Some names are known never to return, and that list is short +//! and stable. The rest is a fixpoint over the call graph: a function whose +//! every path ends in a call to something that does not return does not return +//! either. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_core::Addr; + +use crate::cfg::{Cfg, Terminator}; + +/// Names that never return, across the platforms this loads. +/// +/// Matched after demangling is not attempted, so both the C name and the +/// mangled C++ one appear where both exist. +const KNOWN: &[&str] = &[ + "abort", + "exit", + "_exit", + "_Exit", + "quick_exit", + "__assert_fail", + "__assert_fail_base", + "__stack_chk_fail", + "__fortify_fail", + "__chk_fail", + "__libc_fatal", + "longjmp", + "siglongjmp", + "__longjmp_chk", + "pthread_exit", + "_Unwind_Resume", + "__cxa_throw", + "__cxa_rethrow", + "__cxa_bad_cast", + "__cxa_bad_typeid", + "__cxa_pure_virtual", + "_ZSt9terminatev", + "_ZSt10unexpectedv", + "_ZSt20__throw_length_errorPKc", + "_ZSt20__throw_out_of_rangePKc", + "_ZSt17__throw_bad_allocv", + "_ZSt19__throw_logic_errorPKc", + "_ZSt24__throw_out_of_range_fmtPKcz", + "rust_begin_unwind", + "rust_panic", + "_ZN4core9panicking5panic17h", + "ExitProcess", + "TerminateProcess", + "RaiseException", + "_invalid_parameter_noinfo_noreturn", +]; + +/// True when the name is one that never returns. +/// +/// Rust's panic entry points carry a hash suffix, so those are matched by +/// prefix; everything else matches exactly. +pub fn name_never_returns(name: &str) -> bool { + // A PLT thunk stands in for the function it reaches. + let name = name.strip_suffix("@plt").unwrap_or(name); + KNOWN.iter().any(|k| { + if k.ends_with("17h") { + name.starts_with(k) + } else { + name == *k + } + }) || name.starts_with("_ZN4core9panicking") + || name.starts_with("_ZN3std9panicking") +} + +/// Work out which of the recovered functions never return. +/// +/// `named` gives each function's name where one is known, and `graph` gives +/// the calls each one makes. A function is added when its name says so, or +/// when every one of its terminating blocks ends in a call to something +/// already in the set. +pub fn compute( + named: &BTreeMap>, + graph: &BTreeMap, +) -> BTreeSet { + let mut set: BTreeSet = named + .iter() + .filter(|(_, n)| n.as_deref().is_some_and(name_never_returns)) + .map(|(a, _)| *a) + .collect(); + + // A fixpoint, bounded: each round can only add, and there are finitely + // many functions, but a bound keeps a pathological graph from looping. + for _ in 0..8 { + let mut added = false; + for (addr, cfg) in graph { + if set.contains(addr) { + continue; + } + if never_returns(cfg, &set) { + set.insert(*addr); + added = true; + } + } + if !added { + break; + } + } + set +} + +/// True when no path through `cfg` reaches a return. +/// +/// Every block that ends the function has to end in a trap or in a call to +/// something already known not to return. A single `ret`, a tail call to a +/// function that does return, or anything unresolved means the claim cannot be +/// made: the conservative answer is that it returns. +fn never_returns(cfg: &Cfg, set: &BTreeSet) -> bool { + if cfg.blocks.is_empty() || cfg.has_indirect || !cfg.is_complete() { + return false; + } + let mut terminal = 0; + for b in cfg.blocks.values() { + if !b.successors.is_empty() { + continue; + } + terminal += 1; + match b.terminator { + Terminator::Trap | Terminator::NoReturnCall => {} + // A tail call to something in the set is also an exit. + Terminator::TailCall if cfg.calls.iter().any(|c| set.contains(c)) => {} + _ => return false, + } + } + terminal > 0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_known_names_are_recognized() { + assert!(name_never_returns("abort")); + assert!(name_never_returns("exit")); + assert!(name_never_returns("__stack_chk_fail")); + // A PLT thunk stands in for what it reaches. + assert!(name_never_returns("abort@plt")); + assert!(name_never_returns("_ZSt9terminatev")); + assert!(name_never_returns( + "_ZN4core9panicking5panic17habcdef0123456789E" + )); + } + + #[test] + fn ordinary_names_are_not_claimed() { + assert!(!name_never_returns("main")); + assert!(!name_never_returns("printf")); + assert!(!name_never_returns("exit_handler")); + assert!(!name_never_returns("aborted")); + } +} diff --git a/crates/e5r-analysis/src/program.rs b/crates/e5r-analysis/src/program.rs new file mode 100644 index 0000000..bd91410 --- /dev/null +++ b/crates/e5r-analysis/src/program.rs @@ -0,0 +1,949 @@ +//! The analyzed program: functions, their graphs, references and strings. +//! +//! Discovery runs in rounds. Each round walks every pending entry in parallel, +//! then merges the results in sorted order and queues what they found. Sorting +//! before the merge is what makes the answer independent of which thread +//! finished first, which is gate G5. + +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::OnceLock; + +use e5r_arch::Insn; +use e5r_core::{Addr, AddrRange, Arch, Caps, Evidence, Provenance, Strength}; +use e5r_format::Object; +use rayon::prelude::*; + +use crate::cfg::{self, BlockInterner, Cfg, Halt, RawCfg}; +use crate::data::{self, DataMap}; +use crate::noreturn; +use crate::progress::{Sink, Stage}; +use crate::strings::{self, Found}; +use crate::xref::{self, Xref, XrefIndex}; + +/// One recovered function. +#[derive(Debug, Clone)] +pub struct Function { + /// Entry address. + pub entry: Addr, + /// Name, when something named it. + pub name: Option, + /// Lowest to highest byte the blocks cover. + pub range: AddrRange, + /// Its control flow graph. + pub cfg: Cfg, + /// What said this was a function. + pub provenance: Provenance, +} + +impl Function { + /// The name to show, falling back to the conventional address form. + /// + /// Demangled where possible: `std::vector::push_back(int&&)` is what + /// an analyst needs to see, and the mangled form is one command away. + pub fn display_name(&self) -> String { + match &self.name { + Some(n) => e5r_types::pretty(n), + None => format!("sub_{:x}", self.entry.get()), + } + } + + /// The name exactly as the file spells it, still mangled. + pub fn raw_name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// True when the walk finished with nothing unresolved. + pub fn is_complete(&self) -> bool { + self.cfg.is_complete() + } +} + +/// What analysis to do. +#[derive(Debug, Clone)] +pub struct Options { + /// Resource caps. + pub caps: Caps, + /// Follow direct calls to find functions nothing named. + pub follow_calls: bool, + /// Scan gaps between known functions for prologues. Heuristic, so it is + /// tagged as such and can be turned off. + pub scan_gaps: bool, + /// Extract strings. + pub strings: bool, + /// String scanning options. + pub string_opts: strings::Options, + /// Build the cross reference index. + pub xrefs: bool, + /// Work out which functions never return and re-walk the callers that + /// assumed they did. + pub noreturn: bool, + /// Prove which regions hold data, and stop the code partition at them. + /// + /// Off leaves [`Program::data`](crate::Program::data) empty and lets the + /// gap scan read a literal pool as candidate code again. It exists so the + /// difference can be measured, not so it can be switched off. + pub data: bool, + /// Threads to use. `None` means rayon's default. + pub threads: Option, +} + +impl Default for Options { + fn default() -> Self { + Options { + caps: Caps::default(), + follow_calls: true, + scan_gaps: true, + strings: true, + string_opts: strings::Options::default(), + xrefs: true, + noreturn: true, + data: true, + threads: None, + } + } +} + +impl Options { + /// The option bytes one cache part depends on. + /// + /// Exactly the inputs that determine that part's value, and nothing else. + /// Thread count is deliberately absent: the answer may not depend on it + /// (gate G5), so keying on it would be admitting that it does. Including + /// too much would only cost hit rate; including too little would return a + /// wrong answer, so anything doubtful goes in. + pub fn cache_bytes(&self, part: crate::cache::Part) -> Vec { + let mut v = Vec::with_capacity(64); + let c = &self.caps; + for n in [ + c.sections, + c.symbols, + c.relocations, + c.string_len, + c.function_insns, + c.function_blocks, + c.jump_table_entries, + ] { + v.extend_from_slice(&n.to_le_bytes()); + } + match part { + // Cross references are read off the functions, so what changes the + // functions changes them too. + crate::cache::Part::Functions | crate::cache::Part::Xrefs => { + v.push(self.follow_calls as u8); + v.push(self.scan_gaps as u8); + v.push(self.noreturn as u8); + // The data map decides where the gap scan stops looking, so + // it changes which functions exist. + v.push(self.data as u8); + } + crate::cache::Part::Strings => { + v.extend_from_slice(&(self.string_opts.min_len as u64).to_le_bytes()); + v.push(self.string_opts.utf16 as u8); + v.push(self.string_opts.in_code as u8); + } + } + v + } +} + +/// A binary, analyzed. +pub struct Program { + /// What the loader produced. + pub object: Object, + /// Functions by entry address. + pub functions: BTreeMap, + /// Every reference found. + pub xrefs: XrefIndex, + /// Every string found. + pub strings: Vec, + /// Rounds of discovery it took, for reporting. + pub rounds: usize, + /// Functions found never to return. + pub noreturn: BTreeSet, + /// Regions that are provably data, each with its proof. + /// + /// Computed on the first ask rather than eagerly: finding literal pools + /// means decoding every instruction a second time, and the partition has + /// already stopped at what it needed. Pre-filled empty when the options + /// turn it off. + pub(crate) data: OnceLock, +} + +impl Program { + /// Regions that are provably data, computing them if needed. + pub fn data(&self) -> &DataMap { + self.data.get_or_init(|| { + data::build( + &self.object, + &self.functions, + data::Sources { pointers: true }, + Sink::none(), + ) + }) + } + + /// Function whose blocks contain `addr`. + pub fn function_at(&self, addr: Addr) -> Option<&Function> { + // Hulls can overlap when a function is split, so a hull hit still has + // to be confirmed against the blocks. + self.functions + .values() + .filter(|f| f.range.contains(addr)) + .find(|f| f.cfg.blocks.values().any(|b| b.range.contains(addr))) + } + + /// The function starting exactly at `addr`. + pub fn function(&self, addr: Addr) -> Option<&Function> { + self.functions.get(&addr) + } + + /// Functions in address order. + pub fn functions_by_address(&self) -> impl Iterator { + self.functions.values() + } + + /// Decode one function's instructions. + pub fn instructions(&self, f: &Function) -> Vec { + cfg::instructions(&self.object.memory, &self.object.arch, &f.cfg) + } + + /// Name for an address: an exact function, else a symbol, else nothing. + pub fn name_of(&self, addr: Addr) -> Option { + if let Some(f) = self.functions.get(&addr) { + return Some(f.display_name()); + } + self.object + .symbol_at(addr) + // ARM mapping symbols ($x, $d) mark code and data boundaries, not + // names; showing one as a label is worse than showing nothing. + .filter(|s| !s.name.is_empty() && !s.name.starts_with('$')) + .map(|s| { + let name = e5r_types::pretty(&s.name); + if s.addr == addr { + name + } else { + format!("{name}+{:#x}", addr.get() - s.addr.get()) + } + }) + } + + /// A NUL-terminated printable run at `addr`, for annotating a reference + /// the string scan did not report (it is shorter than the floor, or sits + /// in a section the scan skipped). + pub fn text_at(&self, addr: Addr, max: usize) -> Option { + let bytes = self.object.memory.decode_window(addr, max as u64)?; + let end = bytes.iter().position(|&b| b == 0)?; + if end == 0 { + return None; + } + let run = &bytes[..end]; + run.iter() + .all(|&b| matches!(b, 0x20..=0x7e | b'\t' | b'\n' | b'\r')) + .then(|| String::from_utf8_lossy(run).into_owned()) + } + + /// How many functions finished with nothing unresolved. + pub fn complete_count(&self) -> usize { + self.functions.values().filter(|f| f.is_complete()).count() + } +} + +/// Analyze a loaded object, eagerly. +/// +/// Everything the options ask for is computed before this returns. It is a +/// [`Session`](crate::Session) with every part forced, and it stays because a +/// `Program` is what the rest of the workspace wants; a caller that needs only +/// one part should open a session and ask for that part instead. +pub fn analyze(object: Object, opts: &Options) -> Program { + crate::Session::new(object, opts.clone()).into_program() +} + +/// How many functions are walked in parallel before their blocks are interned. +/// The batch is the only thing ever held in the duplicated form, so it bounds +/// the peak; it is large enough to keep every thread busy. +const WALK_BATCH: usize = 8192; + +/// Nothing is known never to return during discovery proper: the no-return +/// pass runs after it and re-walks the callers it affects. +static EMPTY: BTreeSet = BTreeSet::new(); + +/// Intern one batch of walked functions and give them the table they name. +/// +/// Every function in a batch is published together, because a function must +/// not be read before the table holding its blocks exists. +fn intern_batch(interner: &mut BlockInterner, walked: Vec<(Addr, RawCfg)>) -> Vec<(Addr, Cfg)> { + let mut out: Vec<(Addr, Cfg)> = walked + .into_iter() + .map(|(a, raw)| (a, interner.install(raw))) + .collect(); + interner.publish(out.iter_mut().map(|(_, c)| c)); + out +} + +/// One seed: an address to walk and the evidence that put it there. +#[derive(Clone)] +struct Seed { + addr: Addr, + name: Option, + provenance: Provenance, +} + +/// Find functions and walk each one's control flow. +/// +/// The first stage and the only one the others depend on. Returns the +/// functions and how many rounds of discovery it took. +pub(crate) fn discover( + object: &Object, + opts: &Options, + interner: &mut BlockInterner, + sink: Sink<'_>, +) -> (BTreeMap, usize) { + let arch = object.arch.clone(); + let mem = &object.memory; + // Nothing is proved data yet: the proofs are read off the functions these + // rounds are about to find. + let nothing = DataMap::default(); + + // Seeds from the loader, strongest evidence first. Only executable + // addresses: a symbol table can name a function in a section that is not + // mapped, and walking it would decode whatever is nearby. + let mut pending: Vec = object + .function_hints + .iter() + .filter(|h| mem.is_executable(h.addr)) + .map(|h| Seed { + addr: h.addr, + name: h.name.clone(), + provenance: h.provenance.clone(), + }) + .collect(); + + let mut known: BTreeMap = BTreeMap::new(); + let mut evidence: BTreeMap)> = BTreeMap::new(); + for s in &pending { + merge_evidence(&mut evidence, s.addr, &s.provenance, s.name.clone()); + } + let mut rounds = 0; + + // Entries known before a round starts are where a tail call may land. + while !pending.is_empty() { + rounds += 1; + let stop_at: BTreeSet = evidence.keys().copied().collect(); + + let mut batch: Vec = pending + .drain(..) + .filter(|s| !known.contains_key(&s.addr)) + .collect(); + batch.sort_by_key(|s| s.addr); + batch.dedup_by_key(|s| s.addr); + + // A round's own total is known; the stage's is not, because what this + // round walks is what queues the next one. + let round_total = Some(batch.len() as u64); + let mut done = 0u64; + sink.report(Stage::Discovery, rounds as u32, 0, round_total); + + for slice in batch.chunks(WALK_BATCH) { + let walked: Vec<(Addr, RawCfg)> = slice + .par_iter() + .map(|s| { + ( + s.addr, + cfg::build_raw(mem, &arch, s.addr, &stop_at, &EMPTY, &opts.caps, ¬hing), + ) + }) + .collect(); + + for (addr, c) in intern_batch(interner, walked) { + if c.blocks.is_empty() { + continue; + } + if opts.follow_calls { + for t in &c.calls { + if mem.is_executable(*t) && !evidence.contains_key(t) { + let p = Provenance::new(Evidence::CallTarget); + merge_evidence(&mut evidence, *t, &p, None); + pending.push(Seed { + addr: *t, + name: None, + provenance: p, + }); + } else if mem.is_executable(*t) { + let p = Provenance::new(Evidence::CallTarget); + merge_evidence(&mut evidence, *t, &p, None); + } + } + } + let (provenance, name) = evidence + .get(&addr) + .cloned() + .unwrap_or((Provenance::new(Evidence::CallTarget), None)); + known.insert( + addr, + Function { + entry: addr, + name, + range: c.hull(), + cfg: c, + provenance, + }, + ); + } + // Between batches, so the callback is entered from one thread and + // never from inside the parallel walk. + done += slice.len() as u64; + sink.report(Stage::Discovery, rounds as u32, done, round_total); + } + + // Gap scanning runs once, after the evidence-led rounds settle, so it + // never invents a function the real evidence would have found. + if pending.is_empty() && opts.scan_gaps { + // What is provably data, worked out from everything the + // evidence-led rounds found, and then used to keep the sweep out + // of it. This is the one place the partition guesses, so it is the + // one place the marking has to reach. + // + // Only where the sweep runs. It costs a pass over the instructions + // to find the literal pools, and paying that for an architecture + // whose gaps are never swept would be paying for nothing. + let data = if opts.data && object.arch == Arch::AArch64 { + data::build(object, &known, data::Sources { pointers: false }, sink) + } else { + DataMap::default() + }; + let found = scan_gaps(object, &known, &opts.caps, &data); + for a in found { + let p = Provenance::new(Evidence::ProloguePattern); + merge_evidence(&mut evidence, a, &p, None); + pending.push(Seed { + addr: a, + name: None, + provenance: p, + }); + } + if !pending.is_empty() { + // One pass only: what the gap scan found is walked, and what + // that walk finds is not scanned for again. + let batch = std::mem::take(&mut pending); + walk_pending( + object, + &mut known, + &mut evidence, + batch, + interner, + Leftovers { + opts, + data: &data, + sink, + round: rounds as u32 + 1, + }, + ); + return (known, rounds); + } + } + } + + (known, rounds) +} + +/// What one leftover batch is walked under. +/// +/// Bundled rather than passed one by one: these four travel together and +/// nothing else needs them. +#[derive(Clone, Copy)] +struct Leftovers<'a> { + opts: &'a Options, + /// What is known to be data by the time the gap scan runs. + data: &'a DataMap, + sink: Sink<'a>, + /// The round number to report these under, which follows the last real + /// round. + round: u32, +} + +/// Walk seeds that are left over, without following what they call. +/// +/// These are the gap scan's guesses, so unlike the evidence-led rounds they +/// get the data map: a prologue pattern a few words before a literal pool +/// would otherwise decode straight into it. +fn walk_pending( + object: &Object, + known: &mut BTreeMap, + evidence: &mut BTreeMap)>, + pending: Vec, + interner: &mut BlockInterner, + over: Leftovers<'_>, +) { + if pending.is_empty() { + return; + } + let Leftovers { + opts, + data, + sink, + round, + } = over; + let arch = object.arch.clone(); + let mem = &object.memory; + let stop_at: BTreeSet = evidence.keys().copied().collect(); + let mut batch = pending; + batch.sort_by_key(|s| s.addr); + batch.dedup_by_key(|s| s.addr); + let total = Some(batch.len() as u64); + let mut done = 0u64; + sink.report(Stage::Discovery, round, 0, total); + for slice in batch.chunks(WALK_BATCH) { + let walked: Vec<(Addr, RawCfg)> = slice + .par_iter() + .filter(|s| !known.contains_key(&s.addr)) + .map(|s| { + ( + s.addr, + cfg::build_raw(mem, &arch, s.addr, &stop_at, &EMPTY, &opts.caps, data), + ) + }) + .collect(); + for (addr, c) in intern_batch(interner, walked) { + if c.blocks.is_empty() { + continue; + } + let (provenance, name) = evidence + .remove(&addr) + .unwrap_or((Provenance::new(Evidence::ProloguePattern), None)); + known.insert( + addr, + Function { + entry: addr, + name, + range: c.hull(), + cfg: c, + provenance, + }, + ); + } + done += slice.len() as u64; + sink.report(Stage::Discovery, round, done, total); + } +} + +/// Which functions never come back, and a re-walk of the callers that assumed +/// they did. +/// +/// One pass: the set is computed from complete functions, and a re-walk only +/// ever shortens a function, so a second pass finds nothing the first did not. +/// Part of discovery rather than a stage after it, because it corrects the +/// functions themselves. +pub(crate) fn refine_noreturn( + object: &Object, + known: &mut BTreeMap, + opts: &Options, + interner: &mut BlockInterner, + sink: Sink<'_>, +) -> BTreeSet { + if !opts.noreturn { + return BTreeSet::new(); + } + let arch = object.arch.clone(); + let mem = &object.memory; + // The re-walk only ever shortens a function, so it cannot reach bytes the + // first walk did not. + let nothing = DataMap::default(); + let named: BTreeMap> = + known.iter().map(|(a, f)| (*a, f.name.clone())).collect(); + let graph: BTreeMap = known.iter().map(|(a, f)| (*a, &f.cfg)).collect(); + let set = noreturn::compute(&named, &graph); + let affected: Vec = known + .iter() + .filter(|(a, f)| !set.contains(a) && f.cfg.calls.iter().any(|c| set.contains(c))) + .map(|(a, _)| *a) + .collect(); + let stop_at: BTreeSet = known.keys().copied().collect(); + let total = Some(affected.len() as u64); + let mut rebuilt_count = 0u64; + sink.report(Stage::NoReturn, 1, 0, total); + for slice in affected.chunks(WALK_BATCH) { + let rebuilt: Vec<(Addr, RawCfg)> = slice + .par_iter() + .map(|a| { + ( + *a, + cfg::build_raw(mem, &arch, *a, &stop_at, &set, &opts.caps, ¬hing), + ) + }) + .collect(); + for (a, c) in intern_batch(interner, rebuilt) { + if c.blocks.is_empty() { + continue; + } + if let Some(f) = known.get_mut(&a) { + f.range = c.hull(); + f.cfg = c; + } + } + rebuilt_count += slice.len() as u64; + sink.report(Stage::NoReturn, 1, rebuilt_count, total); + } + set +} + +/// Give every function the finished table, so the snapshots taken while +/// discovery was running can be dropped. +pub(crate) fn publish_blocks(known: &mut BTreeMap, interner: &mut BlockInterner) { + interner.publish(known.values_mut().map(|f| &mut f.cfg)); +} + +/// Every reference the recovered functions make. +pub(crate) fn build_xrefs( + object: &Object, + known: &BTreeMap, + sink: Sink<'_>, +) -> XrefIndex { + let arch = object.arch.clone(); + let mem = &object.memory; + // One list per unit of parallel work rather than one per function. A + // function makes 25 references on average, so a list of its own is a few + // hundred bytes that doubled its way there, and 127,229 of those left the + // allocator holding several times what the finished index needs: the stage + // cost 543 MB resident for 155 MB of index. + let functions: Vec<&Function> = known.values().collect(); + let total = Some(functions.len() as u64); + let mut done = 0u64; + sink.report(Stage::Xrefs, 1, 0, total); + let mut all: Vec> = Vec::new(); + // Walked in batches rather than in one sweep, so there is a sequential + // point to report from. The answer does not depend on the batching: the + // index sorts what it is given. + for slice in functions.chunks(WALK_BATCH) { + let part: Vec> = slice + .par_iter() + .fold(Vec::new, |mut out: Vec, f| { + let start = out.len(); + // Streamed rather than decoded into a list first: this runs on + // every thread at once, and a decoded instruction is an order of + // magnitude larger than the references it produces. + let mut c = xref::Collector::new(mem, &mut out); + let ordered = cfg::for_each_instruction(mem, &arch, &f.cfg, |i| c.push(i)); + c.finish(); + if !ordered { + // Overlapping blocks, which nothing in the corpus produces. + // The tracker reads instructions in address order, so pay for + // the sort rather than answer differently. Only this + // function's references are dropped, not the whole list. + out.truncate(start); + let insns = cfg::instructions(mem, &arch, &f.cfg); + xref::collect(&insns, mem, &mut out); + } + out + }) + .collect(); + all.extend(part); + done += slice.len() as u64; + sink.report(Stage::Xrefs, 1, done, total); + } + // Concatenation order is not part of the answer: [`XrefIndex::build`] + // sorts and deduplicates, and every field of a reference takes part in the + // ordering, so the arrays it produces are the same set in the same order + // whatever order the work finished in. That is gate G5. + // + // Sized up front: collecting from a flattening iterator has no size hint, + // so it doubles its way there and holds twice the final index at the peak. + let mut flat: Vec = Vec::with_capacity(all.iter().map(Vec::len).sum()); + for v in all.drain(..) { + flat.extend_from_slice(&v); + } + XrefIndex::build(flat) +} + +/// Where to look for strings. +/// +/// Sections when the container has them, because `.rodata` sits inside an +/// executable segment in the usual ELF layout and the segment's permission +/// says nothing useful about it. Segments otherwise. +pub(crate) fn scan_strings(object: &Object, opts: &strings::Options, sink: Sink<'_>) -> Vec { + let mem = &object.memory; + let mut ranges: Vec = object + .sections + .iter() + .filter(|s| !s.range.is_empty() && s.file_size > 0 && (opts.in_code || !s.exec)) + .map(|s| s.range) + .collect(); + if ranges.is_empty() { + ranges = mem + .segments() + .iter() + .filter(|s| opts.in_code || !s.perms.exec) + .map(|s| s.range) + .collect(); + } + // One range at a time, which is a section, so the report says which part + // of the image is being read. Scanning them together and scanning them one + // by one give the same answer: the sort and the deduplication below are + // what the combined scan ends with anyway. + let total = Some(ranges.len() as u64); + sink.report(Stage::Strings, 1, 0, total); + let mut out: Vec = Vec::new(); + for (n, r) in ranges.iter().enumerate() { + out.extend(strings::scan_ranges(mem, std::slice::from_ref(r), opts)); + sink.report(Stage::Strings, 1, n as u64 + 1, total); + } + out.sort_by_key(|f| (f.addr, f.len)); + out.dedup_by_key(|f| f.addr); + out +} + +/// Record evidence for an address, keeping the strongest name. +fn merge_evidence( + map: &mut BTreeMap)>, + addr: Addr, + p: &Provenance, + name: Option, +) { + match map.get_mut(&addr) { + Some((existing, existing_name)) => { + existing.merge(p); + if existing_name.is_none() { + *existing_name = name; + } + } + None => { + map.insert(addr, (p.clone(), name)); + } + } +} + +/// Look for function prologues in executable bytes no function covers. +/// +/// Heuristic by construction, so every hit is tagged as such. Runs only after +/// the evidence-led rounds finish, so it cannot pre-empt a real answer. +/// +/// Covered ranges are merged into a sorted disjoint list first and the scan +/// walks the gaps between them. Testing each address against every block +/// instead is quadratic, and on a libc-sized binary that is the difference +/// between eight seconds and a tenth of one. +fn scan_gaps( + object: &Object, + known: &BTreeMap, + caps: &Caps, + data: &DataMap, +) -> Vec { + // Two architectures whose instructions are aligned and whose prologues are + // a short list of encodings. x86 is not one of them: a one-byte alignment + // makes every offset a candidate and every candidate plausible. + let step = match object.arch { + Arch::AArch64 => 4, + Arch::Thumb => 2, + _ => return Vec::new(), + }; + let mem = &object.memory; + let covered = merge_ranges( + known + .values() + .flat_map(|f| f.cfg.blocks.values().map(|b| b.range)), + ); + + let mut out = Vec::new(); + for seg in mem.segments() { + if !seg.perms.exec { + continue; + } + for gap in gaps_in(seg.range, &covered) { + let mut at = gap.start(); + while at < gap.end() { + // A literal pool, a jump table or a declared object. Four + // bytes of any of them match a prologue as readily as any + // other four do, and a function invented there is a function + // that does not exist. + if let Some(end) = data.end_of_run(at) { + let past = Addr(end.get().next_multiple_of(step)); + at = if past > at { + past + } else { + at.wrapping_offset(step as i64) + }; + continue; + } + if is_prologue(mem, at, &object.arch) { + out.push(at); + } + at = at.wrapping_offset(step as i64); + } + } + if out.len() as u64 > caps.function_blocks { + break; + } + } + out.sort_unstable(); + out.dedup(); + out +} + +/// Sort and coalesce ranges into a disjoint list. +pub(crate) fn merge_ranges(it: impl Iterator) -> Vec { + let mut v: Vec = it.filter(|r| !r.is_empty()).collect(); + v.sort_unstable_by_key(|r| (r.start(), r.end())); + let mut out: Vec = Vec::with_capacity(v.len()); + for r in v { + match out.last_mut() { + Some(last) if r.start() <= last.end() => { + if r.end() > last.end() { + *last = AddrRange::new(last.start(), r.end()).unwrap_or(*last); + } + } + _ => out.push(r), + } + } + out +} + +/// The parts of `span` that `covered` does not cover. `covered` must be sorted +/// and disjoint, as [`merge_ranges`] returns. +fn gaps_in(span: AddrRange, covered: &[AddrRange]) -> Vec { + let mut out = Vec::new(); + let mut at = span.start(); + let first = covered.partition_point(|r| r.end() <= span.start()); + for r in &covered[first..] { + if r.start() >= span.end() { + break; + } + if r.start() > at { + if let Some(g) = AddrRange::new(at, r.start().min(span.end())) { + if !g.is_empty() { + out.push(g); + } + } + } + at = at.max(r.end()); + } + if at < span.end() { + if let Some(g) = AddrRange::new(at, span.end()) { + out.push(g); + } + } + out +} + +/// AArch64 prologue shapes a compiler emits. +/// +/// `stp x29, x30, [sp, #-N]!` is the frame setup, `paciasp`/`bti c` open a +/// pointer-authenticated or branch-target-hardened function, and `sub sp, sp, +/// #N` opens a leaf that needs stack. +fn is_prologue(mem: &e5r_core::MemoryMap, at: Addr, arch: &Arch) -> bool { + if *arch == Arch::Thumb { + return is_thumb_prologue(mem, at); + } + let Some(w) = mem.slice(at, 4) else { + return false; + }; + let word = u32::from_le_bytes([w[0], w[1], w[2], w[3]]); + // stp x29, x30, [sp, #imm]! — pre-index store pair of the frame registers. + let stp_frame = word & 0xffc0_7fff == 0xa980_7bfd; + // paciasp / pacibsp. + let pac = word == 0xd503_233f || word == 0xd503_237f; + // bti c / bti jc. + let bti = word == 0xd503_245f || word == 0xd503_247f; + // sub sp, sp, #imm. + let sub_sp = word & 0xffc0_03ff == 0xd100_03ff; + stp_frame || pac || bti || sub_sp +} + +/// The start of a Thumb function. +/// +/// Every non-leaf function begins by saving the link register, which is the +/// `push` with bit 8 of its register list set, or its wide form. A leaf that +/// only needs stack space begins by taking it. Those three cover what a +/// compiler emits; a function that begins with anything else is found by a +/// call to it rather than by this. +fn is_thumb_prologue(mem: &e5r_core::MemoryMap, at: Addr) -> bool { + let Some(w) = mem.slice(at, 2) else { + return false; + }; + let half = u16::from_le_bytes([w[0], w[1]]); + // push {..., lr} + if half & 0xff00 == 0xb500 { + return true; + } + // push.w {..., lr}: the wide encoding, whose second halfword carries the + // register list with the link register in bit 14. + let Some(w) = mem.slice(at, 4) else { + return false; + }; + let (hi, lo) = ( + u16::from_le_bytes([w[0], w[1]]), + u16::from_le_bytes([w[2], w[3]]), + ); + hi == 0xe92d && lo & 0x4000 != 0 +} + +/// Summary counts, for reporting and for the JSON surface. +#[derive(Debug, Clone, Copy, Default)] +pub struct Stats { + /// Functions found. + pub functions: usize, + /// Of those, how many finished with nothing unresolved. + pub complete: usize, + /// Basic blocks across all functions. + pub blocks: usize, + /// Instructions decoded. + pub insns: u64, + /// References found. + pub xrefs: usize, + /// Strings found. + pub strings: usize, + /// Functions that hit a resource cap. + pub capped: usize, + /// Functions found never to return. + pub noreturn: usize, + /// Jump tables resolved. + pub tables: usize, + /// Targets those tables contributed. + pub table_targets: usize, + /// Functions still holding an unresolved indirect branch. + pub indirect: usize, + /// Regions proved to hold data. + pub data_regions: usize, + /// Bytes those regions cover, counted once where proofs overlap. + pub data_bytes: u64, +} + +impl Program { + /// Count everything worth reporting. + pub fn stats(&self) -> Stats { + Stats { + functions: self.functions.len(), + complete: self.complete_count(), + blocks: self.functions.values().map(|f| f.cfg.blocks.len()).sum(), + insns: self.functions.values().map(|f| f.cfg.insns() as u64).sum(), + xrefs: self.xrefs.len(), + strings: self.strings.len(), + capped: self + .functions + .values() + .filter(|f| matches!(f.cfg.halt, Halt::InstructionCap | Halt::BlockCap)) + .count(), + noreturn: self.noreturn.len(), + tables: self.functions.values().map(|f| f.cfg.tables.len()).sum(), + table_targets: self + .functions + .values() + .flat_map(|f| f.cfg.tables.iter()) + .map(|t| t.targets.len()) + .sum(), + indirect: self + .functions + .values() + .filter(|f| f.cfg.has_indirect) + .count(), + data_regions: self.data().len(), + data_bytes: self.data().bytes(), + } + } + + /// Functions grouped by how strong their evidence is. + pub fn by_strength(&self) -> BTreeMap { + let mut out = BTreeMap::new(); + for f in self.functions.values() { + *out.entry(f.provenance.strength()).or_insert(0) += 1; + } + out + } +} diff --git a/crates/e5r-analysis/src/progress.rs b/crates/e5r-analysis/src/progress.rs new file mode 100644 index 0000000..b98ecc1 --- /dev/null +++ b/crates/e5r-analysis/src/progress.rs @@ -0,0 +1,174 @@ +//! Telling a caller what analysis is doing while it does it. +//! +//! `analyze()` is the longest thing the CLI runs and until now it was opaque: +//! `--progress` could count the loops the CLI itself drove and nothing inside. +//! A caller hands in one closure and gets told which stage is running and how +//! far through it is. +//! +//! Three properties, because a progress meter that changes the thing it +//! measures is worse than none: +//! +//! * **Nothing costs nothing.** The sink is a nullable borrowed function +//! pointer. With no callback attached, every report site is one null check +//! at a batch boundary, and there is no batch boundary in an inner loop. +//! * **No allocation per report.** [`Update`] is `Copy` and its only text is a +//! `&'static str`. Whether the callback allocates is the callback's +//! business. +//! * **No locking on a hot path.** Reports are made from the sequential merge +//! between parallel batches, never from inside one, so the callback is +//! called from one thread at a time and nothing in analysis contends for it. +//! That is also why the granularity is a batch of functions rather than a +//! function: a report per function would put a call from every thread at +//! once on the hottest loop in the crate. +//! +//! What a stage cannot say, it does not say. Discovery's rounds queue what +//! they find, so the total for a round is known and the total for the whole +//! stage is not; [`Update::total`] is `None` there rather than a guess that +//! moves backwards. +//! +//! Stages are not strictly one after another either. [`Stage::Data`] runs once +//! in the middle of discovery, because the gap scan needs to know what is data +//! before it sweeps, and again afterwards for the published map. A caller that +//! draws one bar per stage should expect to come back to a stage it has +//! already seen rather than assume the order is a sequence. + +/// Which stage of analysis is running. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Stage { + /// Walking functions and following what they call, in rounds. + Discovery, + /// Re-walking the callers of functions that turned out never to return. + NoReturn, + /// Proving which regions hold data. + Data, + /// Collecting cross references. + Xrefs, + /// Scanning for strings. + Strings, +} + +impl Stage { + /// A name to print. Static, so reporting one allocates nothing. + pub fn label(self) -> &'static str { + match self { + Stage::Discovery => "discovery", + Stage::NoReturn => "no-return re-walk", + Stage::Data => "data regions", + Stage::Xrefs => "cross references", + Stage::Strings => "strings", + } + } +} + +/// One report from inside analysis. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Update { + /// The stage this came from. + pub stage: Stage, + /// Which pass, for a stage that runs more than one. Discovery counts its + /// rounds here; every other stage reports 1. + pub round: u32, + /// Units finished in this pass. + pub done: u64, + /// Units this pass expects, when the stage knows. `None` means the total + /// is genuinely not known yet, which is the honest answer for discovery + /// across rounds. + pub total: Option, +} + +impl Update { + /// How far through this pass is, from 0.0 to 1.0, when the total is known. + pub fn fraction(&self) -> Option { + match self.total { + Some(0) => Some(1.0), + Some(n) => Some((self.done as f64 / n as f64).clamp(0.0, 1.0)), + None => None, + } + } +} + +/// Where progress reports go, or nowhere. +/// +/// Borrowed rather than owned and `Copy`, so it can be handed down into the +/// stage functions and captured by a closure that runs on the thread pool +/// without a reference count or a clone. +#[derive(Clone, Copy, Default)] +pub struct Sink<'a> { + to: Option<&'a (dyn Fn(Update) + Send + Sync)>, +} + +impl std::fmt::Debug for Sink<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Sink") + .field("attached", &self.to.is_some()) + .finish() + } +} + +impl<'a> Sink<'a> { + /// A sink that reports nowhere. + pub fn none() -> Sink<'a> { + Sink { to: None } + } + + /// A sink that calls `f`. + pub fn new(f: &'a (dyn Fn(Update) + Send + Sync)) -> Sink<'a> { + Sink { to: Some(f) } + } + + /// True when someone is listening, so a caller can skip work that only + /// exists to be reported. + pub fn is_attached(&self) -> bool { + self.to.is_some() + } + + /// Report one step. The whole cost with nothing attached is this branch. + pub fn report(&self, stage: Stage, round: u32, done: u64, total: Option) { + if let Some(f) = self.to { + f(Update { + stage, + round, + done, + total, + }); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + + #[test] + fn a_sink_with_nothing_attached_reports_nothing() { + let s = Sink::none(); + assert!(!s.is_attached()); + s.report(Stage::Discovery, 1, 0, None); + } + + #[test] + fn every_report_reaches_the_callback_once() { + let seen: Mutex> = Mutex::new(Vec::new()); + let f = |u: Update| seen.lock().unwrap().push(u); + let s = Sink::new(&f); + s.report(Stage::Xrefs, 1, 3, Some(10)); + s.report(Stage::Strings, 1, 2, None); + let got = seen.into_inner().unwrap(); + assert_eq!(got.len(), 2); + assert_eq!(got[0].fraction(), Some(0.3)); + assert_eq!(got[1].fraction(), None); + assert_eq!(got[0].stage.label(), "cross references"); + } + + #[test] + fn a_pass_with_no_work_is_finished_rather_than_undefined() { + let u = Update { + stage: Stage::NoReturn, + round: 1, + done: 0, + total: Some(0), + }; + assert_eq!(u.fraction(), Some(1.0)); + } +} diff --git a/crates/e5r-analysis/src/session.rs b/crates/e5r-analysis/src/session.rs new file mode 100644 index 0000000..c6021c7 --- /dev/null +++ b/crates/e5r-analysis/src/session.rs @@ -0,0 +1,370 @@ +//! Analysis on demand: a session that computes a part the first time it is +//! asked for, and remembers the answer. +//! +//! The eager [`analyze`](crate::analyze) shape made every caller decide, in +//! advance, what the command it was about to run would need. The CLI did that +//! by switching pieces off per command, which puts knowledge of the engine's +//! internals in the wrong place: a new command has to be told that it wants +//! strings but not cross references. A session turns it round. It owns the +//! object, and each of `functions`, `xrefs` and `strings` is computed on first +//! use and never twice, so a caller asks for what it wants and pays for that. +//! +//! Opening a session costs nothing: the loader has already parsed the header +//! and the symbol table, and nothing else runs until something asks. +//! +//! Dependencies between parts are the session's business rather than the +//! caller's. Cross references need functions, so asking for cross references +//! computes functions first. The no-return analysis is part of function +//! discovery rather than a stage after it, because it re-walks the callers it +//! affects and a caller asking for functions wants the corrected ones. +//! +//! With a [`Cache`] attached, each part is also looked up on disk before it is +//! computed and written back after. See [`crate::cache`] for what the key +//! covers and why a damaged file is a miss. + +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::{Mutex, OnceLock}; + +use e5r_core::Addr; +use e5r_format::Object; + +use crate::cache::{Cache, ContentHash, Key, Lookup, Part}; +use crate::codec; +use crate::data::{self, DataMap}; +use crate::program::{self, Function, Options, Program}; +use crate::progress::{Sink, Update}; +use crate::strings::Found; +use crate::xref::XrefIndex; + +/// Function discovery's whole answer, which the no-return pass produces in one +/// piece: the functions, which of them never return, and how many rounds it +/// took. +#[derive(Debug, Default)] +struct Core { + functions: BTreeMap, + noreturn: BTreeSet, + rounds: usize, +} + +/// A binary open for analysis, with each part computed on first use. +pub struct Session { + object: Object, + opts: Options, + /// Present only when the options name a thread count. Owning the pool here + /// rather than building one per stage is what lets a lazy stage run on the + /// same threads an eager analysis would have used. + pool: Option, + cache: Option, + content: Option, + /// Damaged cache files and unwritable cache directories. A library does + /// not print; the caller decides whether a person sees these. + warnings: Mutex>, + /// Where stage progress goes. `None` is the default and costs one branch + /// per batch; see [`crate::progress`]. + progress: Option>, + core: OnceLock, + xrefs: OnceLock, + strings: OnceLock>, + /// Not cached, unlike the three above. It is derived from the functions + /// and the container alone, so a cache hit on the functions would have to + /// carry it or contradict it, and recomputing it costs one pass. + data: OnceLock, +} + +impl std::fmt::Debug for Session { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Session") + .field("format", &self.object.format) + .field("arch", &self.object.arch) + .field("functions_computed", &self.core.get().is_some()) + .field("xrefs_computed", &self.xrefs.get().is_some()) + .field("strings_computed", &self.strings.get().is_some()) + .field("data_computed", &self.data.get().is_some()) + .finish() + } +} + +impl Session { + /// Open a loaded object for analysis. Computes nothing. + pub fn new(object: Object, opts: Options) -> Session { + // A pool is built once here, not once per stage: rayon's default pool + // is a global, and a session that asked for four threads must get four + // threads for every part it later computes. + let pool = match opts.threads { + Some(n) if n > 0 => rayon::ThreadPoolBuilder::new().num_threads(n).build().ok(), + _ => None, + }; + Session { + object, + opts, + pool, + cache: None, + content: None, + warnings: Mutex::new(Vec::new()), + progress: None, + core: OnceLock::new(), + xrefs: OnceLock::new(), + strings: OnceLock::new(), + data: OnceLock::new(), + } + } + + /// Read and write analysis parts through an on-disk cache. + /// + /// `content` identifies the bytes this object was loaded from. It is the + /// caller's job to fold in anything else that changed the load, because + /// the object cannot say what options produced it: see + /// [`ContentHash::with`]. + pub fn with_cache(mut self, cache: Cache, content: ContentHash) -> Session { + self.cache = Some(cache); + self.content = Some(content); + self + } + + /// Be told which stage is running and how far through it is. + /// + /// The callback is entered from the sequential merge between parallel + /// batches, so it is never called from two threads at once and never from + /// inside an inner loop. It must still be `Send + Sync`, because which + /// thread makes the call depends on the pool. A session with no callback + /// pays one branch per batch; see [`crate::progress`] for the shape of an + /// update and what a stage can and cannot say about its total. + pub fn with_progress(mut self, f: impl Fn(Update) + Send + Sync + 'static) -> Session { + self.progress = Some(Box::new(f)); + self + } + + /// The sink the stages are handed. + fn sink(&self) -> Sink<'_> { + match &self.progress { + Some(f) => Sink::new(f.as_ref()), + None => Sink::none(), + } + } + + /// What the loader produced. + pub fn object(&self) -> &Object { + &self.object + } + + /// The options this session analyzes under. + pub fn options(&self) -> &Options { + &self.opts + } + + /// Anything worth telling a person about the cache. Empty in the ordinary + /// case. + pub fn warnings(&self) -> Vec { + self.warnings + .lock() + .map(|w| w.clone()) + .unwrap_or_else(|p| p.into_inner().clone()) + } + + /// True when that part has already been computed, so a caller can report + /// what a run actually paid for. + pub fn is_computed(&self, part: Part) -> bool { + match part { + Part::Functions => self.core.get().is_some(), + Part::Xrefs => self.xrefs.get().is_some(), + Part::Strings => self.strings.get().is_some(), + } + } + + /// Recovered functions by entry address, computing them if needed. + pub fn functions(&self) -> &BTreeMap { + &self.core().functions + } + + /// Functions found never to return. Empty when the option is off. + pub fn noreturn(&self) -> &BTreeSet { + &self.core().noreturn + } + + /// Rounds of discovery it took. + pub fn rounds(&self) -> usize { + self.core().rounds + } + + /// The cross reference index, computing it if needed. Empty when the + /// option is off, which is the same answer the eager path gives. + pub fn xrefs(&self) -> &XrefIndex { + self.xrefs.get_or_init(|| { + if !self.opts.xrefs { + return XrefIndex::default(); + } + if let Some((_, bytes)) = self.cached(Part::Xrefs) + && let Some(index) = codec::decode_xrefs(&bytes) + { + return index; + } + // Reading the functions first keeps the borrow of the pool out of + // the closure that runs on it. + let functions = &self.core().functions; + let object = &self.object; + let sink = self.sink(); + let index = self.install(move || program::build_xrefs(object, functions, sink)); + self.store(Part::Xrefs, || codec::encode_xrefs(&index)); + index + }) + } + + /// Strings, computing them if needed. Empty when the option is off. + pub fn strings(&self) -> &[Found] { + self.strings.get_or_init(|| { + if !self.opts.strings { + return Vec::new(); + } + if let Some((_, bytes)) = self.cached(Part::Strings) + && let Some(found) = codec::decode_strings(&bytes) + { + return found; + } + let object = &self.object; + let opts = &self.opts.string_opts; + let sink = self.sink(); + let found = self.install(move || program::scan_strings(object, opts, sink)); + self.store(Part::Strings, || codec::encode_strings(&found)); + found + }) + } + + /// Regions that are provably data, computing them if needed. Empty when + /// the option is off. + /// + /// Needs the functions, because a recovered jump table and a literal pool + /// are both read off them, so asking for this computes those first. + pub fn data(&self) -> &DataMap { + self.data.get_or_init(|| { + if !self.opts.data { + return DataMap::default(); + } + let functions = &self.core().functions; + let object = &self.object; + let sink = self.sink(); + self.install(move || { + data::build(object, functions, data::Sources { pointers: true }, sink) + }) + }) + } + + /// Force everything the options ask for and hand back the eager shape. + /// + /// This is what [`analyze`](crate::analyze) is: every caller in the + /// workspace wants a `Program`, and a session that has computed all its + /// parts is one. + pub fn into_program(self) -> Program { + // Order matters only for the cache: functions first means a run + // interrupted after it writes the functions entry still leaves a + // usable one. + let _ = self.functions(); + let _ = self.xrefs(); + let _ = self.strings(); + // Not the data map: `Program` computes it on the first ask, so a + // caller that never reads it does not pay a second decode of every + // instruction. Off in the options still means empty, not deferred. + if !self.opts.data { + let _ = self.data(); + } + let core = self.core.into_inner().unwrap_or_default(); + Program { + object: self.object, + functions: core.functions, + xrefs: self.xrefs.into_inner().unwrap_or_default(), + strings: self.strings.into_inner().unwrap_or_default(), + rounds: core.rounds, + noreturn: core.noreturn, + data: self + .data + .into_inner() + .map(OnceLock::from) + .unwrap_or_default(), + } + } + + /// Discovery plus the no-return pass, memoized. + fn core(&self) -> &Core { + self.core.get_or_init(|| { + if let Some((_, bytes)) = self.cached(Part::Functions) + && let Some((functions, noreturn, rounds)) = codec::decode_functions(&bytes) + { + return Core { + functions, + noreturn, + rounds, + }; + } + let object = &self.object; + let opts = &self.opts; + let sink = self.sink(); + let core = self.install(move || { + // One block table for the whole program: discovery and the + // no-return re-walk share it, so a block the re-walk keeps is + // the one every other function already names. + let mut interner = crate::cfg::BlockInterner::default(); + let (mut functions, rounds) = program::discover(object, opts, &mut interner, sink); + let noreturn = + program::refine_noreturn(object, &mut functions, opts, &mut interner, sink); + program::publish_blocks(&mut functions, &mut interner); + Core { + functions, + noreturn, + rounds, + } + }); + self.store(Part::Functions, || { + codec::encode_functions(&core.functions, &core.noreturn, core.rounds) + }); + core + }) + } + + /// Run a stage on this session's threads. + fn install(&self, f: impl FnOnce() -> T + Send) -> T { + match &self.pool { + Some(pool) => pool.install(f), + None => f(), + } + } + + /// The key for one part, or `None` when no cache is attached. + fn key(&self, part: Part) -> Option { + let content = self.content.as_ref()?; + self.cache.as_ref()?; + Some(Key::derive(content, part, &self.opts.cache_bytes(part))) + } + + /// Look one part up. A damaged file is recorded and then treated as a + /// miss, and the file is removed so the next run does not trip over it. + fn cached(&self, part: Part) -> Option<(Key, Vec)> { + let cache = self.cache.as_ref()?; + let key = self.key(part)?; + match cache.get(part, &key) { + Lookup::Hit(bytes) => Some((key, bytes)), + Lookup::Miss => None, + Lookup::Corrupt(why) => { + self.warn(why); + cache.remove(part, &key); + None + } + } + } + + /// Write one part back. The payload is only encoded when there is a cache + /// to write it to. + fn store(&self, part: Part, encode: impl FnOnce() -> Vec) { + let (Some(cache), Some(key)) = (self.cache.as_ref(), self.key(part)) else { + return; + }; + if let Err(e) = cache.put(part, &key, &encode()) { + self.warn(format!("could not write the analysis cache: {e}")); + } + } + + fn warn(&self, message: String) { + match self.warnings.lock() { + Ok(mut w) => w.push(message), + Err(p) => p.into_inner().push(message), + } + } +} diff --git a/crates/e5r-analysis/src/strings.rs b/crates/e5r-analysis/src/strings.rs new file mode 100644 index 0000000..69d17f6 --- /dev/null +++ b/crates/e5r-analysis/src/strings.rs @@ -0,0 +1,239 @@ +//! String extraction. +//! +//! Runs of printable bytes in mapped memory, in the encodings that actually +//! appear: NUL-terminated ASCII and UTF-8, UTF-16LE, and the length-prefixed +//! slices Go and Rust use, which have no terminator and are invisible to a +//! scanner that only looks for NUL. + +use e5r_core::{Addr, AddrRange, MemoryMap}; +use serde::Serialize; + +/// How a string was encoded. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Encoding { + /// Single-byte, NUL-terminated. + Ascii, + /// Multi-byte UTF-8, NUL-terminated. + Utf8, + /// Two-byte little-endian, NUL-terminated. + Utf16, +} + +/// One string found in memory. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Found { + /// Where it starts. + pub addr: Addr, + /// How many bytes it occupies, terminator excluded. + pub len: u64, + /// How it was encoded. + pub encoding: Encoding, + /// The text. + pub text: String, +} + +/// Options for a scan. +#[derive(Debug, Clone, Copy)] +pub struct Options { + /// Shortest run worth reporting. Four is the conventional floor: shorter + /// runs are mostly coincidence in binary data. + pub min_len: usize, + /// Include UTF-16LE, which doubles the scan cost and is mostly useful on + /// Windows binaries. + pub utf16: bool, + /// Also scan executable memory. Off by default: instruction bytes produce + /// long runs of plausible ASCII and drown the real strings. + pub in_code: bool, +} + +impl Default for Options { + fn default() -> Self { + Options { + min_len: 4, + utf16: true, + in_code: false, + } + } +} + +/// True for bytes that read as text rather than as data. +fn printable(b: u8) -> bool { + matches!(b, 0x20..=0x7e | b'\t' | b'\n' | b'\r') +} + +/// Find every string in the given address ranges. +/// +/// Ranges rather than segments because `.rodata` lives inside an executable +/// segment in the common two-segment ELF layout, and scanning by segment +/// either misses every string or drowns them in instruction bytes. +pub fn scan_ranges(mem: &MemoryMap, ranges: &[AddrRange], opts: &Options) -> Vec { + let mut out = Vec::new(); + for r in ranges { + let mut at = r.start(); + while at < r.end() { + let Some(seg) = mem.segment_at(at) else { + at = at.wrapping_offset(1); + continue; + }; + let Some(bytes) = seg.slice_to_end(at) else { + break; + }; + let take = bytes + .len() + .min(r.end().get().saturating_sub(at.get()) as usize); + scan_bytes(&bytes[..take], at, opts, &mut out); + at = at.wrapping_offset(take.max(1) as i64); + } + } + out.sort_by_key(|f| (f.addr, f.len)); + out.dedup_by_key(|f| f.addr); + out +} + +/// Every non-executable segment, for an image with no section table. +pub fn scan(mem: &MemoryMap, opts: &Options) -> Vec { + let ranges: Vec = mem + .segments() + .iter() + .filter(|s| opts.in_code || !s.perms.exec) + .map(|s| s.range) + .collect(); + scan_ranges(mem, &ranges, opts) +} + +/// Scan one buffer, reporting strings at `base + offset`. +pub fn scan_bytes(bytes: &[u8], base: Addr, opts: &Options, out: &mut Vec) { + let mut i = 0usize; + while i < bytes.len() { + if !printable(bytes[i]) { + i += 1; + continue; + } + let start = i; + while i < bytes.len() && printable(bytes[i]) { + i += 1; + } + let run = &bytes[start..i]; + if run.len() < opts.min_len { + continue; + } + // A NUL after the run is what separates a string from a stretch of + // data that happens to be printable. + let terminated = i < bytes.len() && bytes[i] == 0; + if !terminated { + continue; + } + if let Some(addr) = base.checked_add(start as u64) { + out.push(Found { + addr, + len: run.len() as u64, + encoding: if run.is_ascii() { + Encoding::Ascii + } else { + Encoding::Utf8 + }, + text: String::from_utf8_lossy(run).into_owned(), + }); + } + } + + if opts.utf16 { + scan_utf16(bytes, base, opts, out); + } +} + +/// UTF-16LE runs: a printable byte followed by a zero, repeated. +fn scan_utf16(bytes: &[u8], base: Addr, opts: &Options, out: &mut Vec) { + let mut i = 0usize; + while i + 1 < bytes.len() { + if !(printable(bytes[i]) && bytes[i + 1] == 0) { + i += 1; + continue; + } + let start = i; + let mut text = String::new(); + while i + 1 < bytes.len() && printable(bytes[i]) && bytes[i + 1] == 0 { + text.push(bytes[i] as char); + i += 2; + } + if text.len() < opts.min_len { + continue; + } + if let Some(addr) = base.checked_add(start as u64) { + out.push(Found { + addr, + len: (i - start) as u64, + encoding: Encoding::Utf16, + text, + }); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finds_terminated_runs_only() { + let mut out = Vec::new(); + scan_bytes( + b"hello\0\x01\x02unterminated", + Addr(0x1000), + &Options::default(), + &mut out, + ); + assert_eq!(out.len(), 1); + assert_eq!(out[0].text, "hello"); + assert_eq!(out[0].addr, Addr(0x1000)); + } + + #[test] + fn short_runs_are_noise() { + let mut out = Vec::new(); + scan_bytes(b"ab\0abcd\0", Addr(0), &Options::default(), &mut out); + assert_eq!( + out.iter().map(|f| f.text.as_str()).collect::>(), + ["abcd"] + ); + } + + #[test] + fn utf16_is_found_when_asked_for() { + let mut data = Vec::new(); + for c in b"wide" { + data.push(*c); + data.push(0); + } + data.extend_from_slice(&[0, 0]); + let mut out = Vec::new(); + scan_bytes(&data, Addr(0x2000), &Options::default(), &mut out); + assert!( + out.iter() + .any(|f| f.text == "wide" && f.encoding == Encoding::Utf16) + ); + + let mut off = Vec::new(); + scan_bytes( + &data, + Addr(0x2000), + &Options { + utf16: false, + ..Options::default() + }, + &mut off, + ); + assert!(!off.iter().any(|f| f.encoding == Encoding::Utf16)); + } + + #[test] + fn scanning_is_deterministic() { + let data: Vec = (0..4096u32).map(|i| (i % 251) as u8).collect(); + let mut a = Vec::new(); + let mut b = Vec::new(); + scan_bytes(&data, Addr(0), &Options::default(), &mut a); + scan_bytes(&data, Addr(0), &Options::default(), &mut b); + assert_eq!(a, b); + } +} diff --git a/crates/e5r-analysis/src/xref.rs b/crates/e5r-analysis/src/xref.rs new file mode 100644 index 0000000..5156c94 --- /dev/null +++ b/crates/e5r-analysis/src/xref.rs @@ -0,0 +1,417 @@ +//! Cross references. +//! +//! Direct branches and calls come free from the decoder. Data references do +//! not: on AArch64 an address is built by an `adrp` that supplies the page and +//! a later `add` or `ldr` that supplies the offset, and the two can be several +//! instructions apart. Resolving that pair is what turns a listing full of +//! bare page numbers into one that names the string being loaded. +//! +//! The tracker is deliberately small: constants per register, forgotten at any +//! instruction that writes a register in a way it does not model, and reset at +//! every block boundary. It never guesses across a join. + +use std::collections::BTreeMap; + +use e5r_arch::{Flow, Insn, Operand, Reg, RegClass}; +use e5r_core::{Addr, MemoryMap}; +use serde::Serialize; + +/// What kind of reference one address makes to another. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum XrefKind { + /// A direct call. + Call, + /// A direct branch, conditional or not. + Branch, + /// An address formed in registers, usually a pointer to data. + Data, + /// A load or store through a computed address. + Read, + /// A store through a computed address. + Write, +} + +/// One reference. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct Xref { + /// The instruction making the reference. + pub from: Addr, + /// What it refers to. + pub to: Addr, + /// What kind of reference it is. + pub kind: XrefKind, +} + +/// Every reference in a program, searchable in both directions. +#[derive(Debug, Clone, Default)] +pub struct XrefIndex { + /// Sorted by `(from, to, kind)`. + by_from: Vec, + /// Sorted by `(to, from, kind)`. + by_to: Vec, +} + +impl XrefIndex { + /// Build from an unsorted list. + pub fn build(mut refs: Vec) -> XrefIndex { + refs.sort_unstable(); + refs.dedup(); + // The capacity that held the duplicates is returned before the second + // array is allocated, so the peak is two arrays rather than three. + refs.shrink_to_fit(); + let mut by_to = refs.clone(); + by_to.sort_unstable_by_key(|x| (x.to, x.from, x.kind)); + XrefIndex { + by_from: refs, + by_to, + } + } + + /// Every reference. + pub fn all(&self) -> &[Xref] { + &self.by_from + } + + /// How many references there are. + pub fn len(&self) -> usize { + self.by_from.len() + } + + /// True when there are none. + pub fn is_empty(&self) -> bool { + self.by_from.is_empty() + } + + /// References made by the instruction at `from`. + pub fn from(&self, from: Addr) -> &[Xref] { + let lo = self.by_from.partition_point(|x| x.from < from); + let hi = self.by_from.partition_point(|x| x.from <= from); + &self.by_from[lo..hi] + } + + /// References pointing at `to`. The question an analyst asks most. + pub fn to(&self, to: Addr) -> &[Xref] { + let lo = self.by_to.partition_point(|x| x.to < to); + let hi = self.by_to.partition_point(|x| x.to <= to); + &self.by_to[lo..hi] + } +} + +/// Constants known to be in registers at one point in a block. +#[derive(Default)] +struct Regs(BTreeMap); + +impl Regs { + fn get(&self, r: Reg) -> Option { + (r.class == RegClass::Gpr).then(|| self.0.get(&r.num).copied())? + } + + fn set(&mut self, r: Reg, v: u64) { + if r.class == RegClass::Gpr { + self.0.insert(r.num, v); + } + } + + fn forget(&mut self, r: Reg) { + if r.class == RegClass::Gpr { + self.0.remove(&r.num); + } + } +} + +/// Collect references from one function's instructions. +/// +/// `insns` must be instructions in address order; the tracker resets whenever +/// the previous instruction did not fall through, which is a cheap stand-in +/// for block boundaries and never carries a value across a join. +pub fn collect(insns: &[Insn], mem: &MemoryMap, out: &mut Vec) { + let mut c = Collector::new(mem, out); + for i in insns { + c.push(*i); + } + c.finish(); +} + +/// The same walk, fed one instruction at a time. +/// +/// Whether an instruction falls through to the next is a fact about a pair, so +/// the collector holds one instruction back and decides about it when its +/// successor arrives. That is the whole reason [`collect`] wanted a slice, and +/// removing the need for one is what lets the caller decode a function without +/// keeping it: an `Insn` is 224 bytes, and a large function on every thread at +/// once is the memory this stage actually spends. +pub struct Collector<'a> { + mem: &'a MemoryMap, + out: &'a mut Vec, + regs: Regs, + /// Whether the instruction processed last fell through to this one. + fresh: bool, + /// The instruction whose successor is not known yet. + pending: Option, +} + +impl<'a> Collector<'a> { + /// Start collecting into `out`. + pub fn new(mem: &'a MemoryMap, out: &'a mut Vec) -> Collector<'a> { + Collector { + mem, + out, + regs: Regs::default(), + fresh: true, + pending: None, + } + } + + /// Offer the next instruction, in address order. + pub fn push(&mut self, i: Insn) { + if let Some(prev) = self.pending.take() { + // The next instruction continues this run only if the previous one + // falls through to exactly it. + let falls_through = prev.flow == Flow::Next && i.addr == prev.next(); + self.process(prev, falls_through); + } + self.pending = Some(i); + } + + /// Process the instruction still held back. Nothing follows it, so it + /// cannot fall through to anything. + pub fn finish(mut self) { + if let Some(prev) = self.pending.take() { + self.process(prev, false); + } + } + + fn process(&mut self, i: Insn, falls_through: bool) { + if !self.fresh { + self.regs = Regs::default(); + } + self.fresh = falls_through; + + match i.flow { + Flow::Call(t) => self.out.push(Xref { + from: i.addr, + to: t, + kind: XrefKind::Call, + }), + Flow::Branch(t) | Flow::CondBranch(t) => self.out.push(Xref { + from: i.addr, + to: t, + kind: XrefKind::Branch, + }), + _ => {} + } + + track(&i, &mut self.regs, self.mem, self.out); + } +} + +/// Update the register model for one instruction and emit any data reference +/// it completes. +fn track(i: &Insn, regs: &mut Regs, mem: &MemoryMap, out: &mut Vec) { + let ops = i.operands(); + + // x86 forms addresses with the program counter directly, so a pc-relative + // operand names its target outright, whatever the instruction is. This is + // the x86 equivalent of the adrp/add pair below. + for (n, op) in ops.iter().enumerate() { + let Operand::Mem(m) = op else { continue }; + if !m.is_pc_relative() { + continue; + } + let Some(t) = m.pc_target(i.end()) else { + continue; + }; + if !mem.is_mapped(t) { + continue; + } + // The first operand is the destination in Intel order, so a memory + // operand there is a write. + out.push(Xref { + from: i.addr, + to: t, + kind: if m.size == 0 { + XrefKind::Data + } else if n == 0 { + XrefKind::Write + } else { + XrefKind::Read + }, + }); + } + + match i.mnemonic { + // adrp and adr put a resolved address straight into a register. + "adrp" | "adr" => { + if let (Some(Operand::Reg(d)), Some(Operand::Addr(a))) = (ops.first(), ops.get(1)) { + regs.set(*d, a.get()); + if i.mnemonic == "adr" && mem.is_mapped(*a) { + out.push(Xref { + from: i.addr, + to: *a, + kind: XrefKind::Data, + }); + } + } + } + // add rd, rn, #imm completes the pair. + "add" => match (ops.first(), ops.get(1), ops.get(2)) { + (Some(Operand::Reg(d)), Some(Operand::Reg(n)), Some(Operand::Imm(v))) => { + match regs.get(*n) { + Some(base) => { + let val = base.wrapping_add(*v as u64); + regs.set(*d, val); + if mem.is_mapped(Addr(val)) { + out.push(Xref { + from: i.addr, + to: Addr(val), + kind: XrefKind::Data, + }); + } + } + None => regs.forget(*d), + } + } + (Some(Operand::Reg(d)), _, _) => regs.forget(*d), + _ => {} + }, + // mov rd, #imm seeds a constant; mov rd, rn copies one. + "mov" => match (ops.first(), ops.get(1)) { + (Some(Operand::Reg(d)), Some(Operand::Imm(v))) => regs.set(*d, *v as u64), + (Some(Operand::Reg(d)), Some(Operand::UImm(v))) => regs.set(*d, *v), + (Some(Operand::Reg(d)), Some(Operand::Reg(n))) => match regs.get(*n) { + Some(v) => regs.set(*d, v), + None => regs.forget(*d), + }, + _ => {} + }, + // A load or store through a tracked base is a reference to that slot. + "ldr" | "ldrb" | "ldrh" | "ldrsb" | "ldrsh" | "ldrsw" | "str" | "strb" | "strh" => { + let write = i.mnemonic.starts_with("str"); + let mut referenced = None; + for op in ops { + match op { + // A literal load names its target outright. + Operand::Addr(a) => referenced = Some(*a), + Operand::Mem(m) if m.index.is_none() => { + if let Some(base) = m.base.and_then(|b| regs.get(b)) { + let at = base.wrapping_add(m.disp as u64); + if mem.is_mapped(Addr(at)) { + referenced = Some(Addr(at)); + } + } + } + _ => {} + } + } + if let Some(to) = referenced { + out.push(Xref { + from: i.addr, + to, + kind: if write { + XrefKind::Write + } else { + XrefKind::Read + }, + }); + } + // The loaded value is unknown, so the destination is no longer + // a constant we can trust. + if !write { + if let Some(Operand::Reg(d)) = ops.first() { + regs.forget(*d); + } + } + } + _ => { + // Anything else invalidates whatever it writes. The first operand + // is the destination in every AArch64 form that has one. + if let Some(Operand::Reg(d)) = ops.first() { + regs.forget(*d); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use e5r_core::{AddrRange, Perms, Segment}; + + fn mem_with(at: u64, len: u64) -> MemoryMap { + let mut m = MemoryMap::new(); + m.add( + Segment::new( + AddrRange::sized(Addr(at), len).unwrap(), + Perms::R, + "data", + 0, + vec![0; len as usize], + ) + .unwrap(), + ); + m + } + + #[test] + fn adrp_plus_add_resolves_to_one_reference() { + // At 0x400000: adrp x0, 0x41c000 ; add x0, x0, #0x123 + let mem = mem_with(0x41c000, 0x1000); + let insns: Vec = [0x9000_00e0u32, 0x9104_8c00] + .iter() + .enumerate() + .filter_map(|(n, w)| e5r_arch::aarch64::decode_word(*w, Addr(0x400000 + n as u64 * 4))) + .collect(); + let mut out = Vec::new(); + collect(&insns, &mem, &mut out); + let data: Vec<_> = out.iter().filter(|x| x.kind == XrefKind::Data).collect(); + assert_eq!(data.len(), 1, "{out:?}"); + assert_eq!(data[0].to, Addr(0x41c123)); + } + + #[test] + fn a_branch_between_the_pair_stops_the_tracker() { + // adrp x0, ... ; ret ; add x0, x0, #0x123 + // The add is not reachable from the adrp, so no data reference. + let mem = mem_with(0x41c000, 0x1000); + let words = [0x9000_00e0u32, 0xd65f_03c0, 0x9104_8c00]; + let insns: Vec = words + .iter() + .enumerate() + .filter_map(|(n, w)| e5r_arch::aarch64::decode_word(*w, Addr(0x400000 + n as u64 * 4))) + .collect(); + let mut out = Vec::new(); + collect(&insns, &mem, &mut out); + assert!(!out.iter().any(|x| x.kind == XrefKind::Data), "{out:?}"); + } + + #[test] + fn unmapped_targets_are_not_reported() { + let mem = mem_with(0x900000, 0x10); + let insns: Vec = [0x9000_00e0u32, 0x9104_8c00] + .iter() + .enumerate() + .filter_map(|(n, w)| e5r_arch::aarch64::decode_word(*w, Addr(0x400000 + n as u64 * 4))) + .collect(); + let mut out = Vec::new(); + collect(&insns, &mem, &mut out); + assert!(!out.iter().any(|x| x.kind == XrefKind::Data)); + } + + #[test] + fn the_index_answers_in_both_directions() { + let x = |f: u64, t: u64, k| Xref { + from: Addr(f), + to: Addr(t), + kind: k, + }; + let ix = XrefIndex::build(vec![ + x(0x10, 0x100, XrefKind::Call), + x(0x20, 0x100, XrefKind::Call), + x(0x10, 0x200, XrefKind::Data), + ]); + assert_eq!(ix.to(Addr(0x100)).len(), 2); + assert_eq!(ix.from(Addr(0x10)).len(), 2); + assert_eq!(ix.to(Addr(0x300)).len(), 0); + } +} diff --git a/crates/e5r-analysis/tests/cache.rs b/crates/e5r-analysis/tests/cache.rs new file mode 100644 index 0000000..860ffac --- /dev/null +++ b/crates/e5r-analysis/tests/cache.rs @@ -0,0 +1,274 @@ +//! The on-disk cache: that a hit is the same answer, that a key which does not +//! match is a miss rather than a partial reuse, and that damage is a miss +//! rather than a panic or a lie. + +mod common; + +use std::path::Path; + +use common::{fingerprint, load, scratch}; +use e5r_analysis::{Cache, ContentHash, Options, Session, analyze}; + +/// A session over `name` that reads and writes `dir`. +fn cached(dir: &Path, name: &str, opts: &Options) -> e5r_analysis::Program { + let (data, obj) = load(name).expect("fixture"); + Session::new(obj, opts.clone()) + .with_cache(Cache::at(dir), ContentHash::of(&data)) + .into_program() +} + +/// Every cache file in a directory, sorted so the list is stable. +fn entries(dir: &Path) -> Vec { + let mut v: Vec<_> = std::fs::read_dir(dir) + .map(|rd| { + rd.flatten() + .map(|e| e.path()) + .filter(|p| p.extension().is_some_and(|e| e == "bin")) + .collect() + }) + .unwrap_or_default(); + v.sort(); + v +} + +#[test] +fn a_second_open_gets_the_same_answer_from_disk() { + if common::corpus().is_none() { + return; + } + let dir = scratch("roundtrip"); + let opts = Options::default(); + let first = cached(&dir, "hello.a64.O2", &opts); + assert_eq!( + entries(&dir).len(), + 3, + "functions, xrefs and strings each get an entry" + ); + let second = cached(&dir, "hello.a64.O2", &opts); + assert_eq!(fingerprint(&first), fingerprint(&second)); + + // And against a run that never saw the cache, so a hit is checked against + // the engine rather than against itself. + let (_, obj) = load("hello.a64.O2").expect("fixture"); + assert_eq!(fingerprint(&analyze(obj, &opts)), fingerprint(&second)); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn only_the_parts_that_were_asked_for_are_written() { + let Some((data, obj)) = load("hello.a64.O2") else { + return; + }; + let dir = scratch("parts"); + let s = + Session::new(obj, Options::default()).with_cache(Cache::at(&dir), ContentHash::of(&data)); + let _ = s.functions(); + assert_eq!( + entries(&dir).len(), + 1, + "a run that asked only for functions wrote more than the functions" + ); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn different_options_do_not_share_an_entry() { + if common::corpus().is_none() { + return; + } + let dir = scratch("keys"); + let with_gaps = Options::default(); + let without = Options { + scan_gaps: false, + ..Options::default() + }; + let a = cached(&dir, "hello.go.stripped", &with_gaps); + let b = cached(&dir, "hello.go.stripped", &without); + + // The two runs must agree with what they would have computed alone, which + // is the property "no partial reuse" protects. + let (_, obj) = load("hello.go.stripped").expect("fixture"); + assert_eq!(fingerprint(&analyze(obj, &with_gaps)), fingerprint(&a)); + let (_, obj) = load("hello.go.stripped").expect("fixture"); + assert_eq!(fingerprint(&analyze(obj, &without)), fingerprint(&b)); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn a_different_binary_does_not_share_an_entry() { + if common::corpus().is_none() { + return; + } + let dir = scratch("content"); + let opts = Options::default(); + let a = cached(&dir, "hello.a64.O2", &opts); + let b = cached(&dir, "hello.a64.O0", &opts); + assert_ne!(fingerprint(&a), fingerprint(&b)); + let (_, obj) = load("hello.a64.O0").expect("fixture"); + assert_eq!(fingerprint(&analyze(obj, &opts)), fingerprint(&b)); + let _ = std::fs::remove_dir_all(&dir); +} + +/// A cached answer has to be the answer, at any thread count: the cache is +/// exactly the kind of thing that can break gate G5 by handing one run a +/// result another run computed differently. +#[test] +fn a_hit_matches_a_fresh_run_at_every_thread_count() { + if common::corpus().is_none() { + return; + } + let dir = scratch("threads"); + let written = Options { + threads: Some(1), + ..Options::default() + }; + let expected = fingerprint(&cached(&dir, "hello.go.stripped", &written)); + for threads in [1, 4, 10] { + let read = Options { + threads: Some(threads), + ..Options::default() + }; + assert_eq!( + expected, + fingerprint(&cached(&dir, "hello.go.stripped", &read)), + "a cache written at one thread read differently at {threads}" + ); + } + let _ = std::fs::remove_dir_all(&dir); +} + +/// Truncation and single-byte corruption at hundreds of offsets in every cache +/// file, each one checked to produce the right answer anyway. +/// +/// This is the test the format exists for. A decoder that indexes a slice +/// panics here; one that trusts a length allocates until it is killed; one +/// without a checksum returns a plausible wrong answer, which is the worst of +/// the three and the only one a person would not notice. +#[test] +fn damaged_cache_files_are_a_miss_and_never_a_wrong_answer() { + if common::corpus().is_none() { + return; + } + let dir = scratch("damage"); + let opts = Options::default(); + let good = fingerprint(&cached(&dir, "hello.a64.O2", &opts)); + + let files = entries(&dir); + assert_eq!(files.len(), 3); + let originals: Vec<(std::path::PathBuf, Vec)> = files + .iter() + .map(|p| (p.clone(), std::fs::read(p).expect("read entry"))) + .collect(); + + let mut checked = 0usize; + for (path, original) in &originals { + // Every offset in the header and then a stride through the payload, so + // the sweep covers the fields that decide what the file is as well as + // the body it describes. + let offsets: Vec = (0..original.len().min(48)) + .chain((48..original.len()).step_by(7)) + .collect(); + for off in offsets { + // Truncated: the file stops in the middle of something. + std::fs::write(path, &original[..off]).expect("truncate"); + restore_others(&originals, path); + let got = fingerprint(&cached(&dir, "hello.a64.O2", &opts)); + assert_eq!(got, good, "truncating {path:?} at {off} changed the answer"); + + // Corrupted: one byte is not what was written. + let mut bad = original.clone(); + bad[off] ^= 0xff; + std::fs::write(path, &bad).expect("corrupt"); + restore_others(&originals, path); + let got = fingerprint(&cached(&dir, "hello.a64.O2", &opts)); + assert_eq!( + got, good, + "flipping byte {off} of {path:?} changed the answer" + ); + checked += 2; + } + std::fs::write(path, original).expect("restore"); + } + assert!(checked > 300, "only {checked} damaged files were tried"); + + // A file of pure noise, and an empty one, are the degenerate cases. + for (path, original) in &originals { + std::fs::write(path, []).expect("empty"); + assert_eq!(fingerprint(&cached(&dir, "hello.a64.O2", &opts)), good); + let noise: Vec = (0..original.len()).map(|i| (i * 31 + 7) as u8).collect(); + std::fs::write(path, &noise).expect("noise"); + assert_eq!(fingerprint(&cached(&dir, "hello.a64.O2", &opts)), good); + } + let _ = std::fs::remove_dir_all(&dir); +} + +/// Put back every entry except the one under test, because a run that found +/// damage rewrites the file it could not read. +fn restore_others(originals: &[(std::path::PathBuf, Vec)], skip: &Path) { + for (path, bytes) in originals { + if path != skip { + let _ = std::fs::write(path, bytes); + } + } +} + +#[test] +fn damage_is_reported_rather_than_swallowed() { + let Some((data, obj)) = load("hello.a64.O2") else { + return; + }; + let dir = scratch("warn"); + let s = + Session::new(obj, Options::default()).with_cache(Cache::at(&dir), ContentHash::of(&data)); + let _ = s.functions(); + let file = entries(&dir).into_iter().next().expect("an entry"); + let mut bytes = std::fs::read(&file).expect("read"); + let n = bytes.len(); + bytes[n - 1] ^= 0x01; + std::fs::write(&file, &bytes).expect("corrupt"); + + let (_, obj) = load("hello.a64.O2").expect("fixture"); + let s = + Session::new(obj, Options::default()).with_cache(Cache::at(&dir), ContentHash::of(&data)); + let _ = s.functions(); + let warnings = s.warnings(); + assert!( + warnings.iter().any(|w| w.contains("damaged cache file")), + "a damaged file was ignored silently: {warnings:?}" + ); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn a_cache_directory_that_cannot_be_written_is_not_an_error() { + let Some((data, obj)) = load("hello.a64.O2") else { + return; + }; + // A file where the directory should be: creating it fails, and analysis + // has to carry on regardless. A cache is an optimization, never a + // precondition. + let blocker = scratch("unwritable"); + std::fs::write(&blocker, b"not a directory").expect("write blocker"); + let s = Session::new(obj, Options::default()) + .with_cache(Cache::at(blocker.join("inside")), ContentHash::of(&data)); + assert!(!s.functions().is_empty()); + assert!( + s.warnings().iter().any(|w| w.contains("could not write")), + "a cache that could not be written said nothing" + ); + let _ = std::fs::remove_file(&blocker); +} + +#[test] +fn the_cache_directory_follows_xdg() { + // Not run in parallel with anything that reads the environment: these two + // variables are process-wide, which is why this is one test and not two. + let dir = Cache::discover().map(|c| c.dir().to_path_buf()); + if let Some(dir) = dir { + let expected = match std::env::var_os("XDG_CACHE_HOME") { + Some(v) if !v.is_empty() => std::path::PathBuf::from(v), + _ => std::path::PathBuf::from(std::env::var_os("HOME").expect("HOME")).join(".cache"), + }; + assert_eq!(dir, expected.join("e5r").join("analysis")); + } +} diff --git a/crates/e5r-analysis/tests/common/mod.rs b/crates/e5r-analysis/tests/common/mod.rs new file mode 100644 index 0000000..373036d --- /dev/null +++ b/crates/e5r-analysis/tests/common/mod.rs @@ -0,0 +1,100 @@ +//! Shared by the session and cache tests: loading a fixture, and flattening a +//! result so two runs can be compared. + +// Each test binary compiles its own copy of this module and uses part of it, +// so what the other one needs looks unused here. +#![allow(dead_code)] + +use std::path::{Path, PathBuf}; + +use e5r_format::{LoadOptions, Object}; + +/// The built fixture directory, absent on a checkout that has not run +/// `scripts/build-fixtures.sh`. +pub fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().ok()).flatten() +} + +/// A fixture's bytes and the object loaded from them. +pub fn load(name: &str) -> Option<(Vec, Object)> { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some((data, obj)) +} + +/// Everything a run is supposed to produce, flattened to a string. +/// +/// A string rather than a derived `PartialEq` because the point is to compare +/// a cached answer against a computed one field by field, and a mismatch has +/// to be readable: `assert_eq!` on two of these names the line that differs. +pub fn fingerprint(p: &e5r_analysis::Program) -> String { + let mut s = String::new(); + for f in p.functions.values() { + s.push_str(&format!( + "{:x} {:?} {} {} {} {:?} {} {}\n", + f.entry.get(), + f.name, + f.range.start().get(), + f.range.end().get(), + f.provenance, + f.cfg.halt, + f.cfg.has_indirect, + f.cfg.insns() + )); + for b in f.cfg.blocks.values() { + s.push_str(&format!( + " b {:x}-{:x} {} {} {:?} {:?}\n", + b.range.start().get(), + b.range.end().get(), + b.insns, + b.unresolved, + b.terminator, + b.successors.iter().map(|a| a.get()).collect::>() + )); + } + for t in &f.cfg.tables { + s.push_str(&format!(" t {t:?}\n")); + } + for c in &f.cfg.calls { + s.push_str(&format!(" c {:x}\n", c.get())); + } + } + for x in p.xrefs.all() { + s.push_str(&format!( + "x {:x} {:x} {:?}\n", + x.from.get(), + x.to.get(), + x.kind + )); + } + for f in &p.strings { + s.push_str(&format!( + "s {:x} {} {:?} {}\n", + f.addr.get(), + f.len, + f.encoding, + f.text + )); + } + for a in &p.noreturn { + s.push_str(&format!("n {:x}\n", a.get())); + } + s.push_str(&format!("rounds {}\n", p.rounds)); + s +} + +/// A directory of our own under the system temporary directory, removed by the +/// caller. Named for the test so a leftover says which one left it. +pub fn scratch(tag: &str) -> PathBuf { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.subsec_nanos()) + .unwrap_or(0); + let d = std::env::temp_dir().join(format!( + "e5r-cache-test-{tag}-{}-{nanos}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&d); + d +} diff --git a/crates/e5r-analysis/tests/data_partition.rs b/crates/e5r-analysis/tests/data_partition.rs new file mode 100644 index 0000000..b75f32b --- /dev/null +++ b/crates/e5r-analysis/tests/data_partition.rs @@ -0,0 +1,146 @@ +//! The code partition stops at what is provably data. +//! +//! Built by hand rather than taken from a fixture, because the failure this +//! guards against needs a coincidence and no binary in the corpus supplies +//! one: a word inside a literal pool whose bit pattern happens to be a +//! function prologue. The gap scan reads such a word as the start of a +//! function and invents one out of data. Here the coincidence is arranged on +//! purpose, so the guard is measured rather than assumed. +//! +//! The image is four instructions of real function, four `nop`s nobody +//! reaches, and then eight bytes the function loads as a literal. The first +//! four of those are `stp x29, x30, [sp, #-16]!` — a pool entry whose value +//! is a prologue encoding, which is a thing a compiler emits every day: it is +//! just the number 0xa9bf7bfd. + +use std::collections::BTreeMap; + +use e5r_analysis::{Options, Proof, analyze}; +use e5r_core::{ + Addr, AddrRange, Arch, Bits, Endian, Evidence, MemoryMap, Perms, Provenance, Segment, +}; +use e5r_format::{Format, FunctionHint, Object, Section}; + +const BASE: u64 = 0x40_0000; + +/// The literal the function reads, and which the gap scan must not walk. +const POOL: u64 = BASE + 0x20; + +fn image() -> Vec { + let words: [u32; 10] = [ + // 0x00 stp x29, x30, [sp, #-16]! the real function's prologue + 0xa9bf_7bfd, + // 0x04 ldr x0, #0x1c reads eight bytes at 0x20 + 0x5800_00e0, + // 0x08 ldp x29, x30, [sp], #16 + 0xa8c1_7bfd, + // 0x0c ret + 0xd65f_03c0, + // 0x10..0x1c nop, so the gap before the pool holds nothing + 0xd503_201f, + 0xd503_201f, + 0xd503_201f, + 0xd503_201f, + // 0x20 the literal's first word, which is also a prologue encoding + 0xa9bf_7bfd, + // 0x24 and a ret behind it, so a walk that starts here succeeds + 0xd65f_03c0, + ]; + words.iter().flat_map(|w| w.to_le_bytes()).collect() +} + +fn object() -> Object { + let bytes = image(); + let range = AddrRange::sized(Addr(BASE), bytes.len() as u64).unwrap(); + let mut memory = MemoryMap::new(); + memory.add(Segment::new(range, Perms::RX, ".text", 0, bytes.clone()).unwrap()); + Object { + format: Format::Elf, + arch: Arch::AArch64, + endian: Endian::Little, + bits: Bits::Bits64, + entry: Some(Addr(BASE)), + image_base: Addr(BASE), + pic: false, + memory, + sections: vec![Section { + name: ".text".into(), + range, + file_offset: 0, + file_size: bytes.len() as u64, + exec: true, + write: false, + kind: 1, + }], + symbols: Vec::new(), + imports: Vec::new(), + exports: Vec::new(), + function_hints: vec![FunctionHint { + addr: Addr(BASE), + size: None, + name: Some("only".into()), + provenance: Provenance::new(Evidence::SymbolTable), + }], + metadata: BTreeMap::new(), + debug: None, + warnings: Vec::new(), + } +} + +#[test] +fn a_literal_pool_is_marked_with_the_instruction_that_reads_it() { + let p = analyze(object(), &Options::default()); + let r = p + .data() + .proof_at(Addr(POOL)) + .expect("the pool the function loads is data"); + assert_eq!(r.range.start(), Addr(POOL)); + assert_eq!(r.range.len(), 8, "an `ldr x0` reads eight bytes"); + assert_eq!( + r.proof, + Proof::LiteralPool { + read_by: Addr(BASE + 4) + }, + "the proof names the instruction that reads it" + ); + assert_eq!( + p.data().refused(), + 0, + "nothing here contradicts a walked block" + ); +} + +#[test] +fn the_gap_scan_does_not_invent_a_function_inside_it() { + let with = analyze(object(), &Options::default()); + let without = analyze( + object(), + &Options { + data: false, + ..Options::default() + }, + ); + + // The point of the fixture: without the marking the scan does take the + // bait, so the test is not passing by there being nothing to find. + assert!( + without.functions.contains_key(&Addr(POOL)), + "the pool word is a prologue encoding, so an unguarded scan walks it" + ); + assert!( + !with.functions.contains_key(&Addr(POOL)), + "a function was invented out of a literal pool" + ); + assert_eq!(with.functions.len(), 1, "only the function that exists"); + assert_eq!(with.functions.keys().next(), Some(&Addr(BASE))); +} + +#[test] +fn the_bytes_marked_are_reported_against_the_section() { + let p = analyze(object(), &Options::default()); + let text = p.object.section(".text").unwrap().range; + assert_eq!(p.data().bytes(), 8); + assert_eq!(p.data().bytes_in(text), 8); + // And the run is one step for a scan, not two words. + assert_eq!(p.data().end_of_run(Addr(POOL)), Some(Addr(POOL + 8))); +} diff --git a/crates/e5r-analysis/tests/jumptable_a64.rs b/crates/e5r-analysis/tests/jumptable_a64.rs new file mode 100644 index 0000000..39dac9c --- /dev/null +++ b/crates/e5r-analysis/tests/jumptable_a64.rs @@ -0,0 +1,322 @@ +//! AArch64 compact jump tables anchored on an `adr`, checked against an +//! external disassembly. +//! +//! The expectations below are transcribed from +//! +//! aarch64-linux-gnu-objdump -d fixtures/build/em-paths.a64.O1 +//! aarch64-linux-gnu-objdump -d fixtures/build/em-paths.a64.O2 +//! aarch64-linux-gnu-objdump -d fixtures/build/hello.static.a64 +//! od -A x -t x1 -j 0x558 -N 0xb fixtures/build/em-paths.a64.O1 +//! od -A x -t x1 -j 0x60c -N 0xb fixtures/build/em-paths.a64.O2 +//! od -A x -t x2 -j 0x5af50 -N 24 fixtures/build/hello.static.a64 +//! +//! and nothing here was read off our own recovery: the anchor, the entry +//! count and every target come from outside this codebase. That matters more +//! for this form than for most, because the gap it closes was invisible from +//! the inside. `pick` simply stopped at its `br` and reported an unresolved +//! indirect branch, which looks exactly like a switch nobody could have +//! resolved. +//! +//! The shape is what gcc emits for a small dense switch at `-O1` and `-Os`: +//! +//! ldrb w1, [x1, w3, uxtw] ; a one-byte entry +//! adr x3, ; not the table's own address +//! add x1, x3, w1, sxtb #2 ; sign extended and scaled by four +//! br x1 +//! +//! Two things have to hold at once for it. The offset is signed and scaled +//! from an anchor that is neither the table nor the entry, and the compare +//! that bounds the switch names a register the value was moved out of, which +//! the `adr` then overwrites two instructions before the branch. The `ldrh` +//! case in `__gettextparse` is the same shape a halfword wide, which is what a +//! compiler emits once the span of the switch outgrows a byte. + +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, TableKind, analyze}; +use e5r_core::{Addr, Arch}; +use e5r_format::LoadOptions; + +/// One switch, as an outside disassembler and `od` report it. +struct Switch { + fixture: &'static str, + func: &'static str, + /// The `br` line of the listing. + branch: u64, + /// The `adrp`/`add` pair that addresses the entries. + table: u64, + /// Bytes per entry, from the load mnemonic. + entry_size: u64, + /// The `adr` line, which is where the offsets are measured from. + anchor: u64, + /// `#2` on the add's extend: entries count instructions, not bytes. + shift: u8, + /// Every entry, in table order, turned into an address by hand. + targets: &'static [u64], + /// True when the fixture statically links whatever libc built it. + /// + /// Then these addresses are a fact about that libc and not about the + /// program, so on a machine whose libc differs they are not a weaker + /// expectation -- they are a false one. The halfword form still has to be + /// recovered and bounded there; only the transcribed addresses are held + /// back, and the run says so rather than passing quietly. + from_system_libc: bool, +} + +const SWITCHES: &[Switch] = &[ + // cmp w2, #0xa ; ... ; mov x3, x2 ; adrp x1, 0x400000 ; add x1, x1, #0x558 + // ldrb w1, [x1, w3, uxtw] ; adr x3, 0x400390 ; add x1, x3, w1, sxtb #2 + // Table bytes: 00 03 05 07 09 0b 0d 0f 11 13 15. + Switch { + fixture: "em-paths.a64.O1", + func: "pick", + branch: 0x40038c, + table: 0x400558, + entry_size: 1, + anchor: 0x400390, + shift: 2, + targets: &[ + 0x400390, 0x40039c, 0x4003a4, 0x4003ac, 0x4003b4, 0x4003bc, 0x4003c4, 0x4003cc, + 0x4003d4, 0x4003dc, 0x4003e4, + ], + from_system_libc: false, + }, + // The same source at -O2, where the compare names the index register + // directly. It recovered before this form was modelled, so it is the + // control: if it ever changes, the change is not about the anchor. + // Table bytes: 02 03 04 05 06 07 08 09 0a 00 01. + Switch { + fixture: "em-paths.a64.O2", + func: "pick", + branch: 0x4003a4, + table: 0x40060c, + entry_size: 1, + anchor: 0x4003a8, + shift: 2, + targets: &[ + 0x4003b0, 0x4003b4, 0x4003b8, 0x4003bc, 0x4003c0, 0x4003c4, 0x4003c8, 0x4003cc, + 0x4003d0, 0x4003a8, 0x4003ac, + ], + from_system_libc: false, + }, + // cmp w10, #0xb ; b.hi ; adrp x4, 0x45a000 ; add x4, x4, #0xf50 + // ldrh w4, [x4, w10, uxtw #1] ; adr x10, 0x433dbc ; add x4, x10, w4, sxth #2 + // Table halfwords: 0089 008e 00a4 00b8 0077 0028 0028 0028 00ce 010f 0119 00cc. + Switch { + fixture: "hello.static.a64", + func: "__gettextparse", + branch: 0x433db8, + table: 0x45af50, + entry_size: 2, + anchor: 0x433dbc, + shift: 2, + targets: &[ + 0x433fe0, 0x433ff4, 0x43404c, 0x43409c, 0x433f98, 0x433e5c, 0x433e5c, 0x433e5c, + 0x4340f4, 0x4341f8, 0x434220, 0x4340ec, + ], + from_system_libc: true, + }, +]; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option<(Program, Vec)> { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some((analyze(obj, &Options::default()), data)) +} + +/// The bytes a mapped address names, straight out of the file. +fn at(p: &Program, data: &[u8], addr: u64, len: usize) -> Option> { + let sec = p.object.section_at(Addr(addr))?; + let off = (sec.file_offset + (addr - sec.range.start().0)) as usize; + (off + len <= data.len() && addr - sec.range.start().0 + len as u64 <= sec.file_size) + .then(|| data[off..off + len].to_vec()) +} + +/// Every expectation here is transcribed from a listing of one build, so a +/// fixture compiled by a different toolchain is a different program sitting at +/// the same path -- and the assertions below would measure it and report a +/// recovery bug that is really a corpus that moved. That is not hypothetical: +/// `build-fixtures.sh` chose its compiler by host, so on an x86-64 machine +/// every `.a64` fixture was an x86-64 binary, and this test failed for a +/// reason its message did not mention. +/// +/// So the file is checked against the transcription before anything is +/// concluded from it. The table entries are read out of the fixture and turned +/// into targets here, by the arithmetic the listing's `add` spells out, which +/// makes this an independent derivation of `targets` rather than a second +/// reading of the same recovery. +fn is_the_transcribed_build(p: &Program, data: &[u8], s: &Switch) -> Result<(), String> { + if p.object.arch != Arch::AArch64 { + return Err(format!( + "{} is {}, not AArch64: build-fixtures.sh built the corpus with a \ + host compiler, so this file is not the program these expectations \ + describe", + s.fixture, p.object.arch + )); + } + let n = s.targets.len(); + let raw = at(p, data, s.table, n * s.entry_size as usize) + .ok_or_else(|| format!("{}: nothing mapped at {:#x}", s.fixture, s.table))?; + let derived: Vec = raw + .chunks(s.entry_size as usize) + .map(|e| { + // Sign extended from the entry's own width, then scaled: the + // listing's `sxtb #2` and `sxth #2`. + let v = match e { + [b] => *b as i8 as i64, + [lo, hi] => i16::from_le_bytes([*lo, *hi]) as i64, + _ => unreachable!("entry widths are one and two bytes"), + }; + (s.anchor as i64 + (v << s.shift)) as u64 + }) + .collect(); + if derived != s.targets { + return Err(format!( + "{}: the table at {:#x} does not hold the entries this listing was \ + transcribed from -- the fixture is a different build, and these \ + expectations have to be re-transcribed from it\n file: {:#x?}\n \ + transcribed: {:#x?}", + s.fixture, s.table, derived, s.targets + )); + } + Ok(()) +} + +#[test] +fn an_anchored_compact_table_recovers_what_objdump_shows() { + let mut checked = 0; + let mut present = 0; + // Cases whose fixture is a different build of a system library, where the + // transcribed addresses are checked on the machine they came from. + let mut elsewhere = 0; + for s in SWITCHES { + let Some((p, data)) = open(s.fixture) else { + continue; + }; + present += 1; + let transcribed = match is_the_transcribed_build(&p, &data, s) { + Ok(()) => true, + // A different libc is a different program, so the addresses below + // do not describe it. The shape still does, and is checked. + Err(why) if s.from_system_libc => { + println!("note: {why}"); + elsewhere += 1; + false + } + Err(why) => panic!("{why}"), + }; + let f = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(s.func)) + .unwrap_or_else(|| panic!("{}/{}: no such function", s.fixture, s.func)); + + let where_ = format!("{}/{}", s.fixture, s.func); + // By branch address rather than by position: a function can hold more + // than one switch, and `__gettextparse` holds two. Where the addresses + // belong to another libc, by entry width instead -- which is the thing + // this case is here for, the halfword form a byte-wide table cannot + // reach. + let t = if transcribed { + f.cfg.tables.iter().find(|t| t.at == Addr(s.branch)) + } else { + f.cfg.tables.iter().find(|t| t.entry_size == s.entry_size) + } + .unwrap_or_else(|| { + panic!( + "{where_}: no table for the branch at {:#x}, {} recovered in this function", + s.branch, + f.cfg.tables.len() + ) + }); + + if transcribed { + assert_eq!(t.table, Addr(s.table), "{where_}: wrong table address"); + } + assert_eq!(t.entry_size, s.entry_size, "{where_}: wrong entry width"); + assert_eq!( + t.kind, + TableKind::RelativeToBase, + "{where_}: wrong entry encoding" + ); + if transcribed { + assert_eq!( + t.base, + Addr(s.anchor), + "{where_}: offsets measured from the wrong place" + ); + } + assert_eq!(t.shift, s.shift, "{where_}: wrong scale"); + assert!( + !t.bounded_by_scan, + "{where_}: sized by scanning, so the count is a guess" + ); + + let got: Vec = t.targets.iter().map(|a| a.get()).collect(); + if transcribed { + assert_eq!( + got.len(), + s.targets.len(), + "{where_}: {} cases recovered, the guard allows {}", + got.len(), + s.targets.len() + ); + assert_eq!(got, s.targets, "{where_}: targets differ from the table"); + } else { + // Every target inside the function that branched, which is what a + // bounded table means and what a guess would break. + assert!(!got.is_empty(), "{where_}: a table with no cases"); + for a in &got { + assert!( + f.range.contains(Addr(*a)), + "{where_}: case {a:#x} is outside the function" + ); + } + } + checked += 1; + } + assert!( + checked == present, + "only {checked} of {present} present fixtures recovered their table" + ); + assert!( + present - elsewhere >= 2, + "{elsewhere} of {present} cases were system-library builds: the \ + transcribed addresses went unchecked here" + ); +} + +#[test] +fn the_functions_holding_them_analyze_completely() { + // The point of the exercise. A switch nobody reads leaves the function + // with an unresolved indirect branch, and `pick` at -O1 was the one this + // form cost us. + for (fixture, func) in [ + ("em-paths.a64.O1", "pick"), + ("em-paths.a64.O2", "pick"), + ("hello.static.a64", "__gettextparse"), + ] { + let Some((p, _)) = open(fixture) else { + continue; + }; + assert_eq!( + p.object.arch, + Arch::AArch64, + "{fixture} is {}, not AArch64: the corpus was built with a host compiler", + p.object.arch + ); + let f = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(func)) + .unwrap_or_else(|| panic!("{fixture}/{func}: no such function")); + assert!( + !f.cfg.has_indirect, + "{fixture}/{func}: an indirect branch nothing resolved" + ); + assert!(f.is_complete(), "{fixture}/{func}: still incomplete"); + } +} diff --git a/crates/e5r-analysis/tests/jumptable_x64.rs b/crates/e5r-analysis/tests/jumptable_x64.rs new file mode 100644 index 0000000..d1c2b4e --- /dev/null +++ b/crates/e5r-analysis/tests/jumptable_x64.rs @@ -0,0 +1,179 @@ +//! x86-64 jump table recovery, checked against an external disassembly. +//! +//! The expectations below are transcribed from +//! +//! llvm-objdump -d fixtures/build/dt-control.x64.O{1,2} +//! od -A x -t x8 -j 0x1b8 -N 0x110 fixtures/build/dt-control.x64.O{1,2} +//! +//! which is the point: a test that only compared recovery against recovery +//! would have passed just as happily while every one of these switches went +//! unresolved. The table addresses, the entry counts and every target come +//! from outside this codebase. +//! +//! The shape is clang's non-PIE dense switch: +//! +//! cmp edi, 0xa +//! ja default +//! mov eax, edi +//! jmp qword ptr [8*rax + 0x2001b8] +//! +//! an absolute table of eight-byte targets addressed with no base register, +//! bounded by a compare that names `edi` where the branch names `rax`. + +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, TableKind, analyze}; +use e5r_core::Addr; +use e5r_format::LoadOptions; + +/// One switch, as an outside disassembler reports it. +struct Switch { + fixture: &'static str, + func: &'static str, + /// The indirect branch, from the `jmpq` line of the listing. + branch: u64, + /// The table, from the branch's displacement. + table: u64, + /// Every entry, in table order, read out of `.rodata`. + targets: &'static [u64], +} + +const SWITCHES: &[Switch] = &[ + Switch { + fixture: "dt-control.x64.O2", + func: "switchind", + branch: 0x20161c, + table: 0x2001b8, + targets: &[ + 0x201623, 0x20168c, 0x20165b, 0x201676, 0x201631, 0x20169a, 0x2016a1, 0x201681, + 0x2016d1, 0x201646, 0x2016ca, + ], + }, + Switch { + fixture: "dt-control.x64.O2", + func: "switchloop", + branch: 0x201700, + table: 0x200210, + targets: &[ + 0x20171f, 0x201707, 0x201710, 0x201715, 0x2016ea, 0x201724, 0x20172b, 0x20171a, + 0x201737, 0x20170b, 0x201732, 0x201707, + ], + }, + Switch { + fixture: "dt-control.x64.O2", + func: "ifswitch", + branch: 0x201771, + table: 0x200270, + targets: &[ + 0x201797, 0x201778, 0x20179e, 0x20177f, 0x2017ac, 0x2017b6, 0x2017a5, 0x2017e3, + 0x201790, 0x2017d2, 0x201778, + ], + }, + Switch { + fixture: "dt-control.x64.O1", + func: "switchind", + branch: 0x20163c, + table: 0x2001b8, + targets: &[ + 0x201643, 0x2016ac, 0x20167b, 0x201696, 0x201651, 0x2016ba, 0x2016c1, 0x2016a1, + 0x2016f1, 0x201666, 0x2016ea, + ], + }, + Switch { + fixture: "dt-control.x64.O1", + func: "switchloop", + branch: 0x201720, + table: 0x200210, + targets: &[ + 0x20173f, 0x201727, 0x201730, 0x201735, 0x20170a, 0x201744, 0x20174b, 0x20173a, + 0x201757, 0x20172b, 0x201752, 0x201727, + ], + }, + Switch { + fixture: "dt-control.x64.O1", + func: "ifswitch", + branch: 0x201793, + table: 0x200270, + targets: &[ + 0x2017b2, 0x20179a, 0x2017b9, 0x2017a1, 0x2017cb, 0x2017d5, 0x2017be, 0x201802, + 0x2017ad, 0x2017f1, 0x20179a, + ], + }, +]; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +#[test] +fn x86_64_switches_recover_the_table_objdump_shows() { + let mut checked = 0; + for s in SWITCHES { + let Some(p) = open(s.fixture) else { continue }; + let f = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(s.func)) + .unwrap_or_else(|| panic!("{}/{}: no such function", s.fixture, s.func)); + + let where_ = format!("{}/{}", s.fixture, s.func); + assert_eq!( + f.cfg.tables.len(), + 1, + "{where_}: {} tables recovered, objdump shows one indirect branch", + f.cfg.tables.len() + ); + let t = &f.cfg.tables[0]; + assert_eq!(t.at, Addr(s.branch), "{where_}: wrong branch"); + assert_eq!(t.table, Addr(s.table), "{where_}: wrong table address"); + assert_eq!(t.entry_size, 8, "{where_}: wrong entry width"); + assert_eq!( + t.kind, + TableKind::Absolute, + "{where_}: wrong entry encoding" + ); + assert!( + !t.bounded_by_scan, + "{where_}: sized by scanning, so the count is a guess" + ); + + let got: Vec = t.targets.iter().map(|a| a.get()).collect(); + assert_eq!( + got.len(), + s.targets.len(), + "{where_}: {} cases recovered, the table holds {}", + got.len(), + s.targets.len() + ); + assert_eq!(got, s.targets, "{where_}: targets differ from the table"); + assert!(f.is_complete(), "{where_}: function still incomplete"); + checked += 1; + } + assert!( + checked == SWITCHES.len() || checked == 0, + "only {checked} of {} fixtures present", + SWITCHES.len() + ); +} + +#[test] +fn no_x86_64_switch_is_left_unresolved() { + // Every indirect branch in these two builds is a switch, so an unresolved + // one is a recovery failure rather than a genuinely unknowable target. + for fixture in ["dt-control.x64.O1", "dt-control.x64.O2"] { + let Some(p) = open(fixture) else { continue }; + for f in p.functions_by_address() { + assert!( + !f.cfg.has_indirect, + "{fixture}/{}: an indirect branch nothing resolved", + f.name.as_deref().unwrap_or("?") + ); + } + } +} diff --git a/crates/e5r-analysis/tests/progress.rs b/crates/e5r-analysis/tests/progress.rs new file mode 100644 index 0000000..5d9b199 --- /dev/null +++ b/crates/e5r-analysis/tests/progress.rs @@ -0,0 +1,119 @@ +//! What a caller watching `analyze()` is told, and what it costs. +//! +//! Two things have to hold for a progress meter to be worth having. It has to +//! report every stage that runs, in the order they run, with a count that only +//! moves forward inside a pass. And attaching one must not change the answer: +//! a run being watched and a run not being watched have to agree function for +//! function, or the meter is measuring something other than the analysis. + +mod common; + +use std::sync::{Arc, Mutex}; + +use e5r_analysis::{Options, Session, Stage, Update}; +use e5r_format::Object; + +/// Analyze with every update recorded. +/// +/// The callback owns a handle rather than borrowing one, because it outlives +/// this frame as far as the type system is concerned: the session may hand it +/// to the thread pool. +fn watch(object: Object, opts: &Options) -> (e5r_analysis::Program, Vec) { + let seen: Arc>> = Arc::new(Mutex::new(Vec::new())); + let into = Arc::clone(&seen); + let p = Session::new(object, opts.clone()) + .with_progress(move |u| into.lock().unwrap().push(u)) + .into_program(); + let updates = seen.lock().unwrap().clone(); + (p, updates) +} + +#[test] +fn every_stage_reports_and_the_counts_only_go_up() { + let Some((_, object)) = common::load("hello.static.a64") else { + return; + }; + let (_, updates) = watch(object, &Options::default()); + assert!(!updates.is_empty(), "analyze() reported nothing"); + + // Every stage the options ask for has to appear. A stage that silently + // stopped reporting would leave a caller staring at a frozen bar. + for stage in [Stage::Discovery, Stage::Data, Stage::Xrefs, Stage::Strings] { + assert!( + updates.iter().any(|u| u.stage == stage), + "no report from {}", + stage.label() + ); + } + + // Inside one pass of one stage, `done` never goes backwards and never + // passes the total it declared. + let mut last: Option = None; + for u in &updates { + if let Some(p) = last + && p.stage == u.stage + && p.round == u.round + { + assert!( + u.done >= p.done, + "{} went from {} back to {}", + u.stage.label(), + p.done, + u.done + ); + } + if let Some(total) = u.total { + assert!( + u.done <= total, + "{} reported {} of {}", + u.stage.label(), + u.done, + total + ); + assert!(u.fraction().is_some()); + } + last = Some(*u); + } + + // And each pass that declared a total finished at it, so a bar that + // reached 90% is not left there. + for stage in [Stage::Xrefs, Stage::Strings, Stage::Data] { + let last = updates.iter().rfind(|u| u.stage == stage).unwrap(); + assert_eq!( + Some(last.done), + last.total, + "{} stopped short of its own total", + stage.label() + ); + } +} + +#[test] +fn a_stage_that_is_switched_off_does_not_report() { + let Some((_, object)) = common::load("hello.a64.O2") else { + return; + }; + let opts = Options { + strings: false, + xrefs: false, + ..Options::default() + }; + let (_, updates) = watch(object, &opts); + assert!(!updates.iter().any(|u| u.stage == Stage::Strings)); + assert!(!updates.iter().any(|u| u.stage == Stage::Xrefs)); + assert!(updates.iter().any(|u| u.stage == Stage::Discovery)); +} + +#[test] +fn watching_a_run_does_not_change_it() { + // The whole point. If these two disagree the meter is not passive. + let Some((_, a)) = common::load("hello.static.a64") else { + return; + }; + let (_, b) = common::load("hello.static.a64").unwrap(); + let opts = Options::default(); + let (watched, updates) = watch(a, &opts); + let plain = e5r_analysis::analyze(b, &opts); + assert!(updates.len() > 4); + assert_eq!(common::fingerprint(&watched), common::fingerprint(&plain)); +} diff --git a/crates/e5r-analysis/tests/quality.rs b/crates/e5r-analysis/tests/quality.rs new file mode 100644 index 0000000..cefc45a --- /dev/null +++ b/crates/e5r-analysis/tests/quality.rs @@ -0,0 +1,330 @@ +//! G4: function recovery measured against the symbol table as ground truth. +//! +//! A symbol table is not DWARF, but on a binary that has one it names every +//! function the compiler emitted, which is exactly the set discovery has to +//! find. Running against the stripped copy of the same binary measures what +//! discovery achieves with the symbols taken away. + +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::Addr; +use e5r_format::{LoadOptions, SymbolKind}; + +/// Recall floors. Raise as discovery improves; never lower. +/// +/// Stripped sits high because `.eh_frame` survives stripping and carries the +/// boundaries, which is the reason the loader reads it. A fixture built without +/// unwind tables would sit well below this, and would need its own floor rather +/// than a lowering of this one. +const MIN_RECALL_NAMED: f64 = 0.99; +const MIN_RECALL_STRIPPED: f64 = 0.95; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// Function entries the symbol table names, which is the answer key. +fn ground_truth(p: &Program) -> Vec { + let mut v: Vec = p + .object + .symbols + .iter() + .filter(|s| { + s.kind == SymbolKind::Function + && s.addr != Addr::ZERO + && p.object.memory.is_executable(s.addr) + }) + .map(|s| s.addr) + .collect(); + v.sort_unstable(); + v.dedup(); + v +} + +fn recall(p: &Program, truth: &[Addr]) -> f64 { + if truth.is_empty() { + return 1.0; + } + let found = truth.iter().filter(|a| p.function(**a).is_some()).count(); + found as f64 / truth.len() as f64 +} + +#[test] +fn every_named_function_is_recovered() { + let Some(p) = open("hello.a64.O0") else { + return; + }; + let truth = ground_truth(&p); + assert!( + truth.len() >= 5, + "only {} functions in the answer key", + truth.len() + ); + let r = recall(&p, &truth); + let missed: Vec = truth + .iter() + .filter(|a| p.function(**a).is_none()) + .map(|a| { + format!( + "{a} {}", + p.object + .symbol_at(*a) + .map(|s| s.name.clone()) + .unwrap_or_default() + ) + }) + .collect(); + assert!( + r >= MIN_RECALL_NAMED, + "recall {:.3} below {MIN_RECALL_NAMED}; missed {missed:?}", + r + ); +} + +#[test] +fn stripping_the_symbols_keeps_most_functions() { + let (Some(full), Some(stripped)) = (open("hello.a64.O2"), open("hello.a64.O2.stripped")) else { + return; + }; + let truth = ground_truth(&full); + if truth.len() < 5 { + return; + } + let r = recall(&stripped, &truth); + assert!( + r >= MIN_RECALL_STRIPPED, + "stripped recall {:.3} below {MIN_RECALL_STRIPPED} on {} functions", + r, + truth.len() + ); +} + +#[test] +fn analysis_is_deterministic_across_thread_counts() { + let Some(dir) = corpus() else { return }; + let data = std::fs::read(dir.join("hello.a64.O0")).unwrap(); + let obj = e5r_format::load(&data, &LoadOptions::default()).unwrap(); + + let fingerprint = |threads: usize| { + let p = analyze( + obj.clone(), + &Options { + threads: Some(threads), + ..Options::default() + }, + ); + let funcs: Vec<(u64, usize, u32)> = p + .functions_by_address() + .map(|f| (f.entry.get(), f.cfg.blocks.len(), f.cfg.insns())) + .collect(); + let xrefs: Vec<(u64, u64)> = p + .xrefs + .all() + .iter() + .map(|x| (x.from.get(), x.to.get())) + .collect(); + (funcs, xrefs, p.strings.len()) + }; + + let one = fingerprint(1); + for t in [2, 4, 8] { + assert_eq!(one, fingerprint(t), "thread count {t} changed the answer"); + } +} + +#[test] +fn no_function_claims_bytes_outside_executable_memory() { + let Some(p) = open("hello.a64.O0") else { + return; + }; + for f in p.functions_by_address() { + for b in f.cfg.blocks.values() { + assert!( + p.object.memory.is_executable(b.range.start()), + "{} block at {} is not executable", + f.display_name(), + b.range + ); + } + } +} + +#[test] +fn blocks_within_a_function_do_not_overlap() { + let Some(p) = open("hello.a64.O0") else { + return; + }; + for f in p.functions_by_address() { + let mut ranges: Vec<_> = f.cfg.blocks.values().map(|b| b.range).collect(); + ranges.sort_by_key(|r| r.start()); + for w in ranges.windows(2) { + assert!( + !w[0].overlaps(w[1]), + "{}: {} overlaps {}", + f.display_name(), + w[0], + w[1] + ); + } + } +} + +#[test] +fn every_block_successor_is_a_block_or_outside_the_function() { + let Some(p) = open("hello.a64.O0") else { + return; + }; + for f in p.functions_by_address() { + for (at, b) in &f.cfg.blocks { + for s in &b.successors { + assert!( + f.cfg.blocks.contains_key(s), + "{}: block {at} goes to {s}, which is not a block", + f.display_name() + ); + } + } + } +} + +#[test] +fn a_tail_call_does_not_merge_two_functions() { + // shapes.c has `tail_call` calling `sum_to` as its last act. The two must + // stay separate: running them together is the classic sweep failure. + let Some(p) = open("shapes.a64.O2.o") else { + return; + }; + let (Some(tail), Some(sum)) = ( + p.functions_by_address() + .find(|f| f.name.as_deref() == Some("tail_call")), + p.functions_by_address() + .find(|f| f.name.as_deref() == Some("sum_to")), + ) else { + return; + }; + assert_ne!(tail.entry, sum.entry); + for b in tail.cfg.blocks.values() { + assert!( + !b.range.contains(sum.entry), + "tail_call swallowed sum_to at {}", + sum.entry + ); + } +} + +#[test] +fn large_binaries_analyze_without_panicking() { + // The real corpus is where the panics are: this caught a zero-length block + // that made a BTreeSet range with equal excluded bounds. + for path in [ + "/lib/aarch64-linux-gnu/libc.so.6", + "/usr/lib/aarch64-linux-gnu/libc.so.6", + "/bin/bash", + ] { + let p = Path::new(path); + if !p.is_file() { + continue; + } + let data = std::fs::read(p).unwrap(); + let Ok(obj) = e5r_format::load(&data, &LoadOptions::default()) else { + continue; + }; + let prog = analyze(obj, &Options::default()); + assert!( + prog.functions.len() > 100, + "{path}: only {} functions", + prog.functions.len() + ); + } +} + +#[test] +fn a_dense_switch_resolves_to_every_arm() { + // shapes.c and wide.c both hold a dense switch, which a compiler turns + // into a jump table. Every arm has to become a successor, or the function + // is not fully analyzed. + for (fixture, func, arms) in [ + ("shapes.a64.O2.o", "dense_switch", 8usize), + ("wide.a64.O2.o", "dense", 12), + ("wide.x64.O2.o", "dense", 12), + ] { + let Some(p) = open(fixture) else { continue }; + let Some(f) = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(func)) + else { + continue; + }; + // Either the compiler used a table, in which case it must resolve, or + // it used a comparison chain, in which case there is nothing indirect. + if f.cfg.tables.is_empty() { + assert!( + !f.cfg.has_indirect, + "{fixture}/{func}: an indirect branch nothing resolved" + ); + continue; + } + let t = &f.cfg.tables[0]; + assert!( + t.targets.len() >= arms, + "{fixture}/{func}: {} targets for {arms} arms", + t.targets.len() + ); + for target in &t.targets { + assert!( + p.object.memory.is_executable(*target), + "{fixture}/{func}: target {target} is not executable" + ); + assert!( + f.range.contains(*target), + "{fixture}/{func}: target {target} is outside the function" + ); + } + assert!(f.is_complete(), "{fixture}/{func}: still incomplete"); + } +} + +/// A work list, not a gate: what jump table recovery found on a real binary. +#[test] +#[ignore] +fn jump_table_report() { + for path in [ + "/usr/lib/aarch64-linux-gnu/libc.so.6", + "/lib/aarch64-linux-gnu/libc.so.6", + ] { + let p = Path::new(path); + if !p.is_file() { + continue; + } + let data = std::fs::read(p).unwrap(); + let obj = e5r_format::load(&data, &LoadOptions::default()).unwrap(); + let prog = analyze(obj, &Options::default()); + let mut all: Vec<_> = prog + .functions_by_address() + .flat_map(|f| f.cfg.tables.iter().map(move |t| (f.entry, t))) + .collect(); + all.sort_by_key(|(_, t)| std::cmp::Reverse(t.targets.len())); + println!("{} tables", all.len()); + for (f, t) in all.iter().take(20) { + println!( + "fn {f} branch {} table {} size {} kind {:?} base {} -> {} targets (scan={})", + t.at, + t.table, + t.entry_size, + t.kind, + t.base, + t.targets.len(), + t.bounded_by_scan + ); + } + break; + } +} diff --git a/crates/e5r-analysis/tests/session.rs b/crates/e5r-analysis/tests/session.rs new file mode 100644 index 0000000..a70a218 --- /dev/null +++ b/crates/e5r-analysis/tests/session.rs @@ -0,0 +1,102 @@ +//! The lazy session: what it computes, when, and that it agrees with the +//! eager path it replaced. + +mod common; + +use common::{fingerprint, load}; +use e5r_analysis::{Options, Part, Session, analyze}; + +#[test] +fn asking_for_functions_does_not_compute_strings_or_xrefs() { + let Some((_, obj)) = load("hello.a64.O2") else { + return; + }; + let s = Session::new(obj, Options::default()); + assert!(!s.is_computed(Part::Functions)); + assert!(!s.functions().is_empty()); + assert!(s.is_computed(Part::Functions)); + assert!( + !s.is_computed(Part::Xrefs) && !s.is_computed(Part::Strings), + "a session computed parts nothing asked for" + ); + // And the parts that were asked for are still there afterwards. + assert!(!s.strings().is_empty()); + assert!(s.is_computed(Part::Strings)); + assert!(!s.is_computed(Part::Xrefs)); +} + +#[test] +fn cross_references_pull_in_the_functions_they_need() { + let Some((_, obj)) = load("hello.a64.O2") else { + return; + }; + let s = Session::new(obj, Options::default()); + let _ = s.xrefs(); + assert!( + s.is_computed(Part::Functions), + "xrefs are read off functions, so asking for them must compute functions" + ); +} + +#[test] +fn a_session_forced_to_the_end_equals_the_eager_analysis() { + for name in ["hello.a64.O2", "hello.a64.O0", "hello.go.stripped"] { + let Some((_, obj)) = load(name) else { + continue; + }; + let eager = analyze(obj, &Options::default()); + let (_, obj2) = load(name).expect("second load"); + let lazy = Session::new(obj2, Options::default()).into_program(); + assert_eq!( + fingerprint(&eager), + fingerprint(&lazy), + "{name}: lazy and eager disagree" + ); + } +} + +#[test] +fn a_part_switched_off_is_empty_either_way() { + let Some((_, obj)) = load("hello.a64.O2") else { + return; + }; + let opts = Options { + strings: false, + xrefs: false, + noreturn: false, + ..Options::default() + }; + let s = Session::new(obj, opts.clone()); + assert!(s.strings().is_empty()); + assert!(s.xrefs().is_empty()); + assert!(s.noreturn().is_empty()); + + let (_, obj2) = load("hello.a64.O2").expect("second load"); + let eager = analyze(obj2, &opts); + assert_eq!(fingerprint(&eager), fingerprint(&s.into_program())); +} + +#[test] +fn a_thread_count_still_reaches_every_stage() { + let Some((_, obj)) = load("hello.a64.O2") else { + return; + }; + let one = Session::new( + obj, + Options { + threads: Some(1), + ..Options::default() + }, + ) + .into_program(); + let (_, obj2) = load("hello.a64.O2").expect("second load"); + let many = Session::new( + obj2, + Options { + threads: Some(4), + ..Options::default() + }, + ) + .into_program(); + assert_eq!(fingerprint(&one), fingerprint(&many)); +} diff --git a/crates/e5r-analysis/tests/xref_stream.rs b/crates/e5r-analysis/tests/xref_stream.rs new file mode 100644 index 0000000..ea75b1a --- /dev/null +++ b/crates/e5r-analysis/tests/xref_stream.rs @@ -0,0 +1,56 @@ +//! The streamed cross reference walk against the one that decoded a whole +//! function first. +//! +//! Cross references are read off instructions in address order, and the tracker +//! that resolves an `adrp`/`add` pair carries state from one instruction to the +//! next. Feeding it one instruction at a time instead of a decoded list saves +//! 224 bytes per instruction on every thread at once, and is only worth doing +//! if it cannot change an answer. This checks that on every function of every +//! fixture, which is the only way to know. + +mod common; + +use common::{corpus, load}; +use e5r_analysis::{Options, Session, cfg, xref}; + +#[test] +fn streaming_agrees_with_decoding_the_whole_function() { + let Some(dir) = corpus() else { + return; + }; + let mut checked = 0usize; + for name in ["hello.a64.O2", "hello.a64.O0", "hello.go", "panicky"] { + if !dir.join(name).exists() { + continue; + } + let Some((_, obj)) = load(name) else { continue }; + let session = Session::new(obj, Options::default()); + let functions = session.functions(); + let mem = &session.object().memory; + let arch = &session.object().arch; + for f in functions.values() { + let insns = cfg::instructions(mem, arch, &f.cfg); + let mut whole = Vec::new(); + xref::collect(&insns, mem, &mut whole); + + let mut streamed = Vec::new(); + let mut c = xref::Collector::new(mem, &mut streamed); + let ordered = cfg::for_each_instruction(mem, arch, &f.cfg, |i| c.push(i)); + c.finish(); + + assert!( + ordered, + "{name}: blocks of {:x} are out of address order", + f.entry.get() + ); + assert_eq!( + whole, + streamed, + "{name}: streaming changed the references of {:x}", + f.entry.get() + ); + checked += 1; + } + } + assert!(checked > 100, "only {checked} functions were compared"); +} diff --git a/crates/e5r-api/Cargo.toml b/crates/e5r-api/Cargo.toml new file mode 100644 index 0000000..6aa96f4 --- /dev/null +++ b/crates/e5r-api/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "e5r-api" +description = "e5r — the library surface the CLI and the servers share." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +e5r-core = { workspace = true } +e5r-format = { workspace = true } +e5r-analysis = { workspace = true } +e5r-ir = { workspace = true } +e5r-decomp = { workspace = true } +e5r-db = { workspace = true } +e5r-types = { workspace = true } diff --git a/crates/e5r-api/src/classes.rs b/crates/e5r-api/src/classes.rs new file mode 100644 index 0000000..2b1a335 --- /dev/null +++ b/crates/e5r-api/src/classes.rs @@ -0,0 +1,1489 @@ +//! C++ classes: the hierarchy, the constructors and destructors, and `this`. +//! +//! [`crate::vtables`] finds the tables. A table on its own is a list of +//! function pointers with an address for a name; it says nothing about what a +//! class inherits from, which of its functions build an object and which tear +//! one down, or what the first argument of any of them points at. This module +//! answers those three, and says for each answer what it rests on. +//! +//! The Itanium C++ ABI publishes the layout this reads. A virtual table's +//! second word points at a `std::type_info` derived object whose own first +//! word names one of `__class_type_info`, `__si_class_type_info` or +//! `__vmi_class_type_info`; the second points at the mangled type name; and +//! what follows gives the direct bases with their offsets and access flags. +//! Read that and a list of tables becomes a class hierarchy. +//! +//! Nothing here invents a name or a field. A class name out of the type +//! information is proven, and so is one out of a mangled symbol. A constructor +//! recognized by the vtable pointer it writes is inferred, and so is the type +//! of a `this` that only a vtable slot argued for. A binary built with +//! `-fno-rtti` has no type information to read, and [`Classes::rtti`] says so +//! rather than letting the vtable-only answer pass for the full one. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_analysis::Program; +use e5r_core::{Addr, Bits, Endian, Strength}; +use e5r_ir::op::{Op, Space}; +use e5r_ir::ssa::{Location, Operand, SsaFunction, SsaKind, Value}; + +use crate::vtables::{VTable, vtables}; + +/// What a recovered fact about a class rests on. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Basis { + /// The Itanium type information, identified by the ABI's own symbols. The + /// compiler wrote the class name, the bases and their offsets down in a + /// structure the ABI defines and the runtime reads. + Rtti, + /// A mangled symbol name: `_ZTV` for a table, `_ZTI` for type information, + /// `C1` or `D0` for what a member function is. + MangledName, + /// Type information whose kind was decided by its shape, because the ABI's + /// own `__class_type_info` symbols are not in this binary to compare + /// against. The structure is the ABI's; which of the three it is, is a + /// conclusion. + RttiShape, + /// A slot in a virtual table. The first argument of a function reached + /// through a class's table is that class, by the ABI; that the function is + /// a member of exactly this class is the step past what is written down. + VtableSlot, + /// The function writes a known vtable pointer into what its first argument + /// points at, which is what a constructor and a destructor both do. + VptrStore, + /// The offsets and widths the code touched through the pointer. + Accesses, +} + +impl Basis { + /// The phrase it prints as. + pub fn as_str(self) -> &'static str { + match self { + Basis::Rtti => "RTTI", + Basis::MangledName => "mangled name", + Basis::RttiShape => "RTTI structure", + Basis::VtableSlot => "vtable slot", + Basis::VptrStore => "vtable pointer store", + Basis::Accesses => "accesses through the pointer", + } + } + + /// How strongly a fact resting on this is known. + pub fn strength(self) -> Strength { + match self { + // Written down by the compiler in a structure the ABI defines, and + // named by a symbol. Reading it back is not a guess. + Basis::Rtti | Basis::MangledName => Strength::Proven, + // Everything else is a conclusion drawn from what the code does. + Basis::RttiShape | Basis::VtableSlot | Basis::VptrStore | Basis::Accesses => { + Strength::Inferred + } + } + } +} + +/// Which of the three Itanium type-information classes an object is. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum TypeInfoKind { + /// `__class_type_info`: no bases. + Class, + /// `__si_class_type_info`: one public, non-virtual base at offset zero. + SingleInheritance, + /// `__vmi_class_type_info`: anything else, with an explicit base array. + MultipleInheritance, +} + +impl TypeInfoKind { + /// The ABI's name for it. + pub fn as_str(self) -> &'static str { + match self { + TypeInfoKind::Class => "__class_type_info", + TypeInfoKind::SingleInheritance => "__si_class_type_info", + TypeInfoKind::MultipleInheritance => "__vmi_class_type_info", + } + } +} + +/// One direct base class, as the type information records it. +#[derive(Debug, Clone)] +pub struct BaseClass { + /// The base's name, demangled. + pub name: String, + /// Where the base's type information lives. + pub typeinfo: Addr, + /// For an ordinary base, the byte offset of its subobject inside the + /// derived object. For a virtual base this is not that: the ABI stores the + /// offset into the virtual table at which the base's real offset is found, + /// because a virtual base moves depending on the complete object. + pub offset: i64, + /// True when the base is inherited virtually, and `offset` is a table + /// offset rather than an object offset. + pub is_virtual: bool, + /// True when the inheritance is public. + pub is_public: bool, + /// What this rests on. + pub basis: Basis, +} + +/// One place in an object that the code touched. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Field { + /// Byte offset from the start of the object. + pub offset: i64, + /// The widest access seen there. + pub size: u8, + /// True when anything wrote it. + pub written: bool, + /// What this rests on. + pub basis: Basis, +} + +/// The first argument of a member function, and what it points at. +#[derive(Debug, Clone)] +pub struct This { + /// The class it points at. + pub class: String, + /// How C++ spells the declaration. + pub declaration: String, + /// The offsets the function touched through it. + pub fields: Vec, + /// What the typing rests on. + pub basis: Basis, +} + +/// What a member function does for its class. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Role { + /// Builds an object: writes the class's vtable pointer into it and is in + /// no table itself. + Constructor, + /// Tears one down. + Destructor, + /// Tears one down and frees the storage. Under Itanium this is the second + /// of the two destructor slots a table carries. + DeletingDestructor, + /// A virtual function that is neither. + Virtual, + /// A thunk: the compiler's adjustment of `this` on the way to the real + /// function, which a secondary table's slots point at. + Thunk, + /// A member function that is in no table. + Method, +} + +impl Role { + /// The phrase it prints as. + pub fn as_str(self) -> &'static str { + match self { + Role::Constructor => "constructor", + Role::Destructor => "destructor", + Role::DeletingDestructor => "deleting destructor", + Role::Virtual => "virtual function", + Role::Thunk => "thunk", + Role::Method => "method", + } + } +} + +/// One function that belongs to a class. +#[derive(Debug, Clone)] +pub struct Member { + /// Where it starts. + pub addr: Addr, + /// The class it belongs to. + pub class: String, + /// Its name, demangled, when a symbol gave one. + pub name: Option, + /// What it does for the class. + pub role: Role, + /// Which table slot it occupies, when it occupies one. + pub slot: Option, + /// What the role rests on. + pub basis: Basis, + /// Its first argument, when the class is known well enough to type it. + pub this: Option, +} + +/// A class, and everything found about it. +#[derive(Debug, Clone)] +pub struct Class { + /// Its name, when anything named it. A stripped binary with no type + /// information has tables whose class nothing names, and inventing a name + /// for one would be a claim. + pub name: Option, + /// The mangled name the type information carries, which is what `c++filt` + /// is handed. + pub mangled: Option, + /// Where its type information lives. + pub typeinfo: Option, + /// Which of the three type-information classes it is. + pub kind: Option, + /// Its direct bases, in the order the type information lists them. + pub bases: Vec, + /// Its virtual tables, primary first. + pub vtables: Vec, + /// Its member functions. + pub members: Vec, + /// The offsets its member functions touch through `this`. + pub fields: Vec, + /// What the class itself rests on. + pub basis: Basis, +} + +impl Class { + /// A name for output: the class's own, or where its first table is. + pub fn display_name(&self) -> String { + match (&self.name, self.vtables.first()) { + (Some(name), _) => name.clone(), + (None, Some(at)) => format!("class with a vtable at {at}"), + (None, None) => "anonymous class".to_string(), + } + } + + /// Its constructors. + pub fn constructors(&self) -> impl Iterator { + self.members.iter().filter(|m| m.role == Role::Constructor) + } + + /// Its destructors, of both kinds. + pub fn destructors(&self) -> impl Iterator { + self.members + .iter() + .filter(|m| matches!(m.role, Role::Destructor | Role::DeletingDestructor)) + } + + /// How big the object is, as far as anything said: past the last field + /// touched, or past the last non-virtual base, whichever is further. + pub fn size(&self) -> Option { + let fields = self + .fields + .iter() + .filter(|f| f.offset >= 0) + .map(|f| f.offset as u64 + f.size as u64); + let bases = self + .bases + .iter() + .filter(|b| !b.is_virtual && b.offset >= 0) + .map(|b| b.offset as u64); + fields.chain(bases).max() + } +} + +/// Which C++ ABI the recovered classes are in. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Abi { + /// The Itanium C++ ABI: GCC, Clang, and everything but MSVC. + Itanium, + /// The Microsoft C++ ABI. + Msvc, + /// Nothing said, because nothing was found. + Unknown, +} + +impl Abi { + /// The phrase it prints as. + pub fn as_str(self) -> &'static str { + match self { + Abi::Itanium => "Itanium", + Abi::Msvc => "MSVC", + Abi::Unknown => "unknown", + } + } +} + +/// Whether the binary carries type information at all. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Rtti { + /// Type information was found and read, so the hierarchy is the compiler's + /// own record of it. + Present, + /// There are virtual tables but none of them points at type information: + /// the binary was built with `-fno-rtti`. What comes back is the + /// vtable-only answer, with no bases and no inheritance offsets, because + /// there is nothing in the file that says what they are. + Absent, + /// No virtual tables, so nothing to say. + NoClasses, +} + +impl Rtti { + /// The phrase it prints as. + pub fn as_str(self) -> &'static str { + match self { + Rtti::Present => "present", + Rtti::Absent => "absent (-fno-rtti): vtables only, no hierarchy", + Rtti::NoClasses => "no classes", + } + } +} + +/// Every class the program appears to contain, with the counts that say how +/// much of the answer came from where. +#[derive(Debug, Clone)] +pub struct Classes { + /// The classes, by name where they have one and by address where they do + /// not. + pub classes: Vec, + /// Whether type information was there to read. + pub rtti: Rtti, + /// Which ABI the type information is in. + pub abi: Abi, + /// How many virtual tables were found. + pub tables: usize, + /// How many of them a `_ZTV` symbol named. + pub named_by_symbol: usize, + /// How many of them only the type information named, which is what RTTI + /// buys over the tables alone. + pub named_by_rtti: usize, +} + +/// What to spend time on. +#[derive(Debug, Clone)] +pub struct Options { + /// Identify constructors and destructors by what they write rather than + /// only by their names, so a stripped binary still gets them. This builds + /// SSA for every function that mentions a virtual table. + pub stores: bool, + /// Recover what each member function touches through `this`. This builds + /// SSA for every member function, which is the expensive part. + pub fields: bool, +} + +impl Default for Options { + fn default() -> Options { + Options { + stores: true, + fields: true, + } + } +} + +/// Every class the program appears to contain. +pub fn classes(p: &Program) -> Classes { + classes_with(p, &Options::default()) +} + +/// Every class the program appears to contain, doing only the work asked for. +pub fn classes_with(p: &Program, opts: &Options) -> Classes { + let tables = vtables(p); + let abi = abi_type_info_vtables(p); + + // Every type-information object anything points at, plus every one a + // symbol names outright. A class with no virtual functions has no table, + // but if it is a base of one that has, its type information is still here. + let mut candidates: BTreeSet = tables.iter().filter_map(|t| t.typeinfo).collect(); + for s in &p.object.symbols { + if s.name.starts_with("_ZTI") && s.addr != Addr::ZERO { + candidates.insert(s.addr); + } + } + // Bases pull in type information nothing else pointed at, so keep reading + // until the set stops growing. + let mut infos: BTreeMap = BTreeMap::new(); + let mut queue: Vec = candidates.iter().copied().collect(); + while let Some(at) = queue.pop() { + if infos.contains_key(&at) { + continue; + } + let Some(info) = read_type_info(p, &abi, at) else { + continue; + }; + for b in &info.bases { + if !infos.contains_key(&b.typeinfo) { + queue.push(b.typeinfo); + } + } + infos.insert(at, info); + } + + // The Microsoft layout, where the Itanium one found nothing. A PE has its + // type information in a different shape and reached through relative + // addresses, and a binary is one or the other, never both. + let mut abi = if infos.is_empty() { + Abi::Unknown + } else { + Abi::Itanium + }; + let mut tables = tables; + if infos.is_empty() { + let found = crate::msvc::read(p); + if !found.infos.is_empty() { + abi = Abi::Msvc; + infos = found.infos; + let known: BTreeSet = tables.iter().map(|t| t.addr.get()).collect(); + tables.extend( + found + .tables + .into_iter() + .filter(|t| !known.contains(&t.addr.get())), + ); + tables.sort_by_key(|t| t.addr); + tables.dedup_by_key(|t| t.addr); + } + } + + let rtti = if !infos.is_empty() { + Rtti::Present + } else if tables.is_empty() { + Rtti::NoClasses + } else { + Rtti::Absent + }; + + // Type information finds tables the scan cannot. A sub-table with two + // slots is under the length a run of pointers has to reach before it is a + // table by itself, and a class that overrides nothing but its destructor + // has exactly that; so does every secondary sub-table of a class with + // virtual bases. A pair of words followed by a function, where the second + // word is type information this program defines, is a table whatever its + // length, because nothing else is shaped that way. + let known: BTreeSet = tables.iter().map(|t| t.addr.get()).collect(); + for t in tables_from_type_info(p, &infos) { + if !known.contains(&t.addr.get()) { + tables.push(t); + } + } + tables.sort_by_key(|t| t.addr); + tables.dedup_by_key(|t| t.addr); + + let mut built = build_classes(&tables, &infos); + attribute_members(p, opts, &tables, &mut built); + + let named_by_symbol = tables.iter().filter(|t| t.class.is_some()).count(); + let named_by_rtti = tables + .iter() + .filter(|t| t.class.is_none()) + .filter(|t| t.typeinfo.and_then(|a| infos.get(&a)).is_some()) + .count(); + + Classes { + classes: built, + rtti, + abi, + tables: tables.len(), + named_by_symbol, + named_by_rtti, + } +} + +/// Every virtual table whose type-information word names a class this program +/// defines. +/// +/// The scan in [`crate::vtables`] needs a run of function pointers long enough +/// to be a table rather than a coincidence. This needs no length at all, +/// because the type information is the evidence: two words followed by one +/// function, where the second word points at type information already read, is +/// a table. +fn tables_from_type_info(p: &Program, infos: &BTreeMap) -> Vec { + let w = word_size(p) as i64; + let mut out = Vec::new(); + for section in &p.object.sections { + // The sections a compiler puts tables in, which is where the scan + // looks too. + if !(section.name.starts_with(".data.rel.ro") + || section.name == ".rodata" + || section.name == ".data") + || section.range.is_empty() + { + continue; + } + let mut at = section.range.start(); + while at.get() + 3 * w as u64 <= section.range.end().get() { + let step = at; + at = at.wrapping_offset(w); + let Some(offset_to_top) = word(p, step) else { + continue; + }; + let Some(typeinfo) = word(p, step.wrapping_offset(w)) else { + continue; + }; + if !infos.contains_key(&Addr(typeinfo)) { + continue; + } + let entry = step.wrapping_offset(2 * w); + let mut methods = Vec::new(); + let mut slot = entry; + while let Some(target) = word(p, slot) { + if !is_code(p, Addr(target)) { + break; + } + methods.push(Addr(target)); + slot = slot.wrapping_offset(w); + } + if methods.is_empty() { + continue; + } + at = slot; + out.push(VTable { + addr: step, + entry, + class: None, + offset_to_top: offset_to_top as i64, + typeinfo: Some(Addr(typeinfo)), + methods, + // The tables `vtables` found say where they came from. These + // rest on the type information, which is not a kind the core + // evidence list has a name for yet; a pointer in data is the + // nearest true thing it can say. + evidence: e5r_core::Evidence::DataPointer, + }); + } + } + out +} + +/// True when an address is inside something the container marked executable. +pub(crate) fn is_code(p: &Program, at: Addr) -> bool { + at != Addr::ZERO + && p.object + .sections + .iter() + .any(|s| s.exec && s.range.contains(at)) +} + +/// One type-information object, read. +#[derive(Debug, Clone)] +pub(crate) struct TypeInfo { + pub(crate) name: String, + pub(crate) mangled: String, + /// Which of the three Itanium type-information classes it is. The + /// Microsoft layout has no such division, and leaves this empty. + pub(crate) kind: Option, + pub(crate) bases: Vec, + pub(crate) basis: Basis, +} + +/// Where the ABI's three type-information class vtables are, when the symbols +/// for them are in this file. +/// +/// A type-information object's first word points sixteen bytes into one of +/// them, which is what says which of the three it is. These are the names the +/// Itanium ABI gives them, and they are what `libstdc++` exports. +fn abi_type_info_vtables(p: &Program) -> BTreeMap { + let w = word_size(p); + let mut out = BTreeMap::new(); + for s in &p.object.symbols { + let kind = match s.name.as_str() { + "_ZTVN10__cxxabiv117__class_type_infoE" => TypeInfoKind::Class, + "_ZTVN10__cxxabiv120__si_class_type_infoE" => TypeInfoKind::SingleInheritance, + "_ZTVN10__cxxabiv121__vmi_class_type_infoE" => TypeInfoKind::MultipleInheritance, + _ => continue, + }; + if s.addr != Addr::ZERO { + out.insert(s.addr.get().wrapping_add(2 * w), kind); + } + } + out +} + +/// Read one type-information object. +fn read_type_info(p: &Program, abi: &BTreeMap, at: Addr) -> Option { + let w = word_size(p); + let vptr = word(p, at)?; + let name_at = Addr(word(p, at.wrapping_offset(w as i64))?); + let mangled = mangled_name(p, name_at)?; + let name = demangle_type_name(&mangled)?; + + // Which of the three it is. The ABI's own symbols settle it outright; with + // them stripped, the structure has to. + let (kind, basis) = match abi.get(&vptr) { + Some(kind) => (*kind, Basis::Rtti), + None => (kind_by_shape(p, at)?, Basis::RttiShape), + }; + + let mut bases = Vec::new(); + match kind { + TypeInfoKind::Class => {} + TypeInfoKind::SingleInheritance => { + // One public base at offset zero, and the ABI stores nothing else + // because there is nothing else to store. + let base = Addr(word(p, at.wrapping_offset(2 * w as i64))?); + bases.push(BaseClass { + name: base_name(p, base)?, + typeinfo: base, + offset: 0, + is_virtual: false, + is_public: true, + basis, + }); + } + TypeInfoKind::MultipleInheritance => { + // `__flags` then `__base_count`, both 32 bits, then one + // `__base_class_type_info` per direct base. + let count = read_int(p, at.wrapping_offset(2 * w as i64 + 4), 4)? as u32; + // An entry is a pointer and a word of packed offset and flags, so + // a count that does not fit in the file is not a count. + if count == 0 || count > 4096 { + return None; + } + let array = at.wrapping_offset(2 * w as i64 + 8); + for i in 0..count as i64 { + let entry = array.wrapping_offset(i * 2 * w as i64); + let base = Addr(word(p, entry)?); + let flags = word(p, entry.wrapping_offset(w as i64))? as i64; + bases.push(BaseClass { + name: base_name(p, base)?, + typeinfo: base, + // The low byte is the flags; the rest is a signed offset. + offset: flags >> 8, + is_virtual: flags & 1 != 0, + is_public: flags & 2 != 0, + basis, + }); + } + } + } + + Some(TypeInfo { + name, + mangled, + kind: Some(kind), + bases, + basis, + }) +} + +/// Which type-information class an object is, decided by its shape. +/// +/// Used only when the ABI's own vtable symbols are not in the file. The three +/// layouts are distinguishable: a single-inheritance object's third word +/// points at another type-information object, a many-bases object's third word +/// is a pair of packed 32-bit counts followed by that many base entries, and +/// anything else has no bases at all. +fn kind_by_shape(p: &Program, at: Addr) -> Option { + let w = word_size(p); + let third = at.wrapping_offset(2 * w as i64); + if let Some(v) = word(p, third) + && looks_like_type_info(p, Addr(v)) + { + return Some(TypeInfoKind::SingleInheritance); + } + // A plausible base array: a small count, and every entry pointing at + // something that is itself type information. + if let Some(raw) = read_int(p, third.wrapping_offset(4), 4) { + let count = raw as u32; + if count > 0 && count <= 64 { + let array = third.wrapping_offset(8); + let ok = (0..count as i64).all(|i| { + word(p, array.wrapping_offset(i * 2 * w as i64)) + .is_some_and(|v| looks_like_type_info(p, Addr(v))) + }); + if ok { + return Some(TypeInfoKind::MultipleInheritance); + } + } + } + Some(TypeInfoKind::Class) +} + +/// Whether an address holds something shaped like type information. +/// +/// A base entry points at another type-information object, and in a binary +/// with no symbols there is nothing to check that against except the layout +/// itself: the second word has to point at a mangled name the demangler +/// accepts. Two words that happen to do that by accident is the false +/// positive this admits, and it has not been seen. +fn looks_like_type_info(p: &Program, at: Addr) -> bool { + if at == Addr::ZERO { + return false; + } + let Some(w) = word(p, at.wrapping_offset(word_size(p) as i64)) else { + return false; + }; + mangled_name(p, Addr(w)) + .as_deref() + .and_then(demangle_type_name) + .is_some() +} + +/// The mangled type name a type-information object points at. +fn mangled_name(p: &Program, at: Addr) -> Option { + if at == Addr::ZERO { + return None; + } + let window = p.object.memory.decode_window(at, 1024)?; + let end = window.iter().position(|b| *b == 0)?; + let text = window.get(..end)?; + // A mangled name is ASCII and never empty. Anything else means the word + // was not a name pointer and this is not type information. + if text.is_empty() || !text.iter().all(|b| b.is_ascii_graphic()) { + return None; + } + String::from_utf8(text.to_vec()).ok() +} + +/// A class name from the mangled type name the ABI stores. +/// +/// The stored name is the class's own mangled name with no prefix, so putting +/// the type-information prefix back on it gives the demangler something it +/// recognizes, nested names and templates included. +fn demangle_type_name(mangled: &str) -> Option { + let (_, text) = e5r_types::demangle(&format!("_ZTI{mangled}"))?; + Some(text.strip_prefix("typeinfo for ")?.to_string()) +} + +/// The name of the class a base entry points at. +fn base_name(p: &Program, at: Addr) -> Option { + let w = word_size(p); + let name_at = Addr(word(p, at.wrapping_offset(w as i64))?); + demangle_type_name(&mangled_name(p, name_at)?) +} + +pub(crate) fn word_size(p: &Program) -> u64 { + match p.object.bits { + Bits::Bits64 => 8, + _ => 4, + } +} + +pub(crate) fn word(p: &Program, at: Addr) -> Option { + read_int(p, at, word_size(p)) +} + +pub(crate) fn read_int(p: &Program, at: Addr, bytes: u64) -> Option { + p.object + .memory + .read_ptr(at, bytes, p.object.endian == Endian::Little) + .ok() +} + +/// Turn the tables and the type information into classes. +fn build_classes(tables: &[VTable], infos: &BTreeMap) -> Vec { + // A class is keyed by its name when anything named it, and by the address + // of its first table when nothing did. + let mut by_key: BTreeMap = BTreeMap::new(); + let mut out: Vec = Vec::new(); + + let mut place = |key: String, seed: Class, out: &mut Vec| -> usize { + match by_key.get(&key) { + Some(i) => *i, + None => { + by_key.insert(key, out.len()); + out.push(seed); + out.len() - 1 + } + } + }; + + // Type information first: it is the strongest thing here, and it exists + // for classes that have no table of their own. + let mut info_index: BTreeMap = BTreeMap::new(); + for (at, info) in infos { + let i = place( + info.name.clone(), + Class { + name: Some(info.name.clone()), + mangled: Some(info.mangled.clone()), + typeinfo: Some(*at), + kind: info.kind, + bases: info.bases.clone(), + vtables: Vec::new(), + members: Vec::new(), + fields: Vec::new(), + basis: info.basis, + }, + &mut out, + ); + info_index.insert(*at, i); + } + + for t in tables { + // The type information names the class outright. Failing that, a + // `_ZTV` symbol does. Failing both, the table is all there is. + let i = match t.typeinfo.and_then(|a| info_index.get(&a)) { + Some(i) => *i, + None => { + let key = t + .class + .clone() + .unwrap_or_else(|| format!("@{:#x}", t.addr.get())); + let seed = Class { + name: t.class.clone(), + mangled: None, + typeinfo: t.typeinfo, + kind: None, + bases: Vec::new(), + vtables: Vec::new(), + members: Vec::new(), + fields: Vec::new(), + basis: if t.class.is_some() { + Basis::MangledName + } else { + Basis::VtableSlot + }, + }; + place(key, seed, &mut out) + } + }; + out[i].vtables.push(t.addr); + } + + // The primary table first: it is the one at offset zero from the top of + // the object, and the one whose slots take a `this` of this class. + for c in &mut out { + c.vtables.sort(); + } + out.sort_by(|a, b| { + a.name + .cmp(&b.name) + .then(a.vtables.first().cmp(&b.vtables.first())) + }); + out +} + +/// Find the member functions of each class and say what each one does. +fn attribute_members(p: &Program, opts: &Options, tables: &[VTable], classes: &mut [Class]) { + let mut index: BTreeMap = BTreeMap::new(); + for (i, c) in classes.iter().enumerate() { + if let Some(name) = &c.name { + index.insert(name.clone(), i); + } + } + // Which class each table belongs to, and which table is its primary. + let mut table_of: BTreeMap = BTreeMap::new(); + for (i, c) in classes.iter().enumerate() { + for t in &c.vtables { + table_of.insert(t.get(), i); + } + } + // Every address a vtable pointer can hold, the class it belongs to, and + // how far into that class's objects the subobject it serves begins. The + // second number is what tells a base's pointer from a member's. + let mut vptr_class: BTreeMap = BTreeMap::new(); + // Every function any table reaches. A constructor is never one of these, + // because a table holds virtual functions and a constructor is not one. + let mut dispatched: BTreeSet = BTreeSet::new(); + for t in tables { + if let Some(i) = table_of.get(&t.addr.get()) { + vptr_class.insert(t.entry.get(), (*i, -t.offset_to_top)); + } + dispatched.extend(t.methods.iter().map(|m| m.get())); + } + + // What each function is, best evidence first. A later weaker claim never + // overwrites an earlier stronger one. + let mut found: BTreeMap = BTreeMap::new(); + + // 1. The symbols. A mangled member-function name says the class and says + // whether the function is a constructor or a destructor, and which of the + // several the ABI emits per class it is. + for s in &p.object.symbols { + if !s.is_defined_function() || s.addr == Addr::ZERO { + continue; + } + let Some(m) = member_from_symbol(&s.name, s.addr) else { + continue; + }; + if !index.contains_key(&m.class) { + continue; + } + found.insert(s.addr.get(), m); + } + + // 2. The tables. A slot of a primary table is a virtual function of that + // class, and the first two slots of a class with a virtual destructor are + // the complete and the deleting destructor, in that order. + for t in tables { + let Some(i) = table_of.get(&t.addr.get()) else { + continue; + }; + let Some(name) = classes[*i].name.clone() else { + continue; + }; + // Only the primary table types a plain `this`: a secondary table's + // slots are entered with a pointer to the subobject, not to the + // complete object, and calling that a `this` of the class would be + // wrong by exactly the offset to the top. + let primary = t.offset_to_top == 0; + for (slot, target) in t.methods.iter().enumerate() { + if *target == Addr::ZERO { + continue; + } + if let Some(m) = found.get_mut(&target.get()) { + // Already known from a name, which outranks a slot. Record + // where it sits, which the name does not say. + if m.slot.is_none() && primary { + m.slot = Some(slot); + } + continue; + } + if !primary { + continue; + } + found.insert( + target.get(), + Member { + addr: *target, + class: name.clone(), + name: p.name_of(*target).map(|n| e5r_types::pretty(&n)), + role: Role::Virtual, + slot: Some(slot), + basis: Basis::VtableSlot, + this: None, + }, + ); + } + } + + // 3. What the code does. A function that writes a class's vtable pointer + // into what its first argument points at is building or tearing down an + // object of that class. + // + // Which store counts is the whole question, and the answer is the one the + // ABI states: offset zero of the object, holding a table whose own + // subobject begins at offset zero, which is the complete object's own + // pointer. A store anywhere else belongs to a subobject, and a subobject + // is either a base this function is not a member of or a member variable + // of another class entirely: `~basic_ofstream` sets its `basic_filebuf` + // member's primary pointer eight bytes in, and eighteen functions in + // `libstdc++` came back a constructor of the wrong class because a store + // like that was read as a claim about the function. + let descends = descendants(classes); + if opts.stores { + for (at, writes) in vptr_writers(p, tables) { + // In the order the code performs them, which is what separates + // building from tearing down when more than one is written. + let mut built: Vec = writes + .iter() + .filter(|(offset, _)| *offset == 0) + .filter_map(|(_, table)| { + let (i, begins) = vptr_class.get(&table.get()).copied()?; + (begins == 0).then_some(i) + }) + .collect(); + built.dedup(); + // The complete object's class: the one of those that is not a base + // of any other. A constructor writes its bases' pointers before + // its own and a destructor writes them after, so taking the most + // derived rather than the first or the last is right either way. + let Some(i) = built + .iter() + .find(|i| !built.iter().any(|j| descends(*j, **i))) + .copied() + else { + continue; + }; + let Some(name) = classes[i].name.clone() else { + continue; + }; + let slot = found.get(&at.get()).and_then(|m| m.slot); + // Which of the two it is. A function that writes the pointer of + // this class and of classes it inherits from has its base + // constructors or base destructors inlined into it, and the order + // settles it outright: an object is built from the base up and torn + // down from the derived end, so the complete object's own pointer + // goes in last when building and first when tearing down. The + // others have to be its bases for that to be the reading; two + // unrelated classes written into the same object are a lattice this + // does not understand, and it says nothing rather than guessing. + // + // Otherwise the tables decide: a constructor is never reached + // through one, and a destructor of a class with a virtual + // destructor is, at the first of the two slots the class spends on + // destruction. What that cannot tell apart is a base-object + // destructor of a class whose bases are not inlined: it writes the + // pointer exactly as a constructor does and is in no table, so it + // comes back a constructor. + let inlined_bases = built.len() > 1 && built.iter().all(|j| *j == i || descends(i, *j)); + let tearing_down = if inlined_bases { + built.first() == Some(&i) + } else { + dispatched.contains(&at.get()) + }; + let role = if !tearing_down { + Role::Constructor + } else { + match slot { + Some(1) => Role::DeletingDestructor, + _ => Role::Destructor, + } + }; + match found.get_mut(&at.get()) { + // A name already said what this is; the store corroborates it + // and does not get to contradict it. + Some(m) if m.basis.strength() >= Strength::Proven => {} + Some(m) => { + m.role = role; + m.basis = Basis::VptrStore; + } + None => { + found.insert( + at.get(), + Member { + addr: at, + class: name, + name: p.name_of(at).map(|n| e5r_types::pretty(&n)), + role, + slot, + basis: Basis::VptrStore, + this: None, + }, + ); + } + } + } + } + + // 4. The second destructor slot. Under Itanium a class with a virtual + // destructor spends two adjacent slots on it: the complete-object one + // first and the deleting one second. The deleting one frees the storage + // and usually writes no vtable pointer of its own, so nothing above + // reaches it; but once the slot before it is known to be a destructor, + // the ABI says what the slot after it is. + for t in tables.iter().filter(|t| t.offset_to_top == 0) { + let (Some(first), Some(second)) = (t.methods.first(), t.methods.get(1)) else { + continue; + }; + let complete = found + .get(&first.get()) + .is_some_and(|m| m.role == Role::Destructor); + if !complete || *second == Addr::ZERO || second == first { + continue; + } + match found.get_mut(&second.get()) { + Some(m) if m.basis.strength() >= Strength::Proven => {} + Some(m) => { + m.role = Role::DeletingDestructor; + m.basis = Basis::VtableSlot; + } + None => {} + } + } + + // 5. `this`. The first argument of every one of these is the object, by + // the ABI on both System V and MSVC, so once the class is known the + // argument is typed and the offsets the code touches through it are the + // class's fields. + let mut members: Vec = found.into_values().collect(); + for m in &mut members { + let fields = if opts.fields { + this_fields(p, m.addr) + } else { + Vec::new() + }; + m.this = Some(This { + class: m.class.clone(), + declaration: format!("{} *this", m.class), + fields, + // The class came from a name, or from the table the function is + // in. The typing is never stronger than that. + basis: match m.basis { + Basis::MangledName => Basis::MangledName, + other => other, + }, + }); + } + + for m in members { + let Some(i) = index.get(&m.class) else { + continue; + }; + classes[*i].members.push(m); + } + for c in classes.iter_mut() { + c.members.sort_by_key(|m| m.addr); + c.fields = merge_fields(&c.members); + } +} + +/// Which classes each one inherits from, transitively, by index. +/// +/// The answer says "a descends from b", which is what decides the complete +/// object's class where a function writes several vtable pointers into the +/// same object: the one that is nobody else's base is the one being built or +/// destroyed. A hierarchy read out of a hostile file can name a cycle, so the +/// walk carries the set it has already seen. +fn descendants(classes: &[Class]) -> impl Fn(usize, usize) -> bool + use<> { + let index: BTreeMap<&str, usize> = classes + .iter() + .enumerate() + .filter_map(|(i, c)| c.name.as_deref().map(|n| (n, i))) + .collect(); + let mut ancestors: BTreeMap> = BTreeMap::new(); + for i in 0..classes.len() { + let mut seen = BTreeSet::new(); + let mut queue = vec![i]; + while let Some(at) = queue.pop() { + for b in &classes[at].bases { + let Some(j) = index.get(b.name.as_str()).copied() else { + continue; + }; + if seen.insert(j) { + queue.push(j); + } + } + } + ancestors.insert(i, seen); + } + move |a, b| ancestors.get(&a).is_some_and(|set| set.contains(&b)) +} + +/// Union the fields every member saw, widest access per offset. +fn merge_fields(members: &[Member]) -> Vec { + let mut widest: BTreeMap = BTreeMap::new(); + for f in members + .iter() + .filter_map(|m| m.this.as_ref()) + .flat_map(|t| t.fields.iter()) + { + let e = widest.entry(f.offset).or_insert(*f); + e.size = e.size.max(f.size); + e.written |= f.written; + } + widest.into_values().collect() +} + +/// Read a member function's name and role out of a mangled symbol. +/// +/// The demangled form says which class it belongs to and whether it is a +/// constructor or a destructor: `A::B::B()` builds, `A::B::~B()` tears down. +/// Which of the several the ABI emits per class it is comes from the mangled +/// form, because all three demangle to the same text. +fn member_from_symbol(symbol: &str, addr: Addr) -> Option { + let (e5r_types::Scheme::Itanium, text) = e5r_types::demangle(symbol)? else { + return None; + }; + // A thunk adjusts `this` and jumps; it is not the member function, and + // typing its argument as the complete object would be wrong by the + // adjustment. + if text.starts_with("non-virtual thunk to ") || text.starts_with("virtual thunk to ") { + return Some(Member { + addr, + class: qualified(text.rsplit(" to ").next()?)?.0, + name: Some(text.clone()), + role: Role::Thunk, + slot: None, + basis: Basis::MangledName, + this: None, + }); + } + let (class, last) = qualified(&text)?; + // The class's own name, without what encloses it and without its template + // arguments: `std::__cxx11::basic_string` is written + // `basic_string` where it declares its own constructor. Splitting on `::` + // has to skip the ones inside the argument list, or the name of a class + // whose arguments are themselves qualified comes out as an argument. + let short = simple_name(&class); + let role = if last == format!("~{short}") { + // `D0` is the deleting destructor, `D1` the complete one and `D2` the + // base one. Only the first frees the storage. + if symbol.contains("D0E") { + Role::DeletingDestructor + } else { + Role::Destructor + } + } else if last == short { + Role::Constructor + } else { + Role::Method + }; + Some(Member { + addr, + class, + name: Some(text), + role, + slot: None, + basis: Basis::MangledName, + this: None, + }) +} + +/// Split a demangled name into the class it is in and its last component. +fn qualified(text: &str) -> Option<(String, String)> { + // Cut the parameter list and anything after it, then split on the last + // `::` that is not inside a template argument list. + let name = before_parameters(text); + let mut depth = 0i32; + let bytes: Vec = name.chars().collect(); + let mut cut = None; + for i in 0..bytes.len() { + match bytes[i] { + '<' | '(' => depth += 1, + '>' | ')' => depth -= 1, + ':' if depth == 0 && i + 1 < bytes.len() && bytes[i + 1] == ':' => cut = Some(i), + _ => {} + } + } + let cut = cut?; + let class: String = bytes[..cut].iter().collect(); + let last: String = bytes[cut + 2..].iter().collect(); + (!class.is_empty() && !last.is_empty()).then_some((class, last)) +} + +/// A class's own name: the last component of a qualified name, with any +/// template argument list taken off. +fn simple_name(class: &str) -> String { + let chars: Vec = class.chars().collect(); + let mut depth = 0i32; + let mut start = 0usize; + let mut template = chars.len(); + for i in 0..chars.len() { + match chars[i] { + '<' | '(' => { + if depth == 0 && template == chars.len() { + template = i; + } + depth += 1; + } + '>' | ')' => depth -= 1, + ':' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == ':' => { + start = i + 2; + // A template list before this `::` belonged to an enclosing + // class, not to this one. + template = chars.len(); + } + _ => {} + } + } + chars[start..template.max(start)].iter().collect() +} + +/// The name part of a demangled function, without its parameters. +fn before_parameters(text: &str) -> &str { + let mut depth = 0i32; + for (i, c) in text.char_indices() { + match c { + '<' => depth += 1, + '>' => depth -= 1, + '(' if depth == 0 => return text[..i].trim_end(), + _ => {} + } + } + text +} + +/// Every function that writes a known vtable pointer through its first +/// argument, and which pointers it writes where. +/// +/// Each write is the offset into the object and the table whose address went +/// there, in the order the instructions appear. The order is what tells a +/// constructor with its bases inlined from a destructor with the same, so it +/// is not sorted away. +/// +/// The prefilter is what makes this affordable: only functions that mention a +/// virtual table at all are lifted, which over a real library is a small +/// fraction of them. +fn vptr_writers(p: &Program, tables: &[VTable]) -> BTreeMap> { + let entries: BTreeSet = tables.iter().map(|t| t.entry.get()).collect(); + // The byte range each table covers, so a reference to any part of one + // counts as mentioning it. + let mut ranges: Vec<(u64, u64)> = tables + .iter() + .map(|t| (t.addr.get(), t.entry.get() + t.methods.len() as u64 * 8)) + .collect(); + ranges.sort_unstable(); + + // Function hulls, sorted, so mapping a reference to its function does not + // walk every function in the program. + let mut hulls: Vec<(u64, u64, Addr)> = p + .functions_by_address() + .map(|f| (f.range.start().get(), f.range.end().get(), f.entry)) + .collect(); + hulls.sort_unstable(); + + let mut candidates: BTreeSet = BTreeSet::new(); + for x in p.xrefs.all() { + let to = x.to.get(); + // The last table that starts at or before the reference is the only + // one that can contain it, because tables do not overlap. A shared + // library does not name its own tables directly: it reaches them + // through a global offset table slot, so a reference to a word that + // holds a table's address counts as a mention of the table. + let inside = |v: u64| { + let i = ranges.partition_point(|(start, _)| *start <= v); + i > 0 && v < ranges[i - 1].1 + }; + if !inside(to) && !word(p, x.to).is_some_and(inside) { + continue; + } + let from = x.from.get(); + // The last hull that starts at or before the reference. + let i = hulls.partition_point(|(start, _, _)| *start <= from); + for (start, end, entry) in hulls[..i].iter().rev().take(8) { + if from >= *start && from < *end { + candidates.insert(*entry); + break; + } + } + } + + let mut out: BTreeMap> = BTreeMap::new(); + for at in candidates { + let Some(f) = p.function(at) else { continue }; + let Some(ssa) = ssa_of(p, f) else { continue }; + let abi = e5r_ir::abi::of(&p.object.arch); + let Some(this) = abi.integer_arguments.first().copied() else { + continue; + }; + let defs = ssa.definitions(); + let mut writes = Vec::new(); + for b in ssa.blocks.values() { + for op in &b.ops { + if op.kind != SsaKind::Op(Op::Store) { + continue; + } + let (Some(address), Some(value)) = (op.inputs.first(), op.inputs.get(1)) else { + continue; + }; + let Some(v) = constant(p, &ssa, &defs, value, 0) else { + continue; + }; + if !entries.contains(&v) { + continue; + } + // Through the first argument, which is what a member function + // is handed a pointer to the object in. + let Some((base, offset)) = based_on(p, &ssa, &defs, address, 0) else { + continue; + }; + if base.space == Space::Register && base.offset == this { + writes.push((op.addr, offset, Addr(v))); + } + } + } + if !writes.is_empty() { + // By the address of the instruction that wrote it, so the order is + // the one the code is written in rather than the one the blocks + // happened to be walked in. A pointer written twice counts once, + // at the first place it went in. + writes.sort(); + let mut seen = BTreeSet::new(); + out.insert( + at, + writes + .into_iter() + .filter(|(_, offset, v)| seen.insert((*offset, *v))) + .map(|(_, offset, v)| (offset, v)) + .collect(), + ); + } + } + out +} + +/// What a member function touched through its first argument. +fn this_fields(p: &Program, at: Addr) -> Vec { + let Some(f) = p.function(at) else { + return Vec::new(); + }; + crate::shapes::shapes_of(p, f) + .into_iter() + .filter(|s| s.argument == Some(0)) + .flat_map(|s| { + let written = s.written; + s.fields.into_iter().map(move |(offset, size)| Field { + offset, + size, + written, + basis: Basis::Accesses, + }) + }) + .collect() +} + +/// Lift a function to optimized SSA, the way every other pass here does. +fn ssa_of(p: &Program, f: &e5r_analysis::Function) -> Option { + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + if blocks.is_empty() { + return None; + } + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut ssa = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut ssa); + Some(ssa) +} + +/// The literal an operand holds, following copies and folding an addition of +/// two literals, which is how an address is built on AArch64. +/// +/// A load from a fixed address counts, because a shared library reaches its own +/// virtual tables through the global offset table rather than as an immediate: +/// `adrp` then `ldr` of a slot the loader filled in. Reading that slot back is +/// reading what the loader will put there, which is why the relocations have to +/// have been applied first. +fn constant( + p: &Program, + f: &SsaFunction, + defs: &BTreeMap, + operand: &Operand, + depth: u32, +) -> Option { + if depth > 16 { + return None; + } + if let Some(v) = operand.as_const() { + return Some(v); + } + let op = defining(f, defs, operand)?; + if op.kind == SsaKind::Op(Op::Load) && op.size as u64 == word_size(p) { + let at = constant(p, f, defs, op.inputs.first()?, depth + 1)?; + return word(p, Addr(at)); + } + match op.kind { + SsaKind::Op(Op::Copy) => constant(p, f, defs, op.inputs.first()?, depth + 1), + SsaKind::Op(Op::IntAdd) => { + let a = constant(p, f, defs, op.inputs.first()?, depth + 1)?; + let b = constant(p, f, defs, op.inputs.get(1)?, depth + 1)?; + Some(a.wrapping_add(b)) + } + SsaKind::Op(Op::IntOr) => { + let a = constant(p, f, defs, op.inputs.first()?, depth + 1)?; + let b = constant(p, f, defs, op.inputs.get(1)?, depth + 1)?; + Some(a | b) + } + _ => None, + } +} + +/// Unwind an address to the incoming value it is measured from, plus a +/// constant offset. +fn based_on( + p: &Program, + f: &SsaFunction, + defs: &BTreeMap, + operand: &Operand, + depth: u32, +) -> Option<(Location, i64)> { + if depth > 16 { + return None; + } + if let Operand::Undefined(l) = operand { + return Some((*l, 0)); + } + let op = defining(f, defs, operand)?; + match op.kind { + SsaKind::Op(Op::Copy) => based_on(p, f, defs, op.inputs.first()?, depth + 1), + SsaKind::Op(Op::IntAdd) | SsaKind::Op(Op::IntSub) => { + let sign = if op.kind == SsaKind::Op(Op::IntSub) { + -1 + } else { + 1 + }; + for (a, b) in [(0, 1), (1, 0)] { + if let Some(k) = constant(p, f, defs, op.inputs.get(b)?, depth + 1) + && let Some((base, offset)) = based_on(p, f, defs, op.inputs.get(a)?, depth + 1) + { + return Some((base, offset.wrapping_add(sign * k as i64))); + } + } + None + } + _ => None, + } +} + +fn defining<'a>( + f: &'a SsaFunction, + defs: &BTreeMap, + operand: &Operand, +) -> Option<&'a e5r_ir::ssa::SsaOp> { + let v = operand.as_value()?; + let (block, index) = defs.get(&v)?; + f.blocks.get(block)?.ops.get(*index) +} diff --git a/crates/e5r-api/src/dataflow.rs b/crates/e5r-api/src/dataflow.rs new file mode 100644 index 0000000..683cd43 --- /dev/null +++ b/crates/e5r-api/src/dataflow.rs @@ -0,0 +1,1419 @@ +//! What a call hands to what it calls, and how well each argument is known. +//! +//! "Find every call to `memcpy` whose third argument is not bounded by a +//! constant" is the question this exists to answer, and the whole value of the +//! answer is in the difference between two ways of failing to find a bound. +//! Either the code constrains the value nowhere between reading it and passing +//! it, which is a claim about the program, or the analysis could not follow it, +//! which is a claim about the analysis. Printing the first when the second is +//! what happened would make every row untrustworthy, so they are separate +//! verdicts and the row always says which one it is. +//! +//! Nothing here re-derives dataflow. SSA, the interval domain, stack promotion +//! and the ABI model already exist; this walks the reaching definition of each +//! argument register at each call site and reports what those passes already +//! know, plus one thing they do not do: the bound a comparison that guards the +//! call site puts on the value. +//! +//! Three limits are deliberate and are visible in the output rather than +//! hidden. +//! +//! * A bound that comes only from the width the value was read at is not a +//! bound the code established. It is reported as `width only` with the width +//! in the bound column, and the verdict stays `unknown`. +//! * A signed comparison bounds a signed value. A length compared as signed +//! and used as unsigned is the classic bug, so a guard that leaves the low +//! end at the signed minimum bounds nothing here. +//! * `unconstrained` is a claim about the code, under the assumption that +//! memory can hold anything the code did not put there. It is only made when +//! the value arrives from a memory read at the full width of the argument +//! with nothing but copies and merges in between. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_analysis::{Function, Program}; +use e5r_core::{Addr, Strength}; +use e5r_ir::abi::Abi; +use e5r_ir::op::{Op, Space}; +use e5r_ir::range::Range; +use e5r_ir::ssa::{Location, Operand, SsaFunction, SsaKind, SsaOp, Value}; + +/// How well an argument is bounded. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Verdict { + /// A constant upper bound the code establishes, with the bound to show. + Bounded, + /// The code constrains the value nowhere: on some path it exceeds any + /// bound. A claim about the program. + Unconstrained, + /// No bound was established. A claim about the analysis, and never to be + /// read as the one above. + Unknown, +} + +impl Verdict { + /// The word it prints as. + pub fn as_str(self) -> &'static str { + match self { + Verdict::Bounded => "bounded", + Verdict::Unconstrained => "unconstrained", + Verdict::Unknown => "unknown", + } + } +} + +/// Why the verdict is what it is. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Basis { + /// The value is one number, which the code computes and nothing can + /// change: a literal, or something folded to one. + Literal, + /// A comparison that guards the call site bounds it. + Guard, + /// Something in the code narrowed it: a mask, a division, a shift. + Narrowed, + /// Only the width it was read at says anything, which the code did not + /// establish and which is not treated as a bound. + Width, + /// Read from memory with nothing in between. + NoConstraint, + /// It arrives as an argument of the function the call is in. + Caller, + /// It is what another call left behind. + Result, + /// Nothing was found to say either way. + Nothing, +} + +impl Basis { + /// The phrase it prints as. + pub fn as_str(self) -> &'static str { + match self { + Basis::Literal => "literal", + Basis::Guard => "guard", + Basis::Narrowed => "narrowed", + Basis::Width => "width only", + Basis::NoConstraint => "no constraint", + Basis::Caller => "from the caller", + Basis::Result => "from a call", + Basis::Nothing => "no evidence", + } + } +} + +/// Where a value came from, at the end of the walk. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Source { + /// A literal the instruction stream carries, with what it points at when + /// it points at something. + Constant { + /// The literal. + value: u64, + /// The string it addresses, when it addresses one. + text: Option, + /// The symbol it addresses, when it addresses one. + symbol: Option, + }, + /// An argument of the function the call site is in. + Caller { + /// Which argument, counting from zero. + index: usize, + }, + /// A stack slot nothing in this function wrote. + Frame { + /// Offset from the entry stack pointer. + offset: i64, + }, + /// Read from memory. + Memory { + /// The address, when it is a constant. + at: Option, + /// The symbol at that address, when there is one. + symbol: Option, + /// How many bytes were read. + size: u8, + /// True when the address is this function's own frame, which is the + /// shape a spilled local has when promotion refused it. + frame: bool, + }, + /// What another call left in the result register. + Result { + /// The callee, when the call is direct. + target: Option, + /// Its name, when it has one. + name: Option, + }, + /// A register a call was free to leave holding anything. + Clobbered, + /// Computed by an operation the walk does not see through. + Computed { + /// The operation. + op: String, + }, + /// No reaching definition at all. + Nothing, +} + +impl Source { + /// One line naming it, for a row of an answer. + pub fn describe(&self) -> String { + match self { + Source::Constant { + value, + text, + symbol, + } => match (text, symbol) { + (Some(t), _) => format!("string {t:?}"), + (None, Some(s)) => format!("address of {s}"), + (None, None) => format!("literal {value:#x}"), + }, + Source::Caller { index } => format!("arg{} of this function", index + 1), + Source::Frame { offset } => format!("stack slot {offset}"), + Source::Memory { + at, + symbol, + size, + frame, + } => match (symbol, at, frame) { + (Some(s), _, _) => format!("{size}-byte read of {s}"), + (None, _, true) => format!("{size}-byte read of the frame"), + (None, Some(a), _) => format!("{size}-byte read of {a}"), + (None, None, _) => format!("{size}-byte read of memory"), + }, + Source::Result { name, target } => match (name, target) { + (Some(n), _) => format!("result of {n}"), + (None, Some(t)) => format!("result of the call to {t}"), + (None, None) => "result of an indirect call".to_string(), + }, + Source::Clobbered => "left by a call".to_string(), + Source::Computed { op } => format!("computed by {op}"), + Source::Nothing => "nothing".to_string(), + } + } + + /// True when the value was read from somewhere rather than built here. + /// + /// A literal is built, whatever it addresses: the pointer to a string is + /// as fixed as the string. Everything else arrived from outside the + /// instruction that passes it. + pub fn is_read(&self) -> bool { + !matches!(self, Source::Constant { .. } | Source::Nothing) + } + + /// How well the source itself is known. + pub fn strength(&self) -> Strength { + match self { + // The value is in the encoding. + Source::Constant { .. } => Strength::Proven, + // The convention says which register is which argument, and that + // this register was never written is what the SSA form says. + Source::Caller { .. } + | Source::Frame { .. } + | Source::Memory { .. } + | Source::Result { .. } + | Source::Clobbered + | Source::Computed { .. } => Strength::Inferred, + Source::Nothing => Strength::Heuristic, + } + } +} + +/// What is known about one argument at one call site. +#[derive(Debug, Clone)] +pub struct ArgumentFact { + /// Which argument, counting from zero. `arg1` in a query is index zero. + pub index: usize, + /// Bounded, unconstrained, or not established. + pub verdict: Verdict, + /// The interval, when anything is known about it. + pub bound: Option, + /// Why. + pub basis: Basis, + /// How strongly the verdict is held. + pub strength: Strength, + /// Where the value comes from. + pub sources: Vec, +} + +impl ArgumentFact { + /// The name it is written as in a query: `arg1` is the first. + pub fn name(&self) -> String { + format!("arg{}", self.index + 1) + } + + /// The bound as a person reads it. + pub fn bound_text(&self) -> Option { + let b = self.bound?; + Some(if b.low == b.high { + format!("{}", b.low) + } else { + format!("{}..={}", b.low, b.high) + }) + } + + /// The sources as one line. + pub fn source_text(&self) -> String { + if self.sources.is_empty() { + return Source::Nothing.describe(); + } + self.sources + .iter() + .map(|s| s.describe()) + .collect::>() + .join(" or ") + } +} + +/// One call, and what it hands over. +#[derive(Debug, Clone)] +pub struct CallSite { + /// The address of the call itself. + pub at: Addr, + /// The function it is in. + pub caller: Addr, + /// That function's name. + pub caller_name: String, + /// The callee, when the call is direct. + pub target: Option, + /// The callee's name, when it has one. + pub target_name: Option, + /// True when the machine computes the target. + pub indirect: bool, + /// How well the target is known. + pub target_strength: Strength, + /// One per argument the callee appears to take. + pub arguments: Vec, +} + +impl CallSite { + /// The target as a person reads it. + pub fn target_text(&self) -> String { + match (&self.target_name, self.target) { + (Some(n), _) => n.clone(), + (None, Some(t)) => format!("{t}"), + (None, None) => "indirect".to_string(), + } + } + + /// One argument by its zero-based index. + pub fn argument(&self, index: usize) -> Option<&ArgumentFact> { + self.arguments.iter().find(|a| a.index == index) + } + + /// True when the callee's name is this one, with a PLT thunk standing in + /// for what it reaches. + pub fn calls(&self, name: &str) -> bool { + match &self.target_name { + Some(n) => n == name || plain(n) == name, + None => false, + } + } +} + +/// A name with the import thunk's decoration taken off. +fn plain(name: &str) -> &str { + let name = name.strip_suffix("@plt").unwrap_or(name); + name.strip_suffix("@got").unwrap_or(name) +} + +/// Every call in the program, with what is known about its arguments. +pub fn call_sites(p: &Program) -> Vec { + let mut state = Analysis::new(p); + let mut out = Vec::new(); + for f in p.functions_by_address() { + out.extend(state.function(f)); + } + out +} + +/// Every call in one function. +pub fn call_sites_in(p: &Program, f: &Function) -> Vec { + Analysis::new(p).function(f) +} + +/// Every call to one named callee, anywhere in the program. +/// +/// Two shortcuts, and together they are the difference between a question +/// that costs what its callee's callers cost and one that costs what the +/// image costs. Only the functions the cross references say reach the callee +/// are lifted, and inside those only the calls to it are described. +pub fn call_sites_to(p: &Program, name: &str) -> Vec { + let mut state = Analysis::new(p); + state.only = Some(name.to_string()); + let mut out = Vec::new(); + for f in callers_of_name(p, name) { + out.extend(state.function(f)); + } + out +} + +/// The functions that contain a direct call to a function of this name. +/// +/// Cross references already record every direct call, so the set of functions +/// worth lifting for a question about one callee is a lookup rather than a +/// scan. +pub fn callers_of_name<'a>(p: &'a Program, name: &str) -> Vec<&'a Function> { + let mut out: BTreeMap = BTreeMap::new(); + for target in p.functions_by_address() { + let display = target.display_name(); + if display != name && plain(&display) != name { + continue; + } + for x in p.xrefs.to(target.entry) { + if x.kind != e5r_analysis::XrefKind::Call { + continue; + } + if let Some(f) = p.function_at(x.from) { + out.insert(f.entry, f); + } + } + } + out.into_values().collect() +} + +/// How many blocks a function may have before the guard search is skipped. +/// +/// The dominator sets are quadratic in the block count, and a query is not +/// allowed to become the slowest thing in the tool on one pathological +/// function. Past this the other evidence still applies and the guard does +/// not, which loses bounds rather than inventing them. +const MAX_BLOCKS_FOR_GUARDS: usize = 512; + +/// How far the walk follows a value back before giving up. +const MAX_DEPTH: usize = 64; + +/// One program's worth of state, so prototypes are recovered once. +struct Analysis<'a> { + p: &'a Program, + abi: Abi, + arity: BTreeMap, + /// The only callee worth describing, when a query named one. A function + /// that calls it usually calls several other things as well, and working + /// out what those are handed is work nobody asked for. + only: Option, +} + +impl<'a> Analysis<'a> { + fn new(p: &'a Program) -> Analysis<'a> { + Analysis { + p, + abi: e5r_ir::abi::of(&p.object.arch), + arity: BTreeMap::new(), + only: None, + } + } + + /// Every call in one function. + fn function(&mut self, f: &Function) -> Vec { + let Some(ssa) = build(self.p, f) else { + return Vec::new(); + }; + let ranges = e5r_ir::range::ranges(&ssa); + let mut defs: BTreeMap = BTreeMap::new(); + for b in ssa.blocks.values() { + for op in &b.ops { + if let Some(out) = op.out { + defs.insert(out, op.clone()); + } + } + } + let doms = dominators(&ssa); + + let mut out = Vec::new(); + for (at, b) in &ssa.blocks { + for (index, op) in b.ops.iter().enumerate() { + let indirect = match op.kind { + SsaKind::Op(Op::Call) => false, + SsaKind::Op(Op::CallInd) => true, + _ => continue, + }; + let target = op + .inputs + .first() + .and_then(|i| i.as_const()) + .filter(|_| !indirect) + .map(Addr); + let target_name = target.and_then(|t| self.p.name_of(t)); + if let Some(only) = &self.only { + let hit = target_name + .as_deref() + .is_some_and(|n| n == only || plain(n) == only); + if !hit { + continue; + } + } + let ctx = Site { + ssa: &ssa, + ranges: &ranges, + defs: &defs, + doms: &doms, + block: *at, + index, + }; + out.push(CallSite { + at: op.addr, + caller: f.entry, + caller_name: f.display_name(), + target, + target_name, + indirect, + target_strength: self.target_strength(target, indirect), + arguments: self.arguments(&ctx, target), + }); + } + } + out + } + + /// How sure we are about who is being called. + fn target_strength(&self, target: Option, indirect: bool) -> Strength { + // That the machine computes its target is what the encoding says: an + // indirect call is indirect however little is known about where it + // goes. + if indirect { + return Strength::Proven; + } + match target.and_then(|t| self.p.function(t)) { + Some(f) => f.provenance.strength(), + // A direct call whose target is not a recovered function: the + // decode gives the address and the rest is a step past it. + None => Strength::Inferred, + } + } + + /// What each argument of a call is. + fn arguments(&mut self, site: &Site<'_>, target: Option) -> Vec { + let count = self.arity_of(target, site); + (0..count) + .map(|index| { + let offset = self.abi.integer_arguments[index]; + let location = Location { + space: Space::Register, + offset, + size: 8, + }; + let reaching = site.reaching(location); + self.fact(site, index, &reaching) + }) + .collect() + } + + /// How many arguments to describe. + /// + /// Whichever of two counts is larger. The callee's own recovered prototype + /// is the better evidence where there is one, since a register holding a + /// leftover from earlier code is not an argument. But an import thunk has + /// a prototype of nothing — it reads no argument register, it jumps — and + /// a query about the third argument of a call through one has to be + /// answered rather than silently dropped, so the argument registers this + /// call site sets up, counted from the first until one is not set, are the + /// other half of the answer. + fn arity_of(&mut self, target: Option, site: &Site<'_>) -> usize { + let most = self.abi.integer_arguments.len(); + let declared = target + .map(|t| self.declared_arity(t)) + .unwrap_or(0) + .min(most); + if declared >= most { + return most; + } + let mut set_up = 0; + for offset in &self.abi.integer_arguments { + let location = Location { + space: Space::Register, + offset: *offset, + size: 8, + }; + // A register nothing in this function wrote is not an argument + // this call passes: it is whatever arrived, which the reaching + // walk reports as an undefined location. Neither is one an earlier + // call was free to leave holding anything, which the IR says in as + // many words. + let written = site.reaching(location).iter().any(|o| { + matches!(o, Operand::Value(_)) + && !matches!( + site.def(*o).map(|d| &d.kind), + Some(SsaKind::Op(Op::Undefine)) + ) + }); + if !written { + break; + } + set_up += 1; + } + declared.max(set_up).min(most) + } + + /// How many integer arguments a callee's own code says it takes. + fn declared_arity(&mut self, target: Addr) -> usize { + if let Some(known) = self.arity.get(&target) { + return *known; + } + let n = self + .p + .function(target) + .and_then(|f| build(self.p, f)) + .map(|ssa| e5r_ir::proto::recover(&ssa, &self.abi).integer_arguments) + .unwrap_or(0); + self.arity.insert(target, n); + n + } + + /// What is known about one argument. + fn fact(&self, site: &Site<'_>, index: usize, reaching: &[Operand]) -> ArgumentFact { + if reaching.is_empty() { + return ArgumentFact { + index, + verdict: Verdict::Unknown, + bound: None, + basis: Basis::Nothing, + strength: Strength::Heuristic, + sources: vec![Source::Nothing], + }; + } + + let mut sources = Vec::new(); + let mut judgements = Vec::new(); + for operand in reaching { + let mut seen = BTreeSet::new(); + site.sources(self.p, &self.abi, *operand, &mut seen, &mut sources, 0); + judgements.push(self.judge(site, *operand)); + } + sources.dedup(); + + // The join over every definition that reaches the call. Bounded only + // when every path is bounded; unconstrained when any path is, because + // the claim is about some path and not about all of them. + let bounded = judgements + .iter() + .all(|j| j.verdict == Verdict::Bounded) + .then(|| { + judgements + .iter() + .filter_map(|j| j.bound) + .reduce(Range::join) + }) + .flatten(); + let verdict = match ( + bounded.is_some(), + judgements + .iter() + .any(|j| j.verdict == Verdict::Unconstrained), + ) { + (true, _) => Verdict::Bounded, + (false, true) => Verdict::Unconstrained, + (false, false) => Verdict::Unknown, + }; + let leader = judgements + .iter() + .find(|j| j.verdict == verdict) + .unwrap_or(&judgements[0]); + let bound = match verdict { + Verdict::Bounded => bounded, + // Nothing is claimed, but what the width admits is still worth + // showing next to the reason the verdict is what it is. + _ => leader.bound, + }; + ArgumentFact { + index, + verdict, + bound, + basis: leader.basis, + strength: strength_of(verdict, leader.basis), + sources, + } + } + + /// What is known about one definition that reaches the call. + fn judge(&self, site: &Site<'_>, operand: Operand) -> Judgement { + if let Operand::Const(v, size) = operand { + let value = signed(v, size); + return Judgement { + verdict: Verdict::Bounded, + bound: Some(Range::exact(value)), + basis: Basis::Literal, + }; + } + + let interval = e5r_ir::range::of(&operand, site.ranges); + // One number and nothing else, which is what the constant a call site + // materializes in a register looks like once folding has run. As good + // a bound as the literal it came from. + if interval.low == interval.high { + return Judgement { + verdict: Verdict::Bounded, + bound: Some(interval), + basis: Basis::Literal, + }; + } + let guard = site.guard(operand); + let narrowed = site.narrows(operand); + + // The tighter of what the intervals say and what the guard says, with + // the guard preferred when it is what produced the upper end. + let combined = match guard { + Some(g) => Range { + low: interval.low.max(g.low), + high: interval.high.min(g.high), + }, + None => interval, + }; + let from_guard = guard.is_some_and(|g| g.high <= interval.high); + // A negative low end means the value is huge read as the unsigned + // count a size argument is, so it bounds nothing. + let usable = combined.low >= 0 && combined.high >= combined.low && !combined.is_any(); + if usable && (from_guard || narrowed) { + return Judgement { + verdict: Verdict::Bounded, + bound: Some(combined), + basis: if from_guard { + Basis::Guard + } else { + Basis::Narrowed + }, + }; + } + + if site.unconstrained(&self.abi, operand) { + return Judgement { + verdict: Verdict::Unconstrained, + bound: None, + basis: Basis::NoConstraint, + }; + } + + // Nothing established. The basis says why, which is what tells an + // analyst whether to go and look. + let basis = match site.root_kind(&self.abi, operand) { + RootKind::Caller => Basis::Caller, + RootKind::Result => Basis::Result, + RootKind::Other if usable => Basis::Width, + RootKind::Other => Basis::Nothing, + }; + Judgement { + verdict: Verdict::Unknown, + bound: usable.then_some(combined), + basis, + } + } +} + +/// How strongly a verdict is held. +/// +/// A literal is in the encoding and nothing can change it. Everything else is +/// derived from a decode that is itself proven, which is what `Inferred` means +/// here. An unknown verdict claims nothing, so it carries the weakest strength +/// there is rather than borrowing one. +fn strength_of(verdict: Verdict, basis: Basis) -> Strength { + match (verdict, basis) { + (Verdict::Bounded, Basis::Literal) => Strength::Proven, + (Verdict::Bounded, _) | (Verdict::Unconstrained, _) => Strength::Inferred, + (Verdict::Unknown, _) => Strength::Heuristic, + } +} + +/// What one reaching definition says on its own. +struct Judgement { + verdict: Verdict, + bound: Option, + basis: Basis, +} + +/// Which kind of thing a value ultimately comes from. +enum RootKind { + Caller, + Result, + Other, +} + +/// One call site, and the function around it. +struct Site<'a> { + ssa: &'a SsaFunction, + ranges: &'a BTreeMap, + defs: &'a BTreeMap, + doms: &'a BTreeMap>, + block: Addr, + index: usize, +} + +impl Site<'_> { + /// The definitions of a location that reach this call site. + /// + /// The block first, then its predecessors, which is where a phi is found + /// when the paths disagree. An empty result at the entry means the + /// function never wrote the location, which the caller reports as an + /// incoming value rather than as nothing. + fn reaching(&self, location: Location) -> Vec { + let mut out: Vec = Vec::new(); + let mut seen: BTreeSet = BTreeSet::new(); + let mut work = vec![(self.block, self.index)]; + seen.insert(self.block); + while let Some((at, before)) = work.pop() { + let Some(b) = self.ssa.blocks.get(&at) else { + continue; + }; + let found = b.ops[..before.min(b.ops.len())] + .iter() + .rev() + .filter_map(|op| op.out) + .find(|v| v.location == location); + if let Some(v) = found { + let operand = Operand::Value(v); + if !out.contains(&operand) { + out.push(operand); + } + continue; + } + if b.predecessors.is_empty() { + let operand = Operand::Undefined(location); + if !out.contains(&operand) { + out.push(operand); + } + continue; + } + for pred in &b.predecessors { + if seen.insert(*pred) { + let len = self.ssa.blocks.get(pred).map(|p| p.ops.len()).unwrap_or(0); + work.push((*pred, len)); + } + } + } + out + } + + /// The operation that defined a value, when one did. + fn def(&self, operand: Operand) -> Option<&SsaOp> { + match operand { + Operand::Value(v) => self.defs.get(&v), + _ => None, + } + } + + /// The values a bound on which is a bound on this one. + /// + /// Only the steps that carry a bound forward: a copy keeps the value, a + /// widening keeps a non-negative one, and a truncation cannot raise it. A + /// phi is not one of these, because a bound on one of the paths into a + /// merge says nothing about the other. + fn carriers(&self, operand: Operand, out: &mut BTreeSet, depth: usize) { + if depth > MAX_DEPTH || !out.insert(operand) { + return; + } + let Some(op) = self.def(operand) else { return }; + let SsaKind::Op(kind) = op.kind else { return }; + let carries = match kind { + Op::Copy | Op::IntZExt | Op::IntSExt => true, + Op::SubPiece => op.inputs.get(1).and_then(|i| i.as_const()) == Some(0), + _ => false, + }; + if !carries { + return; + } + if let Some(input) = op.inputs.first() { + self.carriers(*input, out, depth + 1); + } + } + + /// The bound the comparisons that guard this call site put on a value. + fn guard(&self, operand: Operand) -> Option { + if self.ssa.blocks.len() > MAX_BLOCKS_FOR_GUARDS { + return None; + } + let mut atoms = BTreeSet::new(); + self.carriers(operand, &mut atoms, 0); + let reached = self.doms.get(&self.block)?; + + let mut out: Option = None; + for at in reached { + if *at == self.block { + continue; + } + let Some(b) = self.ssa.blocks.get(at) else { + continue; + }; + let Some(last) = b.ops.last() else { continue }; + if last.kind != SsaKind::Op(Op::CBranch) { + continue; + } + let Some(taken) = self.edge(b, last) else { + continue; + }; + let Some(cond) = last.inputs.get(1) else { + continue; + }; + let facts = self.constraints(*cond, taken, 0); + for (atom, range) in facts { + if !atoms.contains(&atom) { + continue; + } + out = Some(match out { + Some(existing) => Range { + low: existing.low.max(range.low), + high: existing.high.min(range.high), + }, + None => range, + }); + } + } + out + } + + /// Whether the call site is on the branch's taken edge or its other one. + /// + /// Only when one of the two successors dominates the call: if both do, or + /// neither, the branch says nothing about how control got here. + fn edge(&self, b: &e5r_ir::ssa::SsaBlock, cbranch: &SsaOp) -> Option { + let target = Addr(cbranch.inputs.first()?.as_const()?); + if !b.successors.contains(&target) { + return None; + } + let other = *b.successors.iter().find(|s| **s != target)?; + let by_target = self.dominates(target); + let by_other = self.dominates(other); + match (by_target, by_other) { + (true, false) => Some(true), + (false, true) => Some(false), + _ => None, + } + } + + /// True when every path to this call site goes through a block. + fn dominates(&self, block: Addr) -> bool { + self.doms + .get(&self.block) + .is_some_and(|d| d.contains(&block)) + } + + /// What a condition says about the values in it, given which way it went. + /// + /// A disjunction keeps only what both sides agree on, because a value + /// bounded on one side of an `or` and not the other is not bounded. That + /// is what makes a negated `and` usable, which is the shape a compiler + /// emits for `if (n > 64) return;`: the call is on the edge where the + /// conjunction failed, and the two ways it can fail bound the value + /// between them. + fn constraints(&self, cond: Operand, taken: bool, depth: usize) -> BTreeMap { + let mut out = BTreeMap::new(); + if depth > MAX_DEPTH { + return out; + } + let Some(op) = self.def(cond) else { return out }; + let SsaKind::Op(kind) = op.kind else { + return out; + }; + let a = op.inputs.first().copied(); + let b = op.inputs.get(1).copied(); + match kind { + Op::Copy => { + if let Some(a) = a { + return self.constraints(a, taken, depth + 1); + } + } + Op::BoolNot => { + if let Some(a) = a { + return self.constraints(a, !taken, depth + 1); + } + } + Op::BoolAnd | Op::BoolOr => { + let (Some(a), Some(b)) = (a, b) else { + return out; + }; + let both = (kind == Op::BoolAnd) == taken; + let left = self.constraints(a, taken, depth + 1); + let right = self.constraints(b, taken, depth + 1); + if both { + // Both hold: every constraint applies, and one on the same + // value from both sides applies twice. + out = left; + for (atom, range) in right { + let merged = match out.get(&atom) { + Some(existing) => Range { + low: existing.low.max(range.low), + high: existing.high.min(range.high), + }, + None => range, + }; + out.insert(atom, merged); + } + } else { + // One of them holds and nothing says which: only a value + // both sides bound is bounded, by the wider of the two. + for (atom, range) in left { + if let Some(other) = right.get(&atom) { + out.insert(atom, range.join(*other)); + } + } + } + } + Op::IntEqual | Op::IntNotEqual => { + // Equality bounds a value only when it holds; that it is not + // one particular number is not an interval. + if taken == (kind == Op::IntEqual) { + if let Some((value, k)) = compared(a, b) { + out.insert(value, Range::exact(k)); + } + } + } + Op::IntLess | Op::IntLessEqual | Op::IntSLess | Op::IntSLessEqual => { + let signed = matches!(kind, Op::IntSLess | Op::IntSLessEqual); + let orequal = matches!(kind, Op::IntLessEqual | Op::IntSLessEqual); + let floor = if signed { i64::MIN } else { 0 }; + let (Some(a), Some(b)) = (a, b) else { + return out; + }; + // `x < k` holding, or `k < x` failing, are both upper bounds + // on the side that is not the constant. + if let (None, Some(k)) = (a.as_const(), b.as_const().map(|v| signed_of(b, v))) { + let high = if orequal { Some(k) } else { k.checked_sub(1) }; + if let (true, Some(high)) = (taken, high) { + out.insert(a, Range { low: floor, high }); + } + } else if let (Some(k), None) = + (a.as_const().map(|v| signed_of(a, v)), b.as_const()) + { + let high = if orequal { k.checked_sub(1) } else { Some(k) }; + if let (false, Some(high)) = (taken, high) { + out.insert(b, Range { low: floor, high }); + } + } + } + _ => {} + } + out + } + + /// True when something in the code narrowed the value. + /// + /// A mask, a division, a remainder or a shift is the code saying how large + /// the value can be. A widening or a copy is not: it says how large the + /// storage is, which is not a bound the program established. + fn narrows(&self, operand: Operand) -> bool { + let mut seen = BTreeSet::new(); + self.narrows_inner(operand, &mut seen, 0) + } + + fn narrows_inner(&self, operand: Operand, seen: &mut BTreeSet, depth: usize) -> bool { + if depth > MAX_DEPTH || !seen.insert(operand) { + return false; + } + let Some(op) = self.def(operand) else { + return false; + }; + match op.kind { + SsaKind::Phi => op.inputs.iter().all(|i| { + matches!(i, Operand::Const(..)) || self.narrows_inner(*i, seen, depth + 1) + }), + SsaKind::Op(kind) => match kind { + Op::IntAnd + | Op::IntDiv + | Op::IntRem + | Op::IntRight + | Op::IntSRight + | Op::PopCount + | Op::LzCount + | Op::IntEqual + | Op::IntNotEqual + | Op::IntLess + | Op::IntLessEqual + | Op::IntSLess + | Op::IntSLessEqual => true, + Op::Copy | Op::IntZExt | Op::IntSExt | Op::SubPiece => op + .inputs + .first() + .is_some_and(|i| self.narrows_inner(*i, seen, depth + 1)), + // Arithmetic on something narrowed stays narrowed: the + // interval domain already carried the bound through it. + Op::IntAdd | Op::IntSub | Op::IntMul | Op::IntLeft => op + .inputs + .iter() + .any(|i| self.narrows_inner(*i, seen, depth + 1)), + _ => false, + }, + } + } + + /// True when the code constrains this value nowhere. + /// + /// Strict on purpose. The value has to arrive from a memory read at the + /// full width of an argument register with nothing but copies and merges + /// in between, and no guard on the way to the call. Anything narrower has + /// its width as a bound, which is not "exceeds any bound", and anything + /// else on the path is a constraint this refuses to look past. + fn unconstrained(&self, abi: &Abi, operand: Operand) -> bool { + let mut seen = BTreeSet::new(); + self.unconstrained_inner(abi, operand, &mut seen, 0) + } + + fn unconstrained_inner( + &self, + abi: &Abi, + operand: Operand, + seen: &mut BTreeSet, + depth: usize, + ) -> bool { + if depth > MAX_DEPTH || !seen.insert(operand) { + return false; + } + let Some(op) = self.def(operand) else { + return false; + }; + match op.kind { + // A merge is unconstrained when any path into it is, because the + // claim is that the value exceeds any bound on some path. + SsaKind::Phi => op + .inputs + .iter() + .any(|i| self.unconstrained_inner(abi, *i, seen, depth + 1)), + SsaKind::Op(Op::Copy) => op + .inputs + .first() + .is_some_and(|i| self.unconstrained_inner(abi, *i, seen, depth + 1)), + // A read of this function's own frame is a spilled local, not + // memory an attacker fills: what put it there is somewhere in this + // function and the walk did not follow it. Claiming the code + // constrains it nowhere would be claiming something about the + // analysis and calling it a fact about the program. + SsaKind::Op(Op::Load) => { + op.size >= 8 + && !op + .inputs + .first() + .is_some_and(|a| self.frame_relative(abi, *a, &mut BTreeSet::new(), 0)) + } + _ => false, + } + } + + /// True when an address is worked out from this function's own frame. + fn frame_relative( + &self, + abi: &Abi, + operand: Operand, + seen: &mut BTreeSet, + depth: usize, + ) -> bool { + if depth > MAX_DEPTH || !seen.insert(operand) { + return false; + } + match operand { + Operand::Undefined(l) => { + l.space == Space::Stack + || (l.space == Space::Register && l.offset == abi.stack_pointer) + } + Operand::Const(..) => false, + Operand::Value(v) => { + if v.location.space == Space::Stack + || (v.location.space == Space::Register + && v.location.offset == abi.stack_pointer) + { + return true; + } + match self.def(operand) { + Some(op) => op + .inputs + .iter() + .any(|i| self.frame_relative(abi, *i, seen, depth + 1)), + None => false, + } + } + } + } + + /// Which kind of thing a value comes from, for the reason an unknown + /// verdict gives. + fn root_kind(&self, abi: &Abi, operand: Operand) -> RootKind { + let mut seen = BTreeSet::new(); + let mut sources = Vec::new(); + self.collect(abi, operand, &mut seen, &mut sources, 0); + if sources.iter().any(|s| matches!(s, Root::Caller)) { + return RootKind::Caller; + } + if sources.iter().any(|s| matches!(s, Root::Result)) { + return RootKind::Result; + } + RootKind::Other + } + + /// Where a value comes from, followed back to what produced it. + fn sources( + &self, + p: &Program, + abi: &Abi, + operand: Operand, + seen: &mut BTreeSet, + out: &mut Vec, + depth: usize, + ) { + if depth > MAX_DEPTH || !seen.insert(operand) { + return; + } + let Some(op) = self.def(operand) else { + let source = match operand { + // A narrow constant is a number rather than an address, + // whatever happens to sit at that address. + Operand::Const(v, size) if size < 8 => Source::Constant { + value: v, + text: None, + symbol: None, + }, + Operand::Const(v, _) => Source::Constant { + value: v, + text: p.text_at(Addr(v), 96), + symbol: p + .object + .symbol_at(Addr(v)) + .filter(|s| s.addr == Addr(v) && !s.name.is_empty()) + .map(|s| s.name.clone()), + }, + Operand::Undefined(l) if l.space == Space::Stack => Source::Frame { + offset: l.offset as i64, + }, + Operand::Undefined(l) if l.space == Space::Register => { + match abi.integer_arguments.iter().position(|o| *o == l.offset) { + Some(index) => Source::Caller { index }, + None => Source::Computed { + op: "a register this function never wrote".to_string(), + }, + } + } + _ => Source::Nothing, + }; + if !out.contains(&source) { + out.push(source); + } + return; + }; + + match op.kind { + SsaKind::Phi => { + for i in &op.inputs { + self.sources(p, abi, *i, seen, out, depth + 1); + } + } + SsaKind::Op(Op::Copy | Op::IntZExt | Op::IntSExt | Op::SubPiece | Op::Piece) => { + for i in op.inputs.iter().take(1) { + self.sources(p, abi, *i, seen, out, depth + 1); + } + } + SsaKind::Op(Op::Load) => { + let at = op.inputs.first().and_then(|i| i.as_const()).map(Addr); + let source = Source::Memory { + at, + symbol: at.and_then(|a| p.object.symbol_at(a)).and_then(|s| { + (!s.name.is_empty() && !s.name.starts_with('$')).then(|| s.name.clone()) + }), + size: op.size, + frame: op + .inputs + .first() + .is_some_and(|a| self.frame_relative(abi, *a, &mut BTreeSet::new(), 0)), + }; + if !out.contains(&source) { + out.push(source); + } + } + SsaKind::Op(Op::Call | Op::CallInd) => { + let target = op.inputs.first().and_then(|i| i.as_const()).map(Addr); + let source = Source::Result { + target, + name: target.and_then(|t| p.name_of(t)), + }; + if !out.contains(&source) { + out.push(source); + } + } + SsaKind::Op(Op::Undefine) => { + if !out.contains(&Source::Clobbered) { + out.push(Source::Clobbered); + } + } + SsaKind::Op(other) => { + let source = Source::Computed { + op: format!("{other:?}").to_lowercase(), + }; + if !out.contains(&source) { + out.push(source); + } + } + } + } + + /// The same walk, kept to the kinds the unknown reason needs. + fn collect( + &self, + abi: &Abi, + operand: Operand, + seen: &mut BTreeSet, + out: &mut Vec, + depth: usize, + ) { + if depth > MAX_DEPTH || !seen.insert(operand) { + return; + } + let Some(op) = self.def(operand) else { + if let Operand::Undefined(l) = operand { + if l.space == Space::Register && abi.integer_arguments.contains(&l.offset) { + out.push(Root::Caller); + } + } + return; + }; + match op.kind { + SsaKind::Op(Op::Call | Op::CallInd | Op::Undefine) => out.push(Root::Result), + SsaKind::Phi + | SsaKind::Op(Op::Copy | Op::IntZExt | Op::IntSExt | Op::SubPiece | Op::Piece) => { + for i in &op.inputs { + self.collect(abi, *i, seen, out, depth + 1); + } + } + _ => {} + } + } +} + +/// What a walk found, without the detail the reason does not need. +enum Root { + Caller, + Result, +} + +/// The value and the constant of a comparison, when one side is a constant. +fn compared(a: Option, b: Option) -> Option<(Operand, i64)> { + match (a?, b?) { + (value, Operand::Const(k, size)) if value.as_const().is_none() => { + Some((value, signed(k, size))) + } + (Operand::Const(k, size), value) if value.as_const().is_none() => { + Some((value, signed(k, size))) + } + _ => None, + } +} + +/// A constant read at the width the operation reads it at. +fn signed_of(operand: Operand, value: u64) -> i64 { + signed(value, operand.size()) +} + +/// A constant read as a signed number of its own width. +fn signed(value: u64, size: u8) -> i64 { + let bits = (size as u32 * 8).min(64); + if bits == 64 { + value as i64 + } else { + ((value << (64 - bits)) as i64) >> (64 - bits) + } +} + +/// Which blocks every path to a block goes through. +fn dominators(f: &SsaFunction) -> BTreeMap> { + let all: BTreeSet = f.blocks.keys().copied().collect(); + let mut out: BTreeMap> = BTreeMap::new(); + for at in &all { + if *at == f.entry { + out.insert(*at, BTreeSet::from([*at])); + } else { + out.insert(*at, all.clone()); + } + } + if all.len() > MAX_BLOCKS_FOR_GUARDS { + return out; + } + // Iterate to a fixed point. The sets only shrink, so this settles in at + // most one round per block on the longest path. + let order: Vec = f.blocks.keys().copied().collect(); + loop { + let mut changed = false; + for at in &order { + if *at == f.entry { + continue; + } + let Some(b) = f.blocks.get(at) else { continue }; + let mut merged: Option> = None; + for pred in &b.predecessors { + let Some(d) = out.get(pred) else { continue }; + merged = Some(match merged { + Some(existing) => existing.intersection(d).copied().collect(), + None => d.clone(), + }); + } + let mut next = merged.unwrap_or_default(); + next.insert(*at); + if out.get(at) != Some(&next) { + out.insert(*at, next); + changed = true; + } + } + if !changed { + break; + } + } + out +} + +/// One function in SSA form, optimized, which is what every pass here reads. +fn build(p: &Program, f: &Function) -> Option { + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + if blocks.is_empty() { + return None; + } + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut ssa = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut ssa); + Some(ssa) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_constant_is_read_at_its_own_width() { + assert_eq!(signed(0xff, 1), -1); + assert_eq!(signed(0xff, 8), 255); + assert_eq!(signed(0x40, 8), 64); + } + + #[test] + fn the_verdicts_print_as_different_words() { + // The whole feature is that these two are never confused, so they are + // never spelled the same way either. + assert_ne!( + Verdict::Unconstrained.as_str(), + Verdict::Unknown.as_str(), + "the two ways of not being bounded have to read differently" + ); + } + + #[test] + fn only_a_literal_is_proven() { + assert_eq!( + strength_of(Verdict::Bounded, Basis::Literal), + Strength::Proven + ); + assert_eq!( + strength_of(Verdict::Bounded, Basis::Guard), + Strength::Inferred + ); + assert_eq!( + strength_of(Verdict::Unconstrained, Basis::NoConstraint), + Strength::Inferred + ); + assert_eq!( + strength_of(Verdict::Unknown, Basis::Caller), + Strength::Heuristic + ); + } + + #[test] + fn an_import_thunk_stands_in_for_what_it_reaches() { + assert_eq!(plain("memcpy@plt"), "memcpy"); + assert_eq!(plain("memcpy"), "memcpy"); + } +} diff --git a/crates/e5r-api/src/decompile.rs b/crates/e5r-api/src/decompile.rs new file mode 100644 index 0000000..564c1ba --- /dev/null +++ b/crates/e5r-api/src/decompile.rs @@ -0,0 +1,659 @@ +//! Decompiling a program, from analysis to C. +//! +//! The whole pipeline in one place: lift, promote the stack, build SSA, +//! optimize, structure, emit. What the debug information says is used where it +//! says anything, and the functions of a program come out as one translation +//! unit with everything declared before it is used. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_analysis::{Function, Program}; +use e5r_core::{Addr, Evidence}; +use e5r_decomp::{Callee, Output, Param, Prototype}; +use e5r_ir::proto::Asserted; +use e5r_ir::ssa::SsaFunction; +use e5r_types::ctype::{Type, TypeId, Types}; + +pub use e5r_decomp::expr::{Home, Role, Variable}; + +/// One decompiled function. +#[derive(Debug, Clone)] +pub struct Decompiled { + /// Where it starts. + pub addr: Addr, + /// What it is called. + pub name: String, + /// Its declaration, without a body. + pub signature: String, + /// The C text. + pub text: String, + /// Gotos the structuring needed. + pub gotos: usize, + /// Reachable blocks the structuring never placed, so their code is missing + /// from the text. Zero unless the structurer has a defect. + pub lost: usize, + /// Named locals declared. + pub locals: usize, + /// Every variable the text declares, with its name, its type and where the + /// machine kept it. + /// + /// A count of locals is not a fact anything can be checked against. This + /// is what a benchmark matching recovered variables against the ones the + /// source declared has to read, and what an analyst renames one by. + pub variables: Vec, + /// Where an asserted declaration and the code disagreed, in the analyst's + /// favour. Empty unless something was asserted about this function. + pub conflicts: Vec, + /// True when a declaration somebody wrote down decided this signature. + pub asserted: bool, + /// Operations no expression covered, including instructions the lifter did + /// not model. + pub unmodelled: usize, +} + +/// A set of functions decompiled together. +#[derive(Debug, Clone, Default)] +pub struct Unit { + /// Declarations the text needs, in an order C accepts. + pub declarations: Vec, + /// The functions, in address order. + pub functions: Vec, +} + +impl Unit { + /// The whole unit as one translation unit. + pub fn text(&self) -> String { + let mut out = String::new(); + if !self.declarations.is_empty() { + out.push_str("#include \n"); + for d in &self.declarations { + out.push_str(d); + out.push('\n'); + } + out.push('\n'); + } + for (n, f) in self.functions.iter().enumerate() { + if n > 0 { + out.push('\n'); + } + out.push_str(&f.text); + } + out + } + + /// How many gotos the structuring needed across the unit. + pub fn gotos(&self) -> usize { + self.functions.iter().map(|f| f.gotos).sum() + } +} + +/// Decompile one function, with whatever the program knows about it. +pub fn decompile_function(p: &Program, f: &Function) -> Decompiled { + decompile_function_with(p, f, &BTreeMap::new()) +} + +/// The same, with the declarations an analyst asserted about the program. +/// +/// Keyed by the address each declaration resolved to, which is the shape the +/// annotation log folds to and the same one comments reach the disassembler in. +pub fn decompile_function_with( + p: &Program, + f: &Function, + declarations: &BTreeMap, +) -> Decompiled { + let mut unit = decompile_program_with(p, std::slice::from_ref(&f), declarations); + unit.functions.pop().unwrap_or(Decompiled { + addr: f.entry, + name: f.display_name(), + signature: String::new(), + text: String::new(), + gotos: 0, + lost: 0, + locals: 0, + variables: Vec::new(), + conflicts: Vec::new(), + asserted: false, + unmodelled: 0, + }) +} + +/// Decompile a set of functions as one unit. +pub fn decompile_program(p: &Program, targets: &[&Function]) -> Unit { + decompile_program_with(p, targets, &BTreeMap::new()) +} + +/// Decompile a set of functions as one unit, honouring what an analyst +/// declared about them. +/// +/// Two passes: the first works out how many parameters each function declares, +/// and the second uses that so every call passes the number its callee expects. +/// Nothing knows the count until the parameters have been worked out. +pub fn decompile_program_with( + p: &Program, + targets: &[&Function], + declarations: &BTreeMap, +) -> Unit { + let mut callees: BTreeMap = BTreeMap::new(); + let mut outputs: Vec<(Addr, String, One)> = Vec::new(); + + // Everything the targets call, so that asking for one function gives the + // same body as asking for all of them. Without this the callee table holds + // only the targets, and a call to anything else renders as an anonymous + // name with no arguments: two different answers for one function, + // depending on how it was asked for. + let wanted: BTreeSet = targets.iter().map(|f| f.entry).collect(); + let mut around: Vec<&Function> = Vec::new(); + for f in targets { + for site in &f.cfg.calls { + if wanted.contains(site) { + continue; + } + if let Some(callee) = p.function(*site) + && !around.iter().any(|c| c.entry == callee.entry) + { + around.push(callee); + } + } + } + + for pass in 0..2 { + outputs.clear(); + // The neighbours are prototyped but never emitted: they are here to + // fill the table, and on the last pass their bodies would be thrown + // away anyway. + for (n, f) in targets.iter().chain(around.iter()).enumerate() { + let Some(one) = one(p, f, &callees, declarations) else { + continue; + }; + let out = &one.output; + let name = f.display_name(); + callees.insert( + f.entry.get(), + Callee { + name: e5r_decomp::identifier(&name), + arity: out.arity, + pointer_parameters: out.pointer_parameters.clone(), + parameter_widths: out.parameter_widths.clone(), + returns_value: !out.signature.starts_with("void "), + signature: out.signature.clone(), + }, + ); + if n < targets.len() { + outputs.push((f.entry, name, one)); + } + } + let _ = pass; + } + + // Deduplicated but not sorted: a type definition has to come after the + // ones it mentions, and alphabetical order is not that. + let mut declared: Vec = Vec::new(); + for (_, _, one) in &outputs { + for d in &one.output.declarations { + if !declared.contains(d) { + declared.push(d.clone()); + } + } + } + // Every function declared before any is defined, so a call to one defined + // later still type-checks. The callee table is declared too, not just the + // targets: a call to a function nobody asked to see is still written by + // name, and a call to an undeclared name is not C a compiler will accept. + for (_, _, one) in &outputs { + let declaration = format!("{};", one.output.signature); + if !declared.contains(&declaration) { + declared.push(declaration); + } + } + for callee in callees.values() { + if callee.signature.is_empty() { + continue; + } + let declaration = format!("{};", callee.signature); + if !declared.contains(&declaration) { + declared.push(declaration); + } + } + + Unit { + declarations: declared, + functions: outputs + .into_iter() + .map(|(addr, name, one)| Decompiled { + addr, + name, + signature: one.output.signature, + text: one.output.text, + gotos: one.output.gotos, + lost: one.output.lost, + locals: one.output.locals, + variables: one.variables, + conflicts: one.conflicts, + asserted: one.asserted, + unmodelled: one.output.unmodelled + one.unlifted, + }) + .collect(), + } +} + +/// One function's way through the pipeline, and what was learned on the way. +struct One { + output: Output, + /// Instructions the lifter did not model, which the emitter never saw. + unlifted: usize, + variables: Vec, + conflicts: Vec, + asserted: bool, +} + +/// One function through the whole pipeline. +fn one( + p: &Program, + f: &Function, + callees: &BTreeMap, + declarations: &BTreeMap, +) -> Option { + // What each jump table means: the index the branch used and where that + // index goes, which is what turns a many-successor block into a switch. + // + // Keyed by the block the branch ends, not by the branch's own address. The + // structurer knows blocks by where they start, and an indirect branch is + // the last instruction of its block rather than the first, so keying by + // `JumpTable::at` makes every lookup miss and no switch is ever built. + let switches: e5r_decomp::Switches = f + .cfg + .tables + .iter() + .filter_map(|t| { + let (start, _) = f.cfg.blocks.iter().find(|(_, b)| b.range.contains(t.at))?; + Some(( + *start, + t.targets + .iter() + .enumerate() + .map(|(n, target)| (n as u64, *target)) + .collect(), + )) + }) + .collect(); + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut ssa = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut ssa); + // The SSA the emitter runs on is the SSA prototype recovery reads, rather + // than a second build of the same thing: lifting a function twice is not + // free and the two could drift apart. + let shape = prototype(p, f, &ssa, declarations); + let name = f.display_name(); + let output = + e5r_decomp::decompile_full(&name, &ssa, shape.prototype.as_ref(), callees, &switches); + Some(One { + variables: e5r_decomp::expr::variables(&ssa, shape.prototype.as_ref(), &output.text), + output, + unlifted: ir.unlifted.len(), + conflicts: shape.conflicts, + asserted: shape.asserted, + }) +} + +/// What is known about a function's shape, in the form the emitter wants, and +/// what had to be overruled to get there. +struct Shape { + prototype: Option, + conflicts: Vec, + asserted: bool, +} + +/// What is known about a function's shape, in the form the emitter wants. +/// +/// In order: what an analyst wrote down, then the debug information, then what +/// the code itself says, which is how many argument registers arrive with +/// values and whether anything is left for the caller. The last is weakest and +/// it is all a stripped binary has. +/// +/// An assertion outranks the other two because writing one down is the only +/// way an analyst has of telling the engine it was wrong, and an assertion +/// that changed nothing would make the whole loop pointless. What the machine +/// found is not thrown away: it is kept beside the declaration and the +/// disagreements come back as conflicts. +fn prototype( + p: &Program, + f: &Function, + ssa: &SsaFunction, + declarations: &BTreeMap, +) -> Shape { + if let Some(a) = asserted(p, f, declarations) { + let abi = e5r_ir::abi::of(&p.object.arch); + let recovered = e5r_ir::proto::recover_with(ssa, &abi, Some(&a)); + return Shape { + conflicts: recovered.conflicts.iter().map(|c| c.to_string()).collect(), + prototype: Some(asserted_prototype(&a, &recovered)), + asserted: true, + }; + } + Shape { + prototype: declared(p, f) + .or_else(|| import_thunk(f)) + .or_else(|| recovered(p, f, ssa)), + conflicts: Vec::new(), + asserted: false, + } +} + +/// The declaration an analyst wrote down for this function, parsed. +/// +/// Parsed against the program's own types, so a declaration may name a +/// structure the debug information already described rather than having to +/// restate it. A declaration that does not parse is not a reason to refuse to +/// decompile: the engine's own answer is still there and the annotation +/// commands are where a person is told their C is malformed. +fn asserted(p: &Program, f: &Function, declarations: &BTreeMap) -> Option { + let text = declarations.get(&f.entry)?; + let mut types = match p.object.debug.as_ref() { + Some(d) => d.types.clone(), + None => Types::new(), + }; + // The store carries the types and the architecture carries how wide they + // are, and a declaration mentioning `long` needs both. + types.set_model(e5r_ir::proto::model_of(&p.object.arch)); + Asserted::parse_into(types, text).ok() +} + +/// The emitter's form of an asserted declaration, laid over the convention. +fn asserted_prototype(a: &Asserted, recovered: &e5r_ir::proto::Prototype) -> Prototype { + let types = &a.types; + let mut parameters: Vec = Vec::new(); + let mut mentioned: Vec = Vec::new(); + + // A result too large for the result registers is written through a pointer + // the caller passes in the first argument register, so every declared + // parameter sits one register further along than it looks. Declaring the + // pointer is what keeps the body's names on the right registers, and it is + // also what the machine code actually does. + if recovered.returns_via_memory { + if let Some(ty) = a.signature.returns { + let mut with_pointer = types.clone(); + let ptr = with_pointer.pointer(ty); + parameters.push(Param { + decl: with_pointer.declare(ptr, RESULT), + name: RESULT.to_string(), + floating: false, + pointer: true, + size: with_pointer.size_of(ptr).unwrap_or(0).min(255) as u8, + fields: Vec::new(), + stride: None, + stack: None, + }); + mentioned.push(ty); + } + } + + for (n, param) in recovered.parameters.iter().enumerate() { + let name = param.name.clone().unwrap_or_else(|| format!("arg{n}")); + let resolved = types.get(types.resolve(param.ty)); + parameters.push(Param { + decl: types.declare(param.ty, &name), + name, + floating: matches!(resolved, Some(Type::Float { size: 4 | 8 })), + pointer: matches!(resolved, Some(Type::Pointer(_))), + size: param.size.min(255) as u8, + // A declared type names its own fields. Inventing `field_8` for a + // parameter whose structure is written down would contradict the + // declaration the same output prints. + fields: Vec::new(), + stride: None, + stack: None, + }); + mentioned.push(param.ty); + } + + let returns = match a.signature.returns { + // The result does not come back in a register at all; it is already a + // parameter, and a function that returns nothing returns nothing. + Some(_) if recovered.returns_via_memory => "void".to_string(), + Some(ty) => types.name_of(ty), + None => "void".to_string(), + }; + + let mut definitions: Vec = Vec::new(); + for t in mentioned { + for d in types.dependencies(t) { + if let Some(text) = types.definition(d) { + if !definitions.contains(&text) { + definitions.push(text); + } + } + } + } + + Prototype { + parameters, + returns: Some(returns), + definitions, + locals: BTreeMap::new(), + } +} + +/// What the hidden pointer to a memory-returned result is called. +const RESULT: &str = "__result"; + +/// The shape of a jump into another image. +/// +/// A PLT or IAT thunk's body is one indirect jump. It reads no argument +/// register and writes no result, so recovering its shape from its own code +/// says it takes nothing and returns nothing, and neither is a claim the +/// binary supports: both belong to the imported function, which is not here. +/// Saying `void` is the damaging half. Every caller that uses the result then +/// has a local it reads and never assigns, which is undefined behaviour in the +/// emitted C, so the result is kept and the parameter list is left empty, the +/// same as for any callee nothing is known about. +fn import_thunk(f: &Function) -> Option { + let thunk = f.provenance.best == Evidence::ImportThunk + || f.provenance.corroborating.contains(&Evidence::ImportThunk); + thunk.then(|| Prototype { + parameters: Vec::new(), + returns: Some("uint64_t".to_string()), + definitions: Vec::new(), + locals: BTreeMap::new(), + }) +} + +/// The shape the code implies, for a function nothing declared. +fn recovered(p: &Program, f: &Function, ssa: &SsaFunction) -> Option { + if f.cfg.blocks.is_empty() { + return None; + } + let abi = e5r_ir::abi::of(&p.object.arch); + let recovered = e5r_ir::proto::recover(ssa, &abi); + + // What each incoming pointer was used as, so an access at a known offset + // reads as a field rather than as arithmetic. + let shapes: BTreeMap = e5r_ir::shape::shapes(ssa) + .into_iter() + .filter(|(l, _)| l.offset != abi.stack_pointer) + .map(|(l, shape)| (l.offset, (shape.fields(), shape.size()))) + .collect(); + + let mut parameters = Vec::new(); + let mut definitions = Vec::new(); + for n in 0..recovered.integer_arguments { + let name = format!("arg{n}"); + let offset = abi.integer_arguments.get(n).copied().unwrap_or(u64::MAX); + // Two or more fields is a structure worth naming. One is a pointer to + // a plain value, which needs no structure but is still a pointer: + // saying `uint64_t` for something the body dereferences is a worse + // description than saying what it points at. + let shape = shapes.get(&offset); + let (fields, stride) = shape + .filter(|(f, _)| f.len() > 1) + .cloned() + .unwrap_or_default(); + let single = shape + .filter(|(f, _)| f.len() == 1) + .and_then(|(f, _)| f.first().copied()) + .filter(|(at, _)| *at == 0) + .map(|(_, size)| size); + // The name carries the function, because two functions rarely hand + // the same structure to the same argument register and a shared name + // would claim they did. + let tag = format!("{}_{name}", e5r_decomp::identifier(&f.display_name())); + // The register is eight bytes; the argument is as wide as the body + // reads it, which is what stops every parameter being `uint64_t` and + // what lets a caller's `-512` print as itself rather than as + // `0xfffffe00`. + let width = recovered.integer_widths.get(n).copied().unwrap_or(8); + let (decl, pointer) = match (fields.is_empty(), single) { + (false, _) => { + definitions.push(structure(&tag, &fields)); + (format!("struct s_{tag} *{name}"), true) + } + (true, Some(size)) => (format!("{} *{name}", e5r_decomp::c_type(size)), true), + (true, None) => (format!("{} {name}", e5r_decomp::c_type(width)), false), + }; + parameters.push(Param { + decl, + name, + floating: false, + pointer, + size: if pointer { 8 } else { width }, + fields, + stride, + stack: None, + }); + } + for n in 0..recovered.float_arguments { + let name = format!("farg{n}"); + // A `float` and a `double` arrive in the same register; only what the + // body reads says which was declared. + let width = recovered.float_widths.get(n).copied().unwrap_or(8); + let ty = if width == 4 { "float" } else { "double" }; + parameters.push(Param { + decl: format!("{ty} {name}"), + name, + floating: true, + pointer: false, + size: width, + fields: Vec::new(), + stride: None, + stack: None, + }); + } + // Arguments past the registers, which the caller left on the stack. Without + // these the signature is short and every call to such a function passes + // rubbish for the rest: `many_args` takes eight and was declared taking six. + for offset in &recovered.stack_arguments { + let name = e5r_decomp::slot_name(e5r_ir::ssa::Location { + space: e5r_ir::op::Space::Stack, + offset: *offset as u64, + size: 8, + }); + parameters.push(Param { + decl: format!("uint64_t {name}"), + name, + floating: false, + pointer: false, + size: 8, + fields: Vec::new(), + stride: None, + stack: Some(*offset), + }); + } + + Some(Prototype { + parameters, + // A function that leaves nothing behind returns nothing, and saying + // `void` is what makes the output read like the source. + returns: Some(match recovered.returns { + None => "void".to_string(), + Some(offset) if recovered.returns_float => { + let _ = offset; + "double".to_string() + } + Some(_) => "uint64_t".to_string(), + }), + definitions, + locals: BTreeMap::new(), + }) +} + +/// What was seen through one pointer: its fields, and the element size when +/// it walks an array of them. +type Layout = (Vec<(i64, u8)>, Option); + +/// The C definition of a structure the accesses imply. +fn structure(name: &str, fields: &[(i64, u8)]) -> String { + let mut out = format!("struct s_{name} {{"); + let mut at = 0i64; + for (offset, size) in fields { + // Padding, so every field lands where the code put it. + if *offset > at { + out.push_str(&format!(" uint8_t pad_{at:x}[{}];", offset - at)); + at = *offset; + } + if *offset < at { + continue; + } + let ty = match size { + 1 => "uint8_t", + 2 => "uint16_t", + 4 => "uint32_t", + _ => "uint64_t", + }; + out.push_str(&format!(" {ty} {};", e5r_decomp::field_name(*offset))); + at = offset + *size as i64; + } + out.push_str(" };"); + out +} + +/// What the debug information said about a function. +fn declared(p: &Program, f: &Function) -> Option { + let d = p.object.debug.as_ref()?; + let df = d.functions.get(&f.entry)?; + let parameters = df + .signature + .parameters + .iter() + .enumerate() + .map(|(n, (name, ty))| { + let name = name.clone().unwrap_or_else(|| format!("arg{n}")); + let resolved = d.types.get(d.types.resolve(*ty)); + Param { + decl: d.types.declare(*ty, &name), + name, + floating: matches!(resolved, Some(Type::Float { .. })), + pointer: matches!(resolved, Some(Type::Pointer(_))), + size: d.types.size_of(*ty).unwrap_or(0) as u8, + // The declared type already names the fields. + fields: Vec::new(), + stride: None, + stack: None, + } + }) + .collect(); + Some(Prototype { + parameters, + returns: df.signature.returns.map(|t| d.types.name_of(t)), + // The named types the signature mentions, defined before they are used + // so the output is a translation unit and not a fragment. + definitions: df + .signature + .returns + .into_iter() + .chain(df.signature.parameters.iter().map(|(_, t)| *t)) + .flat_map(|t| d.types.dependencies(t)) + .filter_map(|t| d.types.definition(t)) + .collect(), + locals: df + .locals + .iter() + .filter_map(|l| Some((l.frame_offset?, (l.name.clone(), d.types.name_of(l.ty))))) + .collect(), + }) +} diff --git a/crates/e5r-api/src/emulate.rs b/crates/e5r-api/src/emulate.rs new file mode 100644 index 0000000..6a44eb3 --- /dev/null +++ b/crates/e5r-api/src/emulate.rs @@ -0,0 +1,705 @@ +//! Running a function, and what that settles. +//! +//! The interpreter the lifters are tested against, offered as a tool. Set the +//! arguments, place whatever memory the code expects, run, and report what came +//! back, where every indirect branch went, and which system calls were taken. +//! It is the same machine the semantics gate uses, so what it says a function +//! computes is what that gate has been checking against real hardware. +//! +//! Three things this is for, and each one is a question static analysis cannot +//! answer on its own: +//! +//! * **A string that only exists at run time.** A decryptor is a small +//! self-contained routine, and running it and reading the buffer back is the +//! only way to see what it produces. [`Setup::inputs`] places the buffer, +//! [`Setup::watch`] reads it back. +//! * **A control flow that is computed.** [`Run::branches`] says where each +//! indirect branch actually went, and [`resolve`] turns a set of runs into +//! edges that carry the inputs they were observed under, which is the whole +//! of the evidence for them. +//! * **A jump table that was inferred.** [`confirm`] runs the switch once per +//! index and reports which of the recovered targets execution actually +//! reached, which turns a pattern match into a checked answer. +//! +//! Nothing here escapes the process. Memory is a map that starts as the image's +//! own bytes; a system call is answered by a stub that performs no action at +//! all, or refused by number; and the budget bounds the whole thing. See +//! [`crate::kernel`] for exactly what a stub is allowed to do. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +use e5r_analysis::{Function, JumpTable, Program}; +use e5r_core::{Addr, Arch, MemoryMap}; +use e5r_ir::{Machine, Op, Stop}; + +use crate::kernel::{self, Answer, Call, Kernel, Output, Session}; + +/// The most bytes one [`Span`] captures. A span longer than this is truncated +/// rather than allowed to turn a typo into an allocation. +pub const MAX_WATCH: u64 = 1 << 22; + +/// Why a run ended. +/// +/// A superset of the interpreter's own [`Stop`]: emulation here can also end +/// because the program asked to exit, because a system call had no stub, or +/// because the caller asked to stop at an indirect branch, and none of those +/// has an interpreter word for it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Ending { + /// The function returned to its caller. + Returned, + /// The program called `exit`, with this status. + Exited(u64), + /// The operation budget ran out, which is how a loop that does not + /// terminate is reported rather than hung on. + Budget, + /// An instruction the lifter does not model was reached. + Unimplemented(Addr), + /// Control went somewhere with no code. + NoCode(Addr), + /// A call was reached and calls were not being followed that deep. + Call(Addr), + /// Division by zero. + DivideByZero(Addr), + /// A system call with no stub. Reported by number, never answered with a + /// plausible value. + Syscall { + /// The instruction that made it. + at: Addr, + /// The number in the architecture's system call register. + number: u64, + /// What that number is called, where the table knows a name. + name: Option<&'static str>, + }, + /// The indirect transfer [`Setup::stop_at_indirect`] named resolved, and + /// the run stopped there rather than going on into the target. + Indirect { + /// The branching instruction. + at: Addr, + /// Where it went. + to: Addr, + }, +} + +impl Ending { + /// True when the run ended of its own accord rather than being cut off. + pub fn finished(&self) -> bool { + matches!(self, Ending::Returned | Ending::Exited(_)) + } + + /// The nearest thing the interpreter has a word for. + /// + /// This exists so [`Run::stop`] can go on meaning what it meant. It loses + /// information on purpose and [`Run::ending`] is the answer to trust: an + /// exit reads as a return, a refused system call reads as an instruction + /// the model does not have, and a deliberate stop at an indirect branch + /// reads as a transfer that was not followed. + pub fn as_stop(&self) -> Stop { + match self { + Ending::Returned | Ending::Exited(_) => Stop::Returned, + Ending::Budget => Stop::Budget, + Ending::Unimplemented(a) => Stop::Unimplemented(*a), + Ending::NoCode(a) => Stop::NoCode(*a), + Ending::Call(a) => Stop::Call(*a), + Ending::DivideByZero(a) => Stop::DivideByZero(*a), + Ending::Syscall { at, .. } => Stop::Unimplemented(*at), + Ending::Indirect { to, .. } => Stop::Call(*to), + } + } +} + +impl fmt::Display for Ending { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Ending::Returned => write!(f, "returned"), + Ending::Exited(s) => write!(f, "exited with status {s}"), + Ending::Budget => write!(f, "ran out of budget"), + Ending::Unimplemented(a) => write!(f, "unmodelled instruction at {a}"), + Ending::NoCode(a) => write!(f, "no code at {a}"), + Ending::Call(a) => write!(f, "call to {a}, not followed"), + Ending::DivideByZero(a) => write!(f, "divide by zero at {a}"), + Ending::Syscall { at, number, name } => match name { + Some(n) => write!(f, "system call {n} ({number}) at {at} has no stub"), + None => write!(f, "system call {number} at {at} has no stub"), + }, + Ending::Indirect { at, to } => write!(f, "indirect branch at {at} went to {to}"), + } + } +} + +/// Where an indirect transfer actually went. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Indirect { + /// The branching instruction. + pub at: Addr, + /// Where control went. + pub to: Addr, + /// True when it was a call rather than a branch. + pub call: bool, +} + +/// Bytes to put in the machine's memory before the run. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Place { + /// Where they go. + pub at: u64, + /// The bytes. + pub bytes: Vec, +} + +/// A span of memory to capture when the run stops. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Span { + /// Where it starts. + pub at: u64, + /// How long it is, capped at [`MAX_WATCH`]. + pub len: u64, +} + +/// A captured span, as it stood when the run stopped. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Region { + /// Where it starts. + pub at: u64, + /// The bytes. + pub bytes: Vec, +} + +/// What a run did. +#[derive(Debug, Clone)] +pub struct Run { + /// Why it stopped, as the interpreter would put it. + /// + /// [`Ending::as_stop`] explains what this loses; [`Run::ending`] is the + /// field to read when the difference matters. + pub stop: Stop, + /// Why it stopped. + pub ending: Ending, + /// What the result register held when it did. + pub result: u64, + /// Machine instructions executed. + pub insns: u64, + /// IR operations executed. + pub ops: u64, + /// Instructions the lifter did not model, in the order they were reached. + pub unlifted: Vec, + /// Memory the run wrote, by address. + pub written: BTreeMap, + /// The spans [`Setup::watch`] asked for, in that order. + pub regions: Vec, + /// Every system call reached, stubbed or refused, in order. + /// + /// This is how a run that took a stub is told apart from one that did not. + /// A `read` that returned zero bytes because no file exists is a different + /// answer from a `read` that was never made, and both leave the same + /// registers behind. + pub syscalls: Vec, + /// Bytes a stubbed `write` was handed. Nothing was written anywhere. + pub output: Vec, + /// Where each indirect branch and indirect call went, in order. + pub branches: Vec, +} + +impl Run { + /// True when the function returned with nothing unmodelled and no system + /// call stubbed. + /// + /// A stub is a decision about what the kernel would have done, so a run + /// that took one is not a clean reading of the code alone even when it + /// returned. [`Run::ending`] and [`Run::syscalls`] say which of the three + /// it was. + pub fn clean(&self) -> bool { + self.ending == Ending::Returned && self.unlifted.is_empty() && self.syscalls.is_empty() + } + + /// True when any system call was answered by a stub. + pub fn stubbed(&self) -> bool { + self.syscalls.iter().any(|c| c.stubbed) + } + + /// A watched span's bytes. + pub fn region(&self, at: u64) -> Option<&[u8]> { + self.regions + .iter() + .find(|r| r.at == at) + .map(|r| r.bytes.as_slice()) + } + + /// The NUL-terminated text at `at`, when a watched span covers it. + /// + /// `None` rather than replacement characters when the bytes are not UTF-8: + /// a decryption that came out wrong must not be laundered into something + /// that looks like a string. + pub fn text(&self, at: u64) -> Option { + let r = self + .regions + .iter() + .find(|r| r.at <= at && at < r.at + r.bytes.len() as u64)?; + let from = &r.bytes[(at - r.at) as usize..]; + let end = from.iter().position(|b| *b == 0).unwrap_or(from.len()); + String::from_utf8(from[..end].to_vec()).ok() + } + + /// Everything a stubbed `write` was handed, concatenated. + pub fn written_out(&self) -> Vec { + self.output.iter().flat_map(|o| o.bytes.clone()).collect() + } +} + +/// How to set a run up. +#[derive(Debug, Clone)] +pub struct Setup { + /// Integer arguments, in the convention's order. + pub arguments: Vec, + /// How deep to follow calls. + pub depth: u32, + /// How many IR operations to allow. + pub budget: u64, + /// Where the stack goes. + pub stack: u64, + /// Bytes to place before the run: a buffer to decrypt into, a structure the + /// code expects to find, a ciphertext that lives somewhere else. + pub inputs: Vec, + /// Spans to capture when the run stops. + pub watch: Vec, + /// What the system call stubs answer with. + pub kernel: Kernel, + /// Stop when the indirect transfer at this address resolves, rather than + /// going on into the target. + /// + /// This is what makes confirming a jump table cheap: the target is the + /// answer, and running the case body afterwards can only add ways to fail. + pub stop_at_indirect: Option, +} + +impl Default for Setup { + fn default() -> Self { + Setup { + arguments: Vec::new(), + depth: 64, + budget: 1 << 22, + // Away from the image, so a stray read is visibly unmapped rather + // than quietly reading code. + stack: 0x7fff_0000, + inputs: Vec::new(), + watch: Vec::new(), + kernel: Kernel::default(), + stop_at_indirect: None, + } + } +} + +/// Run one function. +pub fn run(p: &Program, f: &Function, setup: &Setup) -> Run { + let arch = &p.object.arch; + let abi = e5r_ir::abi::of(arch); + let mut m = Machine::over(&p.object.memory); + m.budget = setup.budget; + // Real storage under the stack pointer, so a spill and its reload agree. + m.write_mem(setup.stack - 0x8000, &[0u8; 0x10000]); + + let registers = abi.integer_arguments.len(); + for (i, a) in setup.arguments.iter().enumerate().take(registers) { + m.set_reg(abi.integer_arguments[i], 8, *a); + } + m.set_reg(abi.stack_pointer, 8, setup.stack); + // Arguments past the registers go on the stack, above the return address + // on the architectures that push one. + let reserved = match arch { + Arch::X86_64 => 8, + _ => 0, + }; + for (n, a) in setup.arguments.iter().skip(registers).enumerate() { + m.write_mem(setup.stack + reserved + n as u64 * 8, &a.to_le_bytes()); + } + // Somewhere that is not code, so the outermost return stops the run. + const SENTINEL: u64 = 0xdead_0000; + match arch { + Arch::X86_64 => m.write_mem(setup.stack, &SENTINEL.to_le_bytes()), + _ => m.set_reg(e5r_ir::lift::aarch64::gpr_offset(30), 8, SENTINEL), + } + // The caller's own bytes go in last, so they win over the stack fill if a + // caller deliberately places something inside the frame. + for place in &setup.inputs { + m.write_mem(place.at, &place.bytes); + } + + let before = m.written().clone(); + let mut session = Session::new(setup.kernel.clone()); + let walk = walk(&mut m, &mut session, &p.object.memory, arch, setup, f.entry); + + let result = m.reg(abi.results.first().copied().unwrap_or(0), 8); + let regions = setup + .watch + .iter() + .map(|s| { + let len = s.len.min(MAX_WATCH); + let bytes = (0..len).map(|i| m.read_mem(s.at + i, 1) as u8).collect(); + Region { at: s.at, bytes } + }) + .collect(); + let written = m + .written() + .iter() + .filter(|(a, v)| before.get(a) != Some(v)) + .map(|(a, v)| (*a, *v)) + .collect(); + + Run { + stop: walk.ending.as_stop(), + ending: walk.ending, + result, + insns: walk.insns, + ops: walk.ops, + unlifted: walk.unlifted, + written, + regions, + syscalls: walk.syscalls, + output: session.output, + branches: walk.branches, + } +} + +/// What one walk of the code produced. +struct Walk { + ending: Ending, + insns: u64, + ops: u64, + unlifted: Vec, + syscalls: Vec, + branches: Vec, +} + +/// Decode, lift, interpret, one instruction at a time. +/// +/// This is the interpreter's own loop with two things added: a system call goes +/// to the stubs instead of falling through with an undefined result, and an +/// indirect transfer records where it went. Neither belongs in the IR crate, +/// which has no business knowing what Linux is. +fn walk( + m: &mut Machine<'_>, + session: &mut Session, + image: &MemoryMap, + arch: &Arch, + setup: &Setup, + entry: Addr, +) -> Walk { + let mut w = Walk { + ending: Ending::Budget, + insns: 0, + ops: 0, + unlifted: Vec::new(), + syscalls: Vec::new(), + branches: Vec::new(), + }; + let mut pc = entry; + let mut budget = setup.budget; + let mut frames = 0u32; + + loop { + if budget == 0 { + w.ending = Ending::Budget; + return w; + } + let Some(insn) = e5r_ir::func::decode_at(image, arch, pc) else { + w.ending = Ending::NoCode(pc); + return w; + }; + let lifted = e5r_ir::lift::lift(arch, &insn); + if !lifted.complete { + w.unlifted.push(pc); + } + + m.end_instruction(); + let mut next = insn.next(); + let gate = gate(arch, insn.mnemonic); + + for ir in &lifted.ops { + budget = budget.saturating_sub(1); + w.ops += 1; + let indirect = matches!(ir.op, Op::BranchInd | Op::CallInd); + match e5r_ir::step(m, ir) { + e5r_ir::Step::Next => {} + e5r_ir::Step::Jump(t) => { + if indirect { + w.branches.push(Indirect { + at: ir.addr, + to: t, + call: false, + }); + if setup.stop_at_indirect == Some(ir.addr) { + w.insns += 1; + w.ending = Ending::Indirect { at: ir.addr, to: t }; + return w; + } + } + next = t; + break; + } + e5r_ir::Step::Call(target) => { + if indirect { + w.branches.push(Indirect { + at: ir.addr, + to: target, + call: true, + }); + if setup.stop_at_indirect == Some(ir.addr) { + w.insns += 1; + w.ending = Ending::Indirect { + at: ir.addr, + to: target, + }; + return w; + } + } + if frames >= setup.depth { + w.insns += 1; + w.ending = Ending::Call(target); + return w; + } + frames += 1; + next = target; + break; + } + e5r_ir::Step::Leave(target) => { + if frames == 0 { + w.insns += 1; + w.ending = Ending::Returned; + return w; + } + frames -= 1; + next = target; + break; + } + e5r_ir::Step::Halt(s) => { + w.insns += 1; + w.ending = from_stop(s); + return w; + } + } + } + w.insns += 1; + + // The lifting of a system call says what the instruction did to the + // register file and stops there, because what the kernel does is not + // knowable from the instruction. That is the hole the stubs fill, and + // it has to be filled here rather than in the lifter: the lifter is a + // statement about the architecture and this is a statement about an + // operating system. + match gate { + None => {} + Some(Gate::Other) => { + // A hypervisor call or a 32-bit gate. It is a system call by + // flow and nothing here models it, so the run stops rather + // than walking on into the next instruction as if it had run. + w.ending = Ending::Unimplemented(pc); + return w; + } + Some(Gate::Linux) => { + let mut call = Session::observe(m, arch, pc); + match session.serve(m, arch, &call) { + Answer::Returned(v) => { + call.stubbed = true; + call.returned = Some(v); + m.set_reg(kernel::result_register(arch), 8, v); + w.syscalls.push(call); + } + Answer::Exited(status) => { + call.stubbed = true; + w.syscalls.push(call); + w.ending = Ending::Exited(status); + return w; + } + Answer::Refused => { + let ending = Ending::Syscall { + at: pc, + number: call.number, + name: call.name, + }; + w.syscalls.push(call); + w.ending = ending; + return w; + } + } + } + } + + pc = next; + } +} + +/// What the interpreter's own stop reasons become. +fn from_stop(s: Stop) -> Ending { + match s { + Stop::Returned => Ending::Returned, + Stop::Budget => Ending::Budget, + Stop::Unimplemented(a) => Ending::Unimplemented(a), + Stop::NoCode(a) => Ending::NoCode(a), + Stop::Call(a) => Ending::Call(a), + Stop::DivideByZero(a) => Ending::DivideByZero(a), + } +} + +/// Which kind of gate into the kernel an instruction is, if any. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Gate { + /// The Linux system call instruction for this architecture. + Linux, + /// A gate by flow that is not one: a hypervisor call, a secure monitor + /// call, or the 32-bit entries on x86. + Other, +} + +fn gate(arch: &Arch, mnemonic: &str) -> Option { + match arch { + Arch::X86_64 => match mnemonic { + "syscall" => Some(Gate::Linux), + "int" | "sysenter" => Some(Gate::Other), + _ => None, + }, + Arch::AArch64 => match mnemonic { + "svc" => Some(Gate::Linux), + "hvc" | "smc" => Some(Gate::Other), + _ => None, + }, + _ => None, + } +} + +/// An indirect edge an execution settled, and the evidence for it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Resolved { + /// The branching instruction. + pub at: Addr, + /// Where it went. + pub to: Addr, + /// True when it was a call rather than a branch. + pub call: bool, + /// Every argument vector a run took this edge under. + /// + /// This is the whole of the evidence and it is deliberately not thrown + /// away. An executed edge is a statement that control went here for these + /// inputs, which is a weaker and more honest thing than a claim that the + /// branch has this target: the edges an obfuscator hides are exactly the + /// ones whose target depends on something. + pub under: Vec>, +} + +/// Run a function once per argument vector and collect the indirect edges. +/// +/// This is how a flattened or opaquely-predicated control flow is opened up: +/// the targets are computed, so computing them is the only way to see them, and +/// the set that comes back is the set those inputs reached and no larger. +pub fn resolve(p: &Program, f: &Function, setup: &Setup, inputs: &[Vec]) -> Vec { + let mut found: BTreeMap<(Addr, Addr, bool), Vec>> = BTreeMap::new(); + for args in inputs { + let s = Setup { + arguments: args.clone(), + ..setup.clone() + }; + let run = run(p, f, &s); + let mut seen = BTreeSet::new(); + for b in &run.branches { + if seen.insert((b.at, b.to, b.call)) { + found + .entry((b.at, b.to, b.call)) + .or_default() + .push(args.clone()); + } + } + } + found + .into_iter() + .map(|((at, to, call), under)| Resolved { + at, + to, + call, + under, + }) + .collect() +} + +/// What running a switch said about a recovered jump table. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Confirmed { + /// The indirect branch the table resolves. + pub at: Addr, + /// Where the branch went, per index that reached it. + pub taken: BTreeMap, + /// Indices whose run never reached the branch, which is what a guard + /// rejecting an out-of-range index looks like from here. + pub rejected: Vec, + /// Targets execution reached, sorted. + pub reached: BTreeSet, + /// Targets the table claims that no index reached. + /// + /// A table sized by scanning rather than by the guard collects these: the + /// entries past the real end are plausible addresses that nothing selects. + pub unreached: Vec, + /// Targets execution reached that the table does not claim, which would + /// mean recovery stopped short. + pub extra: Vec, +} + +impl Confirmed { + /// True when execution reached every target the table claims and no other. + pub fn agrees(&self) -> bool { + self.unreached.is_empty() && self.extra.is_empty() + } +} + +/// Run a switch once per index and report what the indirect branch did. +/// +/// `slot` is which integer argument carries the switch value; the rest of +/// [`Setup::arguments`] is passed through unchanged, so a function that takes +/// the value alongside other arguments still runs. +/// +/// Recovery bounds a table by the compare that guards the switch, and where +/// that compare cannot be found every byte of a compact table yields a +/// plausible target. Running the branch is the check that tells the two apart: +/// a target nothing selects is reported in [`Confirmed::unreached`] rather than +/// believed. +pub fn confirm( + p: &Program, + f: &Function, + table: &JumpTable, + setup: &Setup, + slot: usize, + indices: &[u64], +) -> Confirmed { + let mut taken = BTreeMap::new(); + let mut rejected = Vec::new(); + let mut reached = BTreeSet::new(); + + for i in indices { + let mut arguments = setup.arguments.clone(); + if arguments.len() <= slot { + arguments.resize(slot + 1, 0); + } + arguments[slot] = *i; + let s = Setup { + arguments, + stop_at_indirect: Some(table.at), + ..setup.clone() + }; + let run = run(p, f, &s); + match run.branches.iter().find(|b| b.at == table.at) { + Some(b) => { + taken.insert(*i, b.to); + reached.insert(b.to); + } + None => rejected.push(*i), + } + } + + let claimed: BTreeSet = table.targets.iter().copied().collect(); + Confirmed { + at: table.at, + taken, + rejected, + unreached: claimed.difference(&reached).copied().collect(), + extra: reached.difference(&claimed).copied().collect(), + reached, + } +} diff --git a/crates/e5r-api/src/kernel.rs b/crates/e5r-api/src/kernel.rs new file mode 100644 index 0000000..eaaf9ce --- /dev/null +++ b/crates/e5r-api/src/kernel.rs @@ -0,0 +1,459 @@ +//! System call stubs: what the kernel would have answered, worked out here. +//! +//! # Safety +//! +//! Nothing in this module opens a file, creates a socket, spawns a process, +//! reads a clock, or touches anything at all outside the interpreter's own +//! memory. A stub is an answer, not an action. `write` writes nowhere: it +//! copies the bytes out of the interpreter's memory and records them. `read` +//! hands back bytes the caller put in [`Kernel::input`] and nothing else, and +//! end of file when those run out, because no descriptor exists to read more +//! from. `mmap` and `brk` hand out addresses inside a region the interpreter +//! owns. The clock calls report a fixed second the caller chose. That is the +//! entire list. +//! +//! # Why a call with no stub is refused +//! +//! A stub is a decision about what the kernel would have done, and a wrong one +//! is easy to write and nearly invisible afterwards: a plausible return value +//! propagates through the rest of the run and comes out the other end as an +//! answer nobody doubts. So a number this module does not model is refused by +//! number, the run stops there, and [`Call::returned`] is `None`. Every call +//! that *was* served is recorded too, so a run that took a stub is never +//! mistaken for a run that did not. + +use e5r_core::{Addr, Arch}; +use e5r_ir::Machine; + +/// The most bytes one `read` or `write` moves. A longer transfer is answered +/// short, which is a legal answer for both calls and is what keeps a wild +/// length from trying to allocate the address space. +pub const MAX_TRANSFER: u64 = 1 << 20; + +/// `-ENOMEM`, as the kernel returns it: a small negative number in the result +/// register. +const ENOMEM: u64 = (-12i64) as u64; + +/// One system call the run reached. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Call { + /// The instruction that made it. + pub at: Addr, + /// The number in the architecture's system call register. + pub number: u64, + /// What that number is called, where this table knows a name. A refused + /// call is named too when the name is known: which call was refused is the + /// point of refusing rather than guessing. + pub name: Option<&'static str>, + /// The six argument registers, in the system call convention's order. + pub args: [u64; 6], + /// True when this module has a stub for the number. + /// + /// `exit` has a stub and does not return, so `returned` is `None` for it + /// as well as for a refusal; this is what tells the two apart. + pub stubbed: bool, + /// What the stub answered, or `None` when the call did not return: either + /// it was refused and the run stopped, or it was `exit`. + pub returned: Option, +} + +impl Call { + /// True when a stub answered this call rather than the run stopping on it. + pub fn served(&self) -> bool { + self.stubbed + } + + /// How the call reads in a report. + pub fn describe(&self) -> String { + let name = self.name.unwrap_or("?"); + match (self.stubbed, self.returned) { + (true, Some(v)) => format!("{name}({}) = {:#x}", self.number, v), + (true, None) => format!("{name}({}) did not return", self.number), + (false, _) => format!("{name}({}) refused: no stub", self.number), + } + } +} + +/// Bytes a stubbed `write` was handed. +/// +/// Nothing was written anywhere. This is the record of what the program tried +/// to send, which for a decryptor or a packer is usually the answer being +/// looked for. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Output { + /// The descriptor number the program passed, which was not consulted. + pub fd: u64, + /// The bytes it asked to send. + pub bytes: Vec, +} + +/// What the stubs answer with. +#[derive(Debug, Clone)] +pub struct Kernel { + /// Bytes a stubbed `read` hands back, consumed from the front across the + /// whole run. When they run out `read` returns zero, which is end of file. + pub input: Vec, + /// Where the region `mmap` and `brk` hand addresses out of starts. Away + /// from the image and from the stack, so a pointer that came from one of + /// them is recognizable on sight. + pub arena: u64, + /// How large that region is. It bounds `mmap` and `brk` together, so a + /// program that asks for the world is told no rather than believed. + pub arena_size: u64, + /// The second every clock call reports. Fixed, because a run that reads + /// the clock has to give the same answer twice or it is not evidence. + pub epoch: u64, +} + +impl Default for Kernel { + fn default() -> Self { + Kernel { + input: Vec::new(), + arena: 0x5000_0000, + arena_size: 1 << 24, + // 2020-09-13T12:26:40Z. Any fixed second would do; this one is + // recent enough that a program checking for an expiry passes. + epoch: 1_600_000_000, + } + } +} + +/// What a stub decided. +pub(crate) enum Answer { + /// The call returns this in the result register. + Returned(u64), + /// The program asked to stop, with this status. + Exited(u64), + /// There is no stub for this number. The run stops. + Refused, +} + +/// The calls this module models. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Which { + Read, + Write, + Mmap, + Munmap, + Brk, + Exit, + Time, + GetTimeOfDay, + ClockGetTime, +} + +/// The stubs' state for one run. +pub(crate) struct Session { + k: Kernel, + /// How much of `input` a `read` has already handed over. + taken: usize, + /// The next address `mmap` will hand out. + map_next: u64, + /// The program break. + brk: u64, + /// Everything the stubs recorded. + pub(crate) output: Vec, +} + +impl Session { + pub(crate) fn new(k: Kernel) -> Session { + // The break grows up from the bottom of the arena and mappings come + // out of the rest, so neither can walk into the other. + let split = k.arena + k.arena_size / 4; + Session { + taken: 0, + map_next: split, + brk: k.arena, + output: Vec::new(), + k, + } + } + + /// Read the call's registers, without deciding anything about it yet. + pub(crate) fn observe(m: &Machine<'_>, arch: &Arch, at: Addr) -> Call { + let (number_reg, arg_regs) = convention(arch); + let number = m.reg(number_reg, 8); + let mut args = [0u64; 6]; + for (i, r) in arg_regs.iter().enumerate() { + args[i] = m.reg(*r, 8); + } + Call { + at, + number, + name: name_of(arch, number), + args, + stubbed: false, + returned: None, + } + } + + /// Answer one call, or refuse it. + pub(crate) fn serve(&mut self, m: &mut Machine<'_>, arch: &Arch, call: &Call) -> Answer { + let Some(which) = which(arch, call.number) else { + return Answer::Refused; + }; + let a = call.args; + match which { + // No descriptor is opened or consulted; the caller's bytes are the + // only thing there is to read. + Which::Read => { + let want = a[2].min(MAX_TRANSFER) as usize; + let have = self.k.input.len().saturating_sub(self.taken); + let n = want.min(have); + if n > 0 { + let bytes = self.k.input[self.taken..self.taken + n].to_vec(); + self.taken += n; + m.write_mem(a[1], &bytes); + } + Answer::Returned(n as u64) + } + Which::Write => { + let n = a[2].min(MAX_TRANSFER); + let mut bytes = Vec::with_capacity(n as usize); + for i in 0..n { + bytes.push(m.read_mem(a[1].wrapping_add(i), 1) as u8); + } + self.output.push(Output { fd: a[0], bytes }); + Answer::Returned(n) + } + // A file-backed mapping would be the contents of a file, and there + // is no file: that one is refused rather than answered with zeros. + Which::Mmap => { + let anonymous = a[4] == u64::MAX || a[4] as i64 == -1; + if !anonymous { + return Answer::Refused; + } + let len = a[1].next_multiple_of(0x1000); + let end = self.k.arena + self.k.arena_size; + if a[1] == 0 || len > end.saturating_sub(self.map_next) { + return Answer::Returned(ENOMEM); + } + let at = self.map_next; + self.map_next += len; + // Nothing is written: the interpreter reads memory nobody + // wrote as zero, which is exactly what an anonymous mapping + // gives, so materializing the pages would cost memory and + // change no answer. + Answer::Returned(at) + } + // The bytes stay readable afterwards, which a real unmap would not + // allow. An answer, not an action: a run that reads back through a + // pointer it unmapped gets the old bytes here and a fault there. + Which::Munmap => Answer::Returned(0), + Which::Brk => { + let limit = self.k.arena + self.k.arena_size / 4; + if a[0] >= self.k.arena && a[0] <= limit { + self.brk = a[0]; + } + // Linux answers a request it cannot meet with the break it + // still has, not with an error. + Answer::Returned(self.brk) + } + Which::Exit => Answer::Exited(a[0]), + Which::Time => { + if a[0] != 0 { + m.write_mem(a[0], &self.k.epoch.to_le_bytes()); + } + Answer::Returned(self.k.epoch) + } + // Both of these fill a two-word structure, and both architectures + // here are 64-bit, so the layout is the same eight bytes twice. + // Every clock reports the same fixed second, monotonic included. + Which::GetTimeOfDay | Which::ClockGetTime => { + let out = if which == Which::ClockGetTime { + a[1] + } else { + a[0] + }; + if out != 0 { + m.write_mem(out, &self.k.epoch.to_le_bytes()); + m.write_mem(out + 8, &0u64.to_le_bytes()); + } + Answer::Returned(0) + } + } + } +} + +/// Where a system call's number and arguments live, per architecture. +/// +/// This is the system call convention, which is not the C one: x86-64 passes +/// the fourth argument in `r10` because `syscall` destroys `rcx`, and AArch64 +/// puts the number in `x8` rather than in a register the C convention uses. +fn convention(arch: &Arch) -> (u64, [u64; 6]) { + match arch { + Arch::X86_64 => { + let r = e5r_ir::lift::x86::gpr_offset; + // rax, then rdi rsi rdx r10 r8 r9. + (r(0), [r(7), r(6), r(2), r(10), r(8), r(9)]) + } + _ => { + let r = e5r_ir::lift::aarch64::gpr_offset; + (r(8), [r(0), r(1), r(2), r(3), r(4), r(5)]) + } + } +} + +/// The register a system call's result comes back in. +pub(crate) fn result_register(arch: &Arch) -> u64 { + match arch { + Arch::X86_64 => e5r_ir::lift::x86::gpr_offset(0), + _ => e5r_ir::lift::aarch64::gpr_offset(0), + } +} + +/// Which modelled call a number is, if any. +fn which(arch: &Arch, number: u64) -> Option { + match arch { + Arch::X86_64 => Some(match number { + 0 => Which::Read, + 1 => Which::Write, + 9 => Which::Mmap, + 11 => Which::Munmap, + 12 => Which::Brk, + 60 | 231 => Which::Exit, + 96 => Which::GetTimeOfDay, + 201 => Which::Time, + 228 => Which::ClockGetTime, + _ => return None, + }), + // The generic Linux numbering, which AArch64 uses unchanged. There is + // no `time` call in it: userland reads the clock through + // `clock_gettime` or the vDSO. + Arch::AArch64 => Some(match number { + 63 => Which::Read, + 64 => Which::Write, + 93 | 94 => Which::Exit, + 113 => Which::ClockGetTime, + 169 => Which::GetTimeOfDay, + 214 => Which::Brk, + 215 => Which::Munmap, + 222 => Which::Mmap, + _ => return None, + }), + // Another architecture's numbering is a different table, and guessing + // at it would be exactly the invented answer this module refuses. + _ => None, + } +} + +/// What a number is called, for the report. +/// +/// The modelled calls plus the ones most likely to be the reason a run +/// stopped: a refusal that says `execve(59)` tells an analyst what the code +/// was about to do, and one that says only `59` does not. +fn name_of(arch: &Arch, number: u64) -> Option<&'static str> { + let table: &[(u64, &'static str)] = match arch { + Arch::X86_64 => &[ + (0, "read"), + (1, "write"), + (2, "open"), + (3, "close"), + (5, "fstat"), + (8, "lseek"), + (9, "mmap"), + (10, "mprotect"), + (11, "munmap"), + (12, "brk"), + (13, "rt_sigaction"), + (16, "ioctl"), + (19, "readv"), + (20, "writev"), + (21, "access"), + (35, "nanosleep"), + (39, "getpid"), + (41, "socket"), + (42, "connect"), + (56, "clone"), + (57, "fork"), + (59, "execve"), + (60, "exit"), + (62, "kill"), + (63, "uname"), + (87, "unlink"), + (96, "gettimeofday"), + (101, "ptrace"), + (158, "arch_prctl"), + (165, "mount"), + (201, "time"), + (202, "futex"), + (228, "clock_gettime"), + (231, "exit_group"), + (257, "openat"), + ], + Arch::AArch64 => &[ + (29, "ioctl"), + (35, "unlinkat"), + (48, "faccessat"), + (56, "openat"), + (57, "close"), + (59, "pipe2"), + (62, "lseek"), + (63, "read"), + (64, "write"), + (65, "readv"), + (66, "writev"), + (80, "fstat"), + (93, "exit"), + (94, "exit_group"), + (98, "futex"), + (101, "nanosleep"), + (113, "clock_gettime"), + (117, "ptrace"), + (129, "kill"), + (134, "rt_sigaction"), + (160, "uname"), + (169, "gettimeofday"), + (172, "getpid"), + (198, "socket"), + (203, "connect"), + (214, "brk"), + (215, "munmap"), + (220, "clone"), + (221, "execve"), + (222, "mmap"), + (226, "mprotect"), + (40, "mount"), + ], + _ => &[], + }; + table + .iter() + .find(|(n, _)| *n == number) + .map(|(_, name)| *name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_two_numberings_are_not_the_same_table() { + // Number 1 is `write` on x86-64 and something unmodelled on AArch64. + // Getting these crossed would answer the wrong call silently, which is + // the failure this whole module is arranged against. + assert_eq!(which(&Arch::X86_64, 1), Some(Which::Write)); + assert_eq!(which(&Arch::AArch64, 1), None); + assert_eq!(which(&Arch::AArch64, 64), Some(Which::Write)); + assert_eq!(which(&Arch::X86_64, 64), None); + } + + #[test] + fn an_unmodelled_number_is_refused_by_number() { + assert_eq!(which(&Arch::X86_64, 59), None); + assert_eq!(name_of(&Arch::X86_64, 59), Some("execve")); + assert_eq!(which(&Arch::AArch64, 221), None); + assert_eq!(name_of(&Arch::AArch64, 221), Some("execve")); + } + + #[test] + fn every_modelled_number_has_a_name() { + for arch in [Arch::X86_64, Arch::AArch64] { + for n in 0..300u64 { + if which(&arch, n).is_some() { + assert!(name_of(&arch, n).is_some(), "{arch:?} {n} has no name"); + } + } + } + } +} diff --git a/crates/e5r-api/src/lib.rs b/crates/e5r-api/src/lib.rs new file mode 100644 index 0000000..424728b --- /dev/null +++ b/crates/e5r-api/src/lib.rs @@ -0,0 +1,42 @@ +//! The library surface the command line and the servers share. +//! +//! Everything a caller needs to go from a loaded program to an answer, without +//! reaching into the crate that happens to implement it. The point is that the +//! CLI and the tests take the same path: a difference between +//! what a test checks and what a user gets is a difference that will be found +//! by a user. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod classes; +pub mod dataflow; +pub mod decompile; +pub mod emulate; +pub mod kernel; +mod msvc; +pub mod prototypes; +pub mod query; +pub mod relocate; +pub mod shapes; +pub mod signatures; +pub mod vtables; + +pub use classes::{ + Abi, BaseClass, Class, Classes, Member, Role, Rtti, This, TypeInfoKind, classes, classes_with, +}; +pub use dataflow::{ArgumentFact, Basis, CallSite, Source, Verdict, call_sites, call_sites_in}; +pub use decompile::{ + Decompiled, Unit, decompile_function, decompile_function_with, decompile_program, + decompile_program_with, +}; +pub use emulate::{ + Confirmed, Ending, Indirect, Place, Region, Resolved, Run, Setup, Span, confirm, resolve, +}; +pub use kernel::{Call, Kernel, Output}; +pub use prototypes::{Recovered, prototypes}; +pub use query::{Answer, Entity, Fact, Query, Row, Want}; +pub use relocate::apply_relative_relocations; +pub use shapes::{Pointer, shapes_of}; +pub use signatures::{Identified, anchor_of, collect_signatures, identify}; +pub use vtables::{VTable, vtables}; diff --git a/crates/e5r-api/src/msvc.rs b/crates/e5r-api/src/msvc.rs new file mode 100644 index 0000000..6eaa165 --- /dev/null +++ b/crates/e5r-api/src/msvc.rs @@ -0,0 +1,236 @@ +//! Microsoft C++ run-time type information. +//! +//! A different layout for the same facts, and also published. The word before +//! a vftable's first slot points at a `RTTICompleteObjectLocator`, which names +//! the `TypeDescriptor` holding the class's decorated name and the +//! `ClassHierarchyDescriptor` holding its bases. Everything inside is a +//! relative virtual address: an offset from the image base, not a pointer, so +//! a PE keeps its type information without a single relocation. +//! +//! The base array is not a list of direct bases. It is a preorder walk of the +//! whole lattice with the class itself first, and each entry says how many +//! entries after it are underneath it. Walking it with those counts is what +//! turns it back into the direct bases the source declared, and it is the only +//! reason `Join : ViaA, ViaB` does not come back with a `Root` it never +//! declared. + +use std::collections::BTreeMap; + +use e5r_core::{Addr, Evidence}; + +use e5r_analysis::Program; + +use crate::classes::{BaseClass, Basis, TypeInfo, is_code, read_int, word, word_size}; +use crate::vtables::VTable; + +/// What the Microsoft type information in an image says. +pub(crate) struct Found { + /// The virtual function tables, each with the type information it names. + pub tables: Vec, + /// One entry per `TypeDescriptor`, which is one per class. + pub infos: BTreeMap, +} + +/// Read every virtual table and every class the image's type information names. +pub(crate) fn read(p: &Program) -> Found { + let ws = word_size(p) as i64; + let mut tables = Vec::new(); + let mut infos: BTreeMap = BTreeMap::new(); + + for section in &p.object.sections { + // Where a compiler puts tables. `.text` is excluded: a code byte + // sequence that reads as a pointer to a locator is a coincidence, and + // one that does is not a table. + if section.exec || section.range.is_empty() { + continue; + } + let mut at = section.range.start(); + while at.get() + 2 * ws as u64 <= section.range.end().get() { + let step = at; + at = at.wrapping_offset(ws); + let Some(pointer) = word(p, step) else { + continue; + }; + let Some(locator) = locator(p, Addr(pointer)) else { + continue; + }; + let entry = step.wrapping_offset(ws); + let mut methods = Vec::new(); + let mut slot = entry; + while let Some(target) = word(p, slot) { + if !is_code(p, Addr(target)) { + break; + } + methods.push(Addr(target)); + slot = slot.wrapping_offset(ws); + } + if methods.is_empty() { + continue; + } + at = slot; + + if let Some(info) = describe(p, &locator) + && !infos.contains_key(&locator.descriptor) + { + infos.insert(locator.descriptor, info); + } + tables.push(VTable { + addr: step, + entry, + class: None, + // The locator says how far into the complete object this + // table's subobject starts, which is the negative of what the + // Itanium ABI writes down in the same place. + offset_to_top: -(locator.offset as i64), + typeinfo: Some(locator.descriptor), + methods, + evidence: Evidence::DataPointer, + }); + } + } + Found { tables, infos } +} + +/// A complete object locator, read. +struct Locator { + /// How far into the complete object this table's subobject starts. + offset: u32, + /// The type descriptor, which holds the class's decorated name. + descriptor: Addr, + /// The class hierarchy descriptor, which holds its bases. + hierarchy: Addr, +} + +/// Read the locator at an address, if that is what is there. +fn locator(p: &Program, at: Addr) -> Option { + // Signature zero on 32-bit and one on 64-bit, where the structure carries + // its own address so a relative address inside it can be resolved. + let signature = read_int(p, at, 4)? as u32; + if signature > 1 { + return None; + } + let offset = read_int(p, at.wrapping_offset(4), 4)? as u32; + let descriptor = rva(p, read_int(p, at.wrapping_offset(12), 4)? as u32)?; + let hierarchy = rva(p, read_int(p, at.wrapping_offset(16), 4)? as u32)?; + // The descriptor has to hold a decorated type name, which is what makes + // this a locator rather than five words that read like one. + decorated_name(p, descriptor)?; + Some(Locator { + offset, + descriptor, + hierarchy, + }) +} + +/// The class a locator names, and the bases the hierarchy descriptor lists. +fn describe(p: &Program, locator: &Locator) -> Option { + let mangled = decorated_name(p, locator.descriptor)?; + let name = class_name(&mangled); + + let count = read_int(p, locator.hierarchy.wrapping_offset(8), 4)? as u32; + // The class itself is the first entry, so one is the least a real + // descriptor has; a count that cannot be an array is not a count. + if count == 0 || count > 4096 { + return None; + } + let array = rva( + p, + read_int(p, locator.hierarchy.wrapping_offset(12), 4)? as u32, + )?; + + // The direct bases: walk the preorder, and after each one skip the bases + // it contains, because those are its and not this class's. + let mut bases = Vec::new(); + let mut i = 1u32; + while i < count { + let at = rva( + p, + read_int(p, array.wrapping_offset(i as i64 * 4), 4)? as u32, + )?; + let descriptor = rva(p, read_int(p, at, 4)? as u32)?; + let contained = read_int(p, at.wrapping_offset(4), 4)? as u32; + // The member displacement structure: where the base sits, and through + // which virtual base table when it is a virtual base. + let mdisp = read_int(p, at.wrapping_offset(8), 4)? as u32 as i32 as i64; + let pdisp = read_int(p, at.wrapping_offset(12), 4)? as u32 as i32 as i64; + let attributes = read_int(p, at.wrapping_offset(20), 4)? as u32; + // A displacement of -1 to the virtual base table means there is none, + // so the base sits at a fixed place in the object. + let is_virtual = pdisp >= 0; + bases.push(BaseClass { + name: class_name(&decorated_name(p, descriptor)?), + typeinfo: descriptor, + offset: if is_virtual { pdisp } else { mdisp }, + is_virtual, + // Bit two says the base is private or protected. Nothing else in + // the attributes bears on what the source declared. + is_public: attributes & 4 == 0, + basis: Basis::Rtti, + }); + i += 1 + contained; + } + + Some(TypeInfo { + name, + mangled, + // The three type-information classes are an Itanium notion; the + // Microsoft layout is one shape for every class. + kind: None, + bases, + basis: Basis::Rtti, + }) +} + +/// The decorated type name a type descriptor holds. +fn decorated_name(p: &Program, at: Addr) -> Option { + let name = at.wrapping_offset(2 * word_size(p) as i64); + let window = p.object.memory.decode_window(name, 512)?; + let end = window.iter().position(|b| *b == 0)?; + let text = std::str::from_utf8(window.get(..end)?).ok()?; + // Every type descriptor's name starts with the encoding of a type at no + // qualification, which is what separates one from any other string. + text.starts_with(".?A").then(|| text.to_string()) +} + +/// The class a decorated type name names. +/// +/// `.?AVFoo@Bar@@` is `Bar::Foo`: the components are innermost first and the +/// leading letter says class or struct. Anything with a template argument list +/// in it is handed back as it is written, because guessing at the argument +/// grammar would put a name in the output that is not the class's. +fn class_name(decorated: &str) -> String { + let plain = || decorated.to_string(); + let Some(body) = decorated.strip_prefix(".?A") else { + return plain(); + }; + let body = body.strip_prefix(['V', 'U', 'T', 'W']).unwrap_or(body); + let Some(body) = body.strip_suffix("@@") else { + return plain(); + }; + let parts: Vec<&str> = body.split('@').collect(); + if parts.is_empty() + || parts.iter().any(|s| { + s.is_empty() + || !s + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$') + }) + { + return plain(); + } + parts + .iter() + .rev() + .copied() + .collect::>() + .join("::") +} + +/// The address a relative virtual address names. +fn rva(p: &Program, rva: u32) -> Option { + if rva == 0 { + return None; + } + let at = Addr(p.object.image_base.get().wrapping_add(rva as u64)); + p.object.memory.is_mapped(at).then_some(at) +} diff --git a/crates/e5r-api/src/prototypes.rs b/crates/e5r-api/src/prototypes.rs new file mode 100644 index 0000000..978dcf5 --- /dev/null +++ b/crates/e5r-api/src/prototypes.rs @@ -0,0 +1,179 @@ +//! Prototypes across a whole program, where the callers get a say. +//! +//! A function on its own cannot always say whether it returns anything. One +//! whose last act computes an address into the first argument register has +//! left a value there, and nothing inside it says whether that was the point. +//! The callers know: if every one of them ignores what came back, it was not a +//! result. +//! +//! This is the roadmap's feedback edge in its simplest useful form. It runs +//! once over the call graph rather than to a fixed point, because the one fact +//! it propagates cannot change what any caller does with its own result. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_analysis::Program; +use e5r_core::Addr; +use e5r_ir::proto::Prototype; +use e5r_ir::ssa::{SsaFunction, SsaKind}; + +/// A recovered prototype and how it was settled. +#[derive(Debug, Clone)] +pub struct Recovered { + /// What the function itself says. + pub prototype: Prototype, + /// How many calls to it were found. + pub callers: usize, + /// True when at least one caller reads what it left in the result + /// register. + pub result_used: bool, +} + +/// Recover every function's prototype, then let the callers settle the +/// returns. +pub fn prototypes(p: &Program) -> BTreeMap { + let abi = e5r_ir::abi::of(&p.object.arch); + let mut out: BTreeMap = BTreeMap::new(); + let mut ssa: BTreeMap = BTreeMap::new(); + + for f in p.functions_by_address() { + let Some(built) = build(p, f) else { continue }; + let prototype = e5r_ir::proto::recover(&built, &abi); + out.insert( + f.entry, + Recovered { + prototype, + callers: 0, + result_used: false, + }, + ); + ssa.insert(f.entry, built); + } + + // Who calls whom, and whether the caller reads what came back. The call + // defines the result register, so "reads what came back" is exactly + // "something uses the value that call defined". + for function in ssa.values() { + let consumed = consumed(function, &abi); + for b in function.blocks.values() { + for op in &b.ops { + if op.kind != SsaKind::Op(e5r_ir::op::Op::Call) { + continue; + } + let Some(target) = op.inputs.first().and_then(|i| i.as_const()) else { + continue; + }; + let Some(entry) = out.get_mut(&Addr(target)) else { + continue; + }; + entry.callers += 1; + if let Some(result) = op.out { + if consumed.contains(&result) { + entry.result_used = true; + } + } + } + } + } + + // A function every caller ignores did not return anything, whatever it + // left lying in the register. One with no callers keeps its own answer: + // absence of evidence is not evidence. + for recovered in out.values_mut() { + if recovered.callers > 0 && !recovered.result_used { + recovered.prototype.returns = None; + recovered.prototype.returns_float = false; + } + } + out +} + +/// Every value this function does something with. +/// +/// Reading it as an operand is the obvious way, but not the only one: a value +/// left in an argument register before a call is passed to that call, and one +/// left in the result register at a return is handed back. Neither appears as +/// an operand of anything, because the registers are the convention rather +/// than the instruction. +fn consumed(f: &SsaFunction, abi: &e5r_ir::abi::Abi) -> BTreeSet { + use e5r_ir::op::{Op, Space}; + + let mut out = BTreeSet::new(); + for b in f.blocks.values() { + for op in &b.ops { + for i in &op.inputs { + if let Some(v) = i.as_value() { + out.insert(v); + } + } + } + } + + let argument: BTreeSet = abi + .integer_arguments + .iter() + .chain(abi.float_arguments.iter()) + .copied() + .collect(); + let result: BTreeSet = abi.results.iter().copied().collect(); + + for b in f.blocks.values() { + // What each register holds as the block is walked, so the value a call + // or a return finds there is the one that was passed or handed back. + let mut latest: BTreeMap = BTreeMap::new(); + for op in &b.ops { + match op.kind { + SsaKind::Op(Op::Call) | SsaKind::Op(Op::CallInd) => { + for offset in &argument { + if let Some(v) = latest.get(offset) { + out.insert(*v); + } + } + } + SsaKind::Op(Op::Return) => { + for offset in &result { + if let Some(v) = latest.get(offset) { + out.insert(*v); + } + } + } + _ => {} + } + if let Some(v) = op.out { + if v.location.space == Space::Register { + latest.insert(v.location.offset, v); + } + } + } + } + out +} + +/// One function in SSA form, optimized. +fn build(p: &Program, f: &e5r_analysis::Function) -> Option { + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + if blocks.is_empty() { + return None; + } + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut ssa = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut ssa); + Some(ssa) +} + +/// The functions that call a given one, for a caller that wants the graph +/// rather than the prototypes. +pub fn callers_of(p: &Program, target: Addr) -> BTreeSet { + p.xrefs + .to(target) + .iter() + .filter(|x| x.kind == e5r_analysis::XrefKind::Call) + .filter_map(|x| p.function(x.from).map(|f| f.entry)) + .collect() +} diff --git a/crates/e5r-api/src/query.rs b/crates/e5r-api/src/query.rs new file mode 100644 index 0000000..cff72ea --- /dev/null +++ b/crates/e5r-api/src/query.rs @@ -0,0 +1,1636 @@ +//! A small query language over the program model. +//! +//! The point is to answer the questions an analyst actually asks without +//! writing a program: which functions are large and unnamed, which imports are +//! called from one place, which strings look like paths. A filter over a named +//! set of entities covers most of them, and a filter is something a person can +//! type correctly on the first try. +//! +//! ```text +//! functions where insns > 100 and name ~ "crypt" +//! strings where length >= 20 and text ~ "/etc/" +//! xrefs where kind = call and to = 0x401000 +//! ``` +//! +//! Every field has a kind: a number, an address, text, or true and false. +//! Text takes any literal, because comparing it against a number or a bool is +//! a search for the characters they print as. The other kinds are narrow, so a +//! number compared against a word is a mistake in the query and is reported as +//! one rather than quietly returning nothing. +//! +//! Two of the entities are questions about dataflow rather than about a table, +//! and they are the reason this language exists rather than a filter over a +//! listing. A call site knows what it hands over, so it can be asked: +//! +//! ```text +//! calls to "memcpy" where arg3 is not bounded +//! calls to "memcpy" where arg3 is bounded +//! calls where target is indirect +//! calls to "strcpy" +//! functions where reads any argument of a call to "system" +//! values reaching arg1 of calls to "system" +//! ``` +//! +//! `is not bounded` covers two different answers and the row says which: a +//! value the code constrains nowhere is `unconstrained`, and one the analysis +//! could not follow is `unknown`. They are never printed as each other, which +//! is the whole point of asking. `crate::dataflow` decides them and explains +//! how. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +use e5r_analysis::{Program, XrefKind}; +use e5r_core::Addr; + +use crate::dataflow::{self, ArgumentFact, Basis, CallSite, Source, Verdict}; + +/// What a query selects over. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Entity { + /// Recovered functions. + Functions, + /// Strings found in the image. + Strings, + /// Symbols the container declares. + Symbols, + /// Symbols needed from elsewhere. + Imports, + /// Symbols offered to others. + Exports, + /// Declared sections. + Sections, + /// References between addresses. + Xrefs, + /// Call sites, with what each one hands over. + Calls, + /// The values that reach one argument of a call. + Values, +} + +impl Entity { + /// The name it is written as. + pub fn as_str(self) -> &'static str { + match self { + Entity::Functions => "functions", + Entity::Strings => "strings", + Entity::Symbols => "symbols", + Entity::Imports => "imports", + Entity::Exports => "exports", + Entity::Sections => "sections", + Entity::Xrefs => "xrefs", + Entity::Calls => "calls", + Entity::Values => "values", + } + } + + /// The fields it offers, for the error message when one is misspelled. + pub fn fields(self) -> &'static [&'static str] { + match self { + Entity::Functions => &[ + "address", "name", "size", "blocks", "insns", "complete", "named", "strength", + "callers", "callees", + ], + Entity::Strings => &["address", "text", "length", "section", "encoding"], + Entity::Symbols => &["address", "name", "size", "kind", "binding", "dynamic"], + Entity::Imports => &["name", "library"], + Entity::Exports => &["address", "name"], + Entity::Sections => &["address", "name", "size", "exec", "write"], + Entity::Xrefs => &["from", "to", "kind"], + Entity::Calls => &[ + "address", "function", "target", "indirect", "argument", "verdict", "bound", + "basis", "source", "strength", + ], + Entity::Values => &[ + "address", "function", "argument", "source", "detail", "strength", + ], + } + } + + fn parse(word: &str) -> Option { + Some(match word { + "functions" | "funcs" => Entity::Functions, + "strings" => Entity::Strings, + "symbols" | "syms" => Entity::Symbols, + "imports" => Entity::Imports, + "exports" => Entity::Exports, + "sections" => Entity::Sections, + "xrefs" => Entity::Xrefs, + "calls" => Entity::Calls, + "values" => Entity::Values, + _ => return None, + }) + } +} + +/// A parsed query. +#[derive(Debug, Clone)] +pub struct Query { + /// What it selects over. + pub entity: Entity, + /// The filter, when there is one. + pub filter: Option, + /// How many results to return at most. + pub limit: Option, + /// The callee a `calls to "name"` query is about. + pub target: Option, + /// The argument a `values reaching argN` query is about, counting from + /// zero. + pub argument: Option, +} + +impl Query { + /// Whether the filter asks this entity something it can answer. + /// + /// Separate from parsing because it is a different kind of mistake: the + /// text was a query, but `strings` have no `arg3`. Separate from running + /// because a caller with no program loaded, such as a prompt checking a + /// line as it is typed, can still be told about it. + pub fn validate(&self) -> Result<(), String> { + match &self.filter { + Some(filter) => check(filter, self.entity), + None => Ok(()), + } + } +} + +/// A filter expression. +#[derive(Debug, Clone)] +pub enum Expr { + /// Both sides. + And(Box, Box), + /// Either side. + Or(Box, Box), + /// The opposite. + Not(Box), + /// A question about what the code does with a value. + Fact(Fact), + /// A comparison between a field and a literal. + Compare { + /// The field's name. + field: String, + /// How to compare. + op: Op, + /// What to compare against. + value: Value, + }, +} + +/// A question a table cannot answer, asked of the dataflow instead. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Fact { + /// `arg3 is bounded`, and the rest of the forms `is` takes. + Argument { + /// Which argument, counting from zero: `arg1` is zero. + index: usize, + /// What is being asked about it. + want: Want, + }, + /// `target is indirect`, or `target is direct`. + Indirect(bool), + /// `reads any argument of a call to "system"`. + Reads { + /// True for `every`, false for `any`. + every: bool, + /// The callee. + callee: String, + }, +} + +/// What `argN is ...` asks. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Want { + /// A constant bound the code establishes. + Bounded, + /// A bound that is in the instruction. + Constant, + /// Proved to exceed any bound on some path. + Unconstrained, + /// No bound established, which is not the same claim. + Unknown, +} + +impl Want { + /// The word it is written as. + fn parse(word: &str) -> Option { + Some(match word { + "bounded" => Want::Bounded, + "constant" => Want::Constant, + "unconstrained" => Want::Unconstrained, + "unknown" => Want::Unknown, + _ => return None, + }) + } +} + +/// How two things are compared. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Op { + /// Equal. + Eq, + /// Not equal. + Ne, + /// Less than. + Lt, + /// Less than or equal. + Le, + /// Greater than. + Gt, + /// Greater than or equal. + Ge, + /// Contains, for strings. + Contains, + /// Does not contain. + Omits, +} + +impl Op { + /// The symbol it is written as, so an error can quote the query back. + pub fn as_str(self) -> &'static str { + match self { + Op::Eq => "=", + Op::Ne => "!=", + Op::Lt => "<", + Op::Le => "<=", + Op::Gt => ">", + Op::Ge => ">=", + Op::Contains => "~", + Op::Omits => "!~", + } + } +} + +/// A literal in a query. +#[derive(Debug, Clone, PartialEq)] +pub enum Value { + /// A number, however it was written. + Number(u64), + /// A string, quoted or a bare word. + Text(String), + /// True or false. + Bool(bool), +} + +impl Value { + /// How an error message writes it back. + fn describe(&self) -> String { + match self { + Value::Number(n) => n.to_string(), + Value::Text(t) => format!("{t:?}"), + Value::Bool(b) => b.to_string(), + } + } +} + +/// What kind of value a field holds. +/// +/// A field's kind is what makes `insns > "large"` a mistake in the query +/// rather than a filter that matches nothing, which is the difference between +/// being told about a typo and believing an empty answer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Kind { + /// A count, a size or a length. + Number, + /// An address, which is a number written in hex. + Address, + /// Text. + Text, + /// True or false. + Bool, +} + +impl Kind { + /// The phrase an error message uses for it. + fn as_str(self) -> &'static str { + match self { + Kind::Number => "a number", + Kind::Address => "an address", + Kind::Text => "text", + Kind::Bool => "true or false", + } + } +} + +/// The kind of one field. +/// +/// Keyed by name rather than by entity, because a name means the same thing +/// wherever it appears: `size` is a size in a symbol and in a section. A test +/// checks the table against the rows the entities actually build, so it cannot +/// drift away from them. +fn field_kind(field: &str) -> Kind { + match field { + "address" | "from" | "to" => Kind::Address, + "size" | "blocks" | "insns" | "length" | "callers" | "callees" => Kind::Number, + "complete" | "named" | "exec" | "write" | "dynamic" | "indirect" => Kind::Bool, + _ => Kind::Text, + } +} + +/// Whether a number was written with quotes around it. +fn looks_numeric(text: &str) -> bool { + let cleaned = text.replace('_', ""); + match cleaned.strip_prefix("0x") { + Some(hex) => u64::from_str_radix(hex, 16).is_ok(), + None => cleaned.parse::().is_ok(), + } +} + +/// Whether this literal is a sensible thing to compare this field against. +/// +/// Text takes anything, since a number or a bool compared against text is a +/// search for the characters it prints as, which is what `kind = call` means. +/// The other kinds are narrow, and say so. +fn check_value(field: &str, op: Op, value: &Value) -> Result<(), String> { + let kind = field_kind(field); + match kind { + Kind::Text => Ok(()), + Kind::Number | Kind::Address => { + if matches!(op, Op::Contains | Op::Omits) { + return Err(format!( + "`{field}` is {}, and `{}` searches text; compare it with =, !=, <, <=, > or >=", + kind.as_str(), + op.as_str() + )); + } + match value { + Value::Number(_) => Ok(()), + Value::Text(t) if looks_numeric(t) => Err(format!( + "`{field}` is {}, and {t:?} is quoted; write `{field} {} {t}`", + kind.as_str(), + op.as_str() + )), + _ => Err(format!( + "`{field}` is {}, and {} is not one", + kind.as_str(), + value.describe() + )), + } + } + Kind::Bool => match (op, value) { + (Op::Eq | Op::Ne, Value::Bool(_)) => Ok(()), + (Op::Eq | Op::Ne, _) => Err(format!( + "`{field}` is true or false, and {} is neither; write `{field} = true`", + value.describe() + )), + _ => Err(format!( + "`{field}` is true or false, and `{}` cannot compare it; \ + write `{field} = true` or `{field} != true`", + op.as_str() + )), + }, + } +} + +/// One field of one result. +#[derive(Debug, Clone, PartialEq)] +pub enum Field { + /// A number. + Number(u64), + /// An address, which prints as hex. + Address(Addr), + /// A string. + Text(String), + /// True or false. + Bool(bool), + /// The field exists but this row has no value for it. + Missing, +} + +impl fmt::Display for Field { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Field::Number(v) => write!(f, "{v}"), + Field::Address(a) => write!(f, "{a}"), + Field::Text(t) => f.write_str(t), + Field::Bool(b) => write!(f, "{b}"), + Field::Missing => f.write_str("-"), + } + } +} + +/// One result. +#[derive(Debug, Clone)] +pub struct Row { + /// The fields, in the entity's declared order. + pub values: Vec<(&'static str, Field)>, +} + +impl Row { + /// One field by name. + pub fn get(&self, name: &str) -> Option<&Field> { + self.values.iter().find(|(n, _)| *n == name).map(|(_, v)| v) + } +} + +/// What a query produced. +#[derive(Debug, Clone)] +pub struct Answer { + /// What was selected over. + pub entity: Entity, + /// The column names, in order. + pub columns: &'static [&'static str], + /// The rows that matched. + pub rows: Vec, + /// How many matched before the limit was applied. + pub matched: usize, +} + +/// Parse and run a query. +pub fn run(p: &Program, text: &str) -> Result { + let query = parse(text)?; + // A field that does not exist is a typo, and a query with one should say + // so rather than match nothing. + query.validate()?; + let mut rows = match query.entity { + Entity::Calls | Entity::Values => dataflow_rows(p, &query), + _ => { + let reads = reads_sets(p, query.filter.as_ref()); + let context = Context { + site: None, + reads: &reads, + }; + let mut rows = collect(p, query.entity); + if let Some(filter) = &query.filter { + rows.retain(|row| matches(row, &context, filter)); + } + rows + } + }; + let matched = rows.len(); + if let Some(limit) = query.limit { + rows.truncate(limit); + } + Ok(Answer { + entity: query.entity, + columns: query.entity.fields(), + rows, + matched, + }) +} + +/// The rows of a query about what a call hands over. +/// +/// One row per call per argument the query asked about, because a question +/// about two arguments has two answers and folding them into one row would +/// have to drop one. A query that names no argument gets one row per call. +fn dataflow_rows(p: &Program, query: &Query) -> Vec { + let sites = sites(p, query); + let reads = reads_sets(p, query.filter.as_ref()); + let wanted = referenced(query.filter.as_ref()); + + let mut rows = Vec::new(); + for site in &sites { + // A question about a third argument is a question about calls that + // have one; without this a call taking two would match `arg3 is not + // bounded` by having no third argument at all. + if !wanted.iter().all(|i| site.argument(*i).is_some()) { + continue; + } + if let Some(filter) = &query.filter { + let context = Context { + site: Some(site), + reads: &reads, + }; + if !matches(&call_row(site, None), &context, filter) { + continue; + } + } + match query.entity { + Entity::Values => { + let Some(index) = query.argument else { + continue; + }; + let Some(argument) = site.argument(index) else { + continue; + }; + for source in &argument.sources { + rows.push(value_row(site, argument, source)); + } + } + _ if wanted.is_empty() => rows.push(call_row(site, None)), + _ => { + for index in &wanted { + rows.push(call_row(site, site.argument(*index))); + } + } + } + } + rows +} + +/// The call sites a query is about. +/// +/// Naming the callee turns the question into a lookup: the cross references +/// say which functions call it, and only those are lifted. Without a name +/// every function has to be looked at, because an indirect call is not in any +/// callee's reference list. +fn sites(p: &Program, query: &Query) -> Vec { + match &query.target { + Some(name) => dataflow::call_sites_to(p, name), + None => dataflow::call_sites(p), + } +} + +/// The arguments a filter asks about, in order. +fn referenced(filter: Option<&Expr>) -> Vec { + let mut out = Vec::new(); + fn walk(e: &Expr, out: &mut Vec) { + match e { + Expr::And(a, b) | Expr::Or(a, b) => { + walk(a, out); + walk(b, out); + } + Expr::Not(a) => walk(a, out), + Expr::Fact(Fact::Argument { index, .. }) if !out.contains(index) => out.push(*index), + _ => {} + } + } + if let Some(e) = filter { + walk(e, &mut out); + } + out.sort_unstable(); + out +} + +/// One call, described for the argument the query asked about. +fn call_row(site: &CallSite, argument: Option<&ArgumentFact>) -> Row { + let strength = match argument { + Some(a) => a.strength, + // With no argument in question the row's claim is about who is being + // called, so that is the strength it carries. + None => site.target_strength, + }; + Row { + values: vec![ + ("address", Field::Address(site.at)), + ("function", Field::Text(site.caller_name.clone())), + ("target", Field::Text(site.target_text())), + ("indirect", Field::Bool(site.indirect)), + ( + "argument", + match argument { + Some(a) => Field::Text(a.name()), + None => Field::Missing, + }, + ), + ( + "verdict", + match argument { + Some(a) => Field::Text(a.verdict.as_str().to_string()), + None => Field::Missing, + }, + ), + ( + "bound", + match argument.and_then(|a| a.bound_text()) { + Some(b) => Field::Text(b), + None => Field::Missing, + }, + ), + ( + "basis", + match argument { + Some(a) => Field::Text(a.basis.as_str().to_string()), + None => Field::Missing, + }, + ), + ( + "source", + match argument { + Some(a) => Field::Text(a.source_text()), + None => Field::Missing, + }, + ), + ("strength", Field::Text(strength.as_str().to_string())), + ], + } +} + +/// One value that reaches one argument. +fn value_row(site: &CallSite, argument: &ArgumentFact, source: &Source) -> Row { + Row { + values: vec![ + ("address", Field::Address(site.at)), + ("function", Field::Text(site.caller_name.clone())), + ("argument", Field::Text(argument.name())), + ("source", Field::Text(kind_of(source).to_string())), + ("detail", Field::Text(source.describe())), + ( + "strength", + Field::Text(source.strength().as_str().to_string()), + ), + ], + } +} + +/// The one word that says what kind of source it is. +fn kind_of(source: &Source) -> &'static str { + match source { + Source::Constant { text: Some(_), .. } => "string", + Source::Constant { + symbol: Some(_), .. + } => "address", + Source::Constant { .. } => "literal", + Source::Caller { .. } => "argument", + Source::Frame { .. } => "frame", + Source::Memory { .. } => "memory", + Source::Result { .. } => "result", + Source::Clobbered => "clobbered", + Source::Computed { .. } => "computed", + Source::Nothing => "nothing", + } +} + +/// Parse a query without running it. +pub fn parse(text: &str) -> Result { + let tokens = lex(text)?; + let mut parser = Parser { tokens, at: 0 }; + let entity = match parser.next() { + Some(Token::Word(w)) => Entity::parse(&w).ok_or_else(|| { + format!("{w:?} is not something to query; try functions, strings, symbols, imports, exports, sections or xrefs") + })?, + _ => return Err("a query starts with what to look at, such as `functions`".into()), + }; + + // `values reaching arg1 of calls`: which value, of which call. The clause + // is what makes the question answerable, so it is required rather than + // defaulted to the first argument of everything. + let argument = match entity { + Entity::Values => Some(parser.reaching()?), + _ => None, + }; + // `calls to "memcpy"`: sugar for a comparison against the target, except + // that it also matches the import thunk that stands in for it. + let mut target = None; + if matches!(entity, Entity::Calls | Entity::Values) + && matches!(parser.peek(), Some(Token::Word(w)) if w == "to") + { + parser.at += 1; + target = Some(parser.name("`to` wants the name of what is called")?); + } + + let mut filter = None; + let mut limit = None; + while let Some(token) = parser.next() { + match token { + Token::Word(w) if w == "where" => { + // A bare `where` is a half-typed query, not a query with an + // empty filter that matches everything. + if parser.peek().is_none() { + return Err("`where` wants a filter and this query ends after it; \ + try `where insns > 100`" + .into()); + } + filter = Some(parser.expression()?) + } + Token::Word(w) if w == "limit" => match parser.next() { + Some(Token::Number(n)) => limit = Some(n as usize), + _ => return Err("`limit` wants a number".into()), + }, + other => return Err(format!("did not expect {other} here")), + } + } + Ok(Query { + entity, + filter, + limit, + target, + argument, + }) +} + +fn check(e: &Expr, entity: Entity) -> Result<(), String> { + match e { + Expr::And(a, b) | Expr::Or(a, b) => { + check(a, entity)?; + check(b, entity) + } + Expr::Not(a) => check(a, entity), + // A dataflow question asked of the wrong thing is a mistake in the + // query, and saying which thing does answer it is more use than a + // list of fields. + Expr::Fact(Fact::Argument { .. } | Fact::Indirect(_)) => { + if matches!(entity, Entity::Calls | Entity::Values) { + Ok(()) + } else { + Err(format!( + "an argument and a target belong to a call, and {} are not calls; \ + try `calls to \"memcpy\" where arg3 is not bounded`", + entity.as_str() + )) + } + } + Expr::Fact(Fact::Reads { .. }) => { + if entity == Entity::Functions { + Ok(()) + } else { + Err(format!( + "`reads` asks what a function does, and {} are not functions; \ + try `functions where reads any argument of a call to \"system\"`", + entity.as_str() + )) + } + } + Expr::Compare { field, op, value } => { + if !entity.fields().contains(&field.as_str()) { + return Err(format!( + "{} have no field {field:?}; they have {}", + entity.as_str(), + entity.fields().join(", ") + )); + } + check_value(field, *op, value) + } + } +} + +fn matches(row: &Row, context: &Context<'_>, e: &Expr) -> bool { + match e { + Expr::And(a, b) => matches(row, context, a) && matches(row, context, b), + Expr::Or(a, b) => matches(row, context, a) || matches(row, context, b), + Expr::Not(a) => !matches(row, context, a), + Expr::Fact(fact) => context.holds(row, fact), + Expr::Compare { field, op, value } => { + let Some(actual) = row.get(field) else { + return false; + }; + compare(actual, *op, value) + } + } +} + +fn compare(actual: &Field, op: Op, value: &Value) -> bool { + match (actual, value) { + (Field::Missing, _) => false, + (Field::Number(a), Value::Number(b)) => number(*a, op, *b), + (Field::Address(a), Value::Number(b)) => number(a.get(), op, *b), + (Field::Bool(a), Value::Bool(b)) => match op { + Op::Eq => a == b, + Op::Ne => a != b, + _ => false, + }, + (Field::Text(a), Value::Text(b)) => text(a, op, b), + // A number compared against a word compares its printed form, which is + // what `kind = call` means. + (Field::Number(a), Value::Text(b)) => text(&a.to_string(), op, b), + (Field::Address(a), Value::Text(b)) => text(&a.to_string(), op, b), + (Field::Bool(a), Value::Text(b)) => text(&a.to_string(), op, b), + (Field::Text(a), Value::Number(b)) => text(a, op, &b.to_string()), + (Field::Text(a), Value::Bool(b)) => text(a, op, &b.to_string()), + (_, _) => false, + } +} + +fn number(a: u64, op: Op, b: u64) -> bool { + match op { + Op::Eq => a == b, + Op::Ne => a != b, + Op::Lt => a < b, + Op::Le => a <= b, + Op::Gt => a > b, + Op::Ge => a >= b, + Op::Contains | Op::Omits => false, + } +} + +fn text(a: &str, op: Op, b: &str) -> bool { + match op { + Op::Eq => a == b, + Op::Ne => a != b, + Op::Lt => a < b, + Op::Le => a <= b, + Op::Gt => a > b, + Op::Ge => a >= b, + Op::Contains => a.contains(b), + Op::Omits => !a.contains(b), + } +} + +/// Every row of one entity. +/// +/// The two dataflow entities are not here: their rows come from lifting the +/// functions a query is about rather than from a table, which is what +/// `dataflow_rows` does. +fn collect(p: &Program, entity: Entity) -> Vec { + match entity { + Entity::Calls | Entity::Values => Vec::new(), + Entity::Functions => p + .functions_by_address() + .map(|f| { + let callers = p.xrefs.to(f.entry).len(); + Row { + values: vec![ + ("address", Field::Address(f.entry)), + ("name", Field::Text(f.display_name())), + ("size", Field::Number(f.cfg.covered_bytes())), + ("blocks", Field::Number(f.cfg.blocks.len() as u64)), + ( + "insns", + Field::Number( + f.cfg.blocks.values().map(|b| b.insns as u64).sum::(), + ), + ), + ("complete", Field::Bool(f.is_complete())), + ("named", Field::Bool(f.name.is_some())), + ( + "strength", + Field::Text(f.provenance.strength().as_str().to_string()), + ), + ("callers", Field::Number(callers as u64)), + ( + "callees", + Field::Number( + f.cfg + .blocks + .keys() + .flat_map(|b| p.xrefs.from(*b)) + .filter(|x| x.kind == XrefKind::Call) + .count() as u64, + ), + ), + ], + } + }) + .collect(), + Entity::Strings => p + .strings + .iter() + .map(|s| Row { + values: vec![ + ("address", Field::Address(s.addr)), + ("text", Field::Text(s.text.clone())), + ("length", Field::Number(s.len)), + ( + "section", + match p.object.section_at(s.addr) { + Some(sec) => Field::Text(sec.name.clone()), + None => Field::Missing, + }, + ), + ( + "encoding", + Field::Text(format!("{:?}", s.encoding).to_lowercase()), + ), + ], + }) + .collect(), + Entity::Symbols => p + .object + .symbols + .iter() + .map(|s| Row { + values: vec![ + ("address", Field::Address(s.addr)), + ("name", Field::Text(s.name.clone())), + ("size", Field::Number(s.size)), + ("kind", Field::Text(format!("{:?}", s.kind).to_lowercase())), + ( + "binding", + Field::Text(format!("{:?}", s.binding).to_lowercase()), + ), + ("dynamic", Field::Bool(s.dynamic)), + ], + }) + .collect(), + Entity::Imports => p + .object + .imports + .iter() + .map(|i| Row { + values: vec![ + ("name", Field::Text(i.name.clone())), + ( + "library", + match &i.library { + Some(l) => Field::Text(l.clone()), + None => Field::Missing, + }, + ), + ], + }) + .collect(), + Entity::Exports => p + .object + .exports + .iter() + .map(|e| Row { + values: vec![ + ("address", Field::Address(e.addr)), + ("name", Field::Text(e.name.clone())), + ], + }) + .collect(), + Entity::Sections => p + .object + .sections + .iter() + .map(|s| Row { + values: vec![ + ("address", Field::Address(s.range.start())), + ("name", Field::Text(s.name.clone())), + ("size", Field::Number(s.range.len())), + ("exec", Field::Bool(s.exec)), + ("write", Field::Bool(s.write)), + ], + }) + .collect(), + Entity::Xrefs => p + .xrefs + .all() + .iter() + .map(|x| Row { + values: vec![ + ("from", Field::Address(x.from)), + ("to", Field::Address(x.to)), + ("kind", Field::Text(format!("{:?}", x.kind).to_lowercase())), + ], + }) + .collect(), + } +} + +/// What a fact needs that a row does not carry. +struct Context<'a> { + /// The call the row describes, when it describes one. + site: Option<&'a CallSite>, + /// Which functions satisfy each `reads` fact, worked out once. + reads: &'a BTreeMap<(String, bool), BTreeSet>, +} + +impl Context<'_> { + /// Whether a fact holds of what this row describes. + fn holds(&self, row: &Row, fact: &Fact) -> bool { + match fact { + Fact::Argument { index, want } => { + let Some(argument) = self.site.and_then(|s| s.argument(*index)) else { + return false; + }; + match want { + Want::Bounded => argument.verdict == Verdict::Bounded, + // A constant is a bound that needed no analysis to find. + Want::Constant => { + argument.verdict == Verdict::Bounded && argument.basis == Basis::Literal + } + Want::Unconstrained => argument.verdict == Verdict::Unconstrained, + Want::Unknown => argument.verdict == Verdict::Unknown, + } + } + Fact::Indirect(want) => self.site.is_some_and(|s| s.indirect == *want), + Fact::Reads { every, callee } => { + let Some(Field::Address(at)) = row.get("address") else { + return false; + }; + self.reads + .get(&(callee.clone(), *every)) + .is_some_and(|set| set.contains(at)) + } + } + } +} + +/// The functions each `reads` fact in a filter is true of. +/// +/// Worked out once per query rather than per function: the answer needs the +/// call sites of every function that calls the named callee, and that set is +/// the same whichever function the filter is being tested against. +fn reads_sets(p: &Program, filter: Option<&Expr>) -> BTreeMap<(String, bool), BTreeSet> { + let mut wanted: Vec<(String, bool)> = Vec::new(); + fn walk(e: &Expr, out: &mut Vec<(String, bool)>) { + match e { + Expr::And(a, b) | Expr::Or(a, b) => { + walk(a, out); + walk(b, out); + } + Expr::Not(a) => walk(a, out), + Expr::Fact(Fact::Reads { every, callee }) => { + let key = (callee.clone(), *every); + if !out.contains(&key) { + out.push(key); + } + } + _ => {} + } + } + if let Some(e) = filter { + walk(e, &mut wanted); + } + let mut out = BTreeMap::new(); + for (callee, every) in wanted { + let set = reads_set(p, &callee, every); + out.insert((callee, every), set); + } + out +} + +/// The functions that hand a call to `callee` something they read. +/// +/// A literal is not read, whatever it addresses: a command built from a string +/// in the image is the program's own, and a command that arrived from +/// somewhere else is the one worth looking at. +fn reads_set(p: &Program, callee: &str, every: bool) -> BTreeSet { + let sites = dataflow::call_sites_to(p, callee); + let mut out = BTreeSet::new(); + for site in &sites { + if site.arguments.is_empty() { + continue; + } + let read = |a: &ArgumentFact| a.sources.iter().any(|s| s.is_read()); + let hit = if every { + site.arguments.iter().all(read) + } else { + site.arguments.iter().any(read) + }; + if hit { + out.insert(site.caller); + } + } + out +} + +/// The argument a word like `arg3` names, counting from zero. +fn argument_index(word: &str) -> Option { + let n: usize = word.strip_prefix("arg")?.parse().ok()?; + // `arg0` is nobody's first argument, and a number past what any + // convention passes in registers is a typo rather than a question. + // `then`, not `then_some`: the latter takes a value, so `n - 1` is + // computed whatever the range says and `arg0` underflows before the + // `None` that was supposed to reject it. + (1..=64).contains(&n).then(|| n - 1) +} + +/// One piece of a query. +#[derive(Debug, Clone, PartialEq)] +enum Token { + Word(String), + Text(String), + Number(u64), + Op(Op), + Open, + Close, +} + +impl fmt::Display for Token { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Token::Word(w) => write!(f, "{w}"), + Token::Text(t) => write!(f, "{t:?}"), + Token::Number(n) => write!(f, "{n}"), + Token::Op(op) => f.write_str(op.as_str()), + Token::Open => f.write_str("("), + Token::Close => f.write_str(")"), + } + } +} + +/// A token as an error message names it. +fn found(token: Option<&Token>) -> String { + match token { + Some(t) => format!("{t}"), + None => "the end of the query".to_string(), + } +} + +fn lex(text: &str) -> Result, String> { + let mut out = Vec::new(); + let chars: Vec = text.chars().collect(); + let mut i = 0; + while i < chars.len() { + let c = chars[i]; + match c { + c if c.is_whitespace() => i += 1, + '(' => { + out.push(Token::Open); + i += 1; + } + ')' => { + out.push(Token::Close); + i += 1; + } + '"' | '\'' => { + let quote = c; + i += 1; + let start = i; + while i < chars.len() && chars[i] != quote { + i += 1; + } + if i >= chars.len() { + // Naming the text makes it obvious which quote was lost, + // which a query with several of them needs. + let rest: String = chars[start..].iter().collect(); + return Err(format!( + "a quoted string was never closed: {quote}{rest} has no closing {quote}" + )); + } + out.push(Token::Text(chars[start..i].iter().collect())); + i += 1; + } + '=' | '!' | '<' | '>' | '~' => { + let next = chars.get(i + 1).copied(); + let (op, len) = match (c, next) { + ('=', Some('=')) => (Op::Eq, 2), + ('=', Some('~')) => (Op::Contains, 2), + ('=', _) => (Op::Eq, 1), + ('!', Some('=')) => (Op::Ne, 2), + ('!', Some('~')) => (Op::Omits, 2), + ('<', Some('=')) => (Op::Le, 2), + ('<', _) => (Op::Lt, 1), + ('>', Some('=')) => (Op::Ge, 2), + ('>', _) => (Op::Gt, 1), + ('~', _) => (Op::Contains, 1), + _ => return Err(format!("{c} is not an operator")), + }; + out.push(Token::Op(op)); + i += len; + } + c if c.is_ascii_digit() => { + let start = i; + while i < chars.len() + && (chars[i].is_ascii_alphanumeric() || chars[i] == 'x' || chars[i] == '_') + { + i += 1; + } + let word: String = chars[start..i].iter().collect(); + let cleaned = word.replace('_', ""); + let value = match cleaned.strip_prefix("0x") { + Some(hex) => u64::from_str_radix(hex, 16), + None => cleaned.parse::(), + }; + match value { + Ok(v) => out.push(Token::Number(v)), + Err(_) => return Err(format!("{word:?} is not a number")), + } + } + _ => { + let start = i; + while i < chars.len() + && (chars[i].is_alphanumeric() + || chars[i] == '_' + || chars[i] == '.' + || chars[i] == '@' + || chars[i] == '$' + || chars[i] == ':') + { + i += 1; + } + if i == start { + return Err(format!("{c} does not belong in a query")); + } + out.push(Token::Word(chars[start..i].iter().collect())); + } + } + } + Ok(out) +} + +struct Parser { + tokens: Vec, + at: usize, +} + +impl Parser { + fn next(&mut self) -> Option { + let t = self.tokens.get(self.at).cloned(); + if t.is_some() { + self.at += 1; + } + t + } + + fn peek(&self) -> Option<&Token> { + self.tokens.get(self.at) + } + + fn peek_at(&self, n: usize) -> Option<&Token> { + self.tokens.get(self.at + n) + } + + /// The next token as a name, quoted or not. + fn name(&mut self, wanted: &str) -> Result { + match self.next() { + Some(Token::Text(t)) | Some(Token::Word(t)) => Ok(t), + other => Err(format!("{wanted}, not {}", found(other.as_ref()))), + } + } + + /// One expected word, for the fixed parts of a clause. + fn word(&mut self, wanted: &str, hint: &str) -> Result<(), String> { + match self.next() { + Some(Token::Word(w)) if w == wanted => Ok(()), + other => Err(format!( + "expected `{wanted}` here, found {}; {hint}", + found(other.as_ref()) + )), + } + } + + /// The `reaching argN of calls` a `values` query begins with. + fn reaching(&mut self) -> Result { + const HINT: &str = "a value query reads `values reaching arg1 of calls to \"system\"`"; + self.word("reaching", HINT)?; + let index = match self.next() { + Some(Token::Word(w)) => argument_index(&w).ok_or_else(|| { + format!("{w:?} does not name an argument; they are arg1, arg2 and so on") + })?, + other => { + return Err(format!( + "expected an argument such as `arg1`, found {}; {HINT}", + found(other.as_ref()) + )); + } + }; + self.word("of", HINT)?; + self.word("calls", HINT)?; + Ok(index) + } + + /// A question about dataflow, when that is what comes next. + /// + /// Recognized by shape rather than by a keyword list, so `target` and + /// `argument` are still ordinary fields when they are compared rather than + /// asked about. + fn fact(&mut self) -> Result, String> { + let Some(Token::Word(head)) = self.peek().cloned() else { + return Ok(None); + }; + if head == "reads" { + self.at += 1; + return self.reads().map(Some); + } + let is_next = matches!(self.peek_at(1), Some(Token::Word(w)) if w == "is"); + if !is_next { + return Ok(None); + } + if head != "target" && argument_index(&head).is_none() { + return Ok(None); + } + self.at += 2; + let negated = matches!(self.peek(), Some(Token::Word(w)) if w == "not"); + if negated { + self.at += 1; + } + let word = match self.next() { + Some(Token::Word(w)) => w, + other => { + return Err(format!( + "`{head} is` wants a word such as bounded, unconstrained, unknown, \ + constant, direct or indirect, found {}", + found(other.as_ref()) + )); + } + }; + let fact = if head == "target" { + match word.as_str() { + "indirect" => Fact::Indirect(true), + "direct" => Fact::Indirect(false), + _ => { + return Err(format!("a target is `direct` or `indirect`, not {word:?}")); + } + } + } else { + let index = argument_index(&head).unwrap_or_default(); + let want = Want::parse(&word).ok_or_else(|| { + format!( + "did not understand `{head} is {word}`; an argument is bounded, \ + constant, unconstrained or unknown" + ) + })?; + Fact::Argument { index, want } + }; + let expr = Expr::Fact(fact); + // `is not bounded` is the negation and nothing more: it covers both + // ways of not being bounded, and the row says which one this is. + Ok(Some(if negated { + Expr::Not(Box::new(expr)) + } else { + expr + })) + } + + /// `reads any argument of a call to "system"`. + fn reads(&mut self) -> Result { + const HINT: &str = "it reads `reads any argument of a call to \"system\"`"; + let every = match self.next() { + Some(Token::Word(w)) if w == "any" => false, + Some(Token::Word(w)) if w == "every" => true, + other => { + return Err(format!( + "`reads` wants `any` or `every`, found {}; {HINT}", + found(other.as_ref()) + )); + } + }; + match self.next() { + Some(Token::Word(w)) if w == "argument" || w == "arguments" => {} + other => { + return Err(format!( + "expected `argument` here, found {}; {HINT}", + found(other.as_ref()) + )); + } + } + self.word("of", HINT)?; + if matches!(self.peek(), Some(Token::Word(w)) if w == "a") { + self.at += 1; + } + match self.next() { + Some(Token::Word(w)) if w == "call" || w == "calls" => {} + other => { + return Err(format!( + "expected `call` here, found {}; {HINT}", + found(other.as_ref()) + )); + } + } + self.word("to", HINT)?; + let callee = self.name("a call is to something named")?; + Ok(Expr::Fact(Fact::Reads { every, callee })) + } + + fn expression(&mut self) -> Result { + let mut left = self.conjunction()?; + while matches!(self.peek(), Some(Token::Word(w)) if w == "or") { + self.at += 1; + // Naming the operator that was left dangling beats reporting the + // missing field name, which the reader did not write. + if self.peek().is_none() { + return Err("`or` wants another condition after it".into()); + } + let right = self.conjunction()?; + left = Expr::Or(Box::new(left), Box::new(right)); + } + Ok(left) + } + + fn conjunction(&mut self) -> Result { + let mut left = self.unary()?; + while matches!(self.peek(), Some(Token::Word(w)) if w == "and") { + self.at += 1; + if self.peek().is_none() { + return Err("`and` wants another condition after it".into()); + } + let right = self.unary()?; + left = Expr::And(Box::new(left), Box::new(right)); + } + Ok(left) + } + + fn unary(&mut self) -> Result { + if matches!(self.peek(), Some(Token::Word(w)) if w == "not") { + self.at += 1; + return Ok(Expr::Not(Box::new(self.unary()?))); + } + if self.peek() == Some(&Token::Open) { + self.at += 1; + let inner = self.expression()?; + if self.next() != Some(Token::Close) { + return Err("a bracket was never closed".into()); + } + return Ok(inner); + } + if let Some(fact) = self.fact()? { + return Ok(fact); + } + self.comparison() + } + + fn comparison(&mut self) -> Result { + let field = match self.next() { + Some(Token::Word(w)) => w, + Some(other) => return Err(format!("expected a field name, found {other}")), + None => return Err("the filter ends where a field name should be".into()), + }; + let op = match self.next() { + Some(Token::Op(op)) => op, + Some(other) => { + return Err(format!("expected an operator after {field}, found {other}")); + } + None => { + return Err(format!( + "`{field}` needs an operator such as = or > and something to compare against" + )); + } + }; + let value = match self.next() { + Some(Token::Number(n)) => Value::Number(n), + Some(Token::Text(t)) => Value::Text(t), + Some(Token::Word(w)) if w == "true" => Value::Bool(true), + Some(Token::Word(w)) if w == "false" => Value::Bool(false), + Some(Token::Word(w)) => Value::Text(w), + Some(other) => { + return Err(format!( + "`{field} {}` cannot be compared against {other}", + op.as_str() + )); + } + None => { + return Err(format!( + "`{field} {}` needs something to compare against", + op.as_str() + )); + } + }; + Ok(Expr::Compare { field, op, value }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_query_without_a_filter_parses() { + let q = parse("functions").expect("parses"); + assert_eq!(q.entity, Entity::Functions); + assert!(q.filter.is_none()); + } + + #[test] + fn operators_and_precedence_come_out_right() { + let q = parse("functions where insns > 10 and name ~ \"str\" or named = false") + .expect("parses"); + // `and` binds tighter than `or`. + let Some(Expr::Or(left, right)) = q.filter else { + panic!("expected an or at the top"); + }; + assert!(matches!(*left, Expr::And(..))); + assert!(matches!(*right, Expr::Compare { .. })); + } + + #[test] + fn brackets_override_precedence() { + let q = parse("functions where (insns > 10 or named = false) and complete = true") + .expect("parses"); + assert!(matches!(q.filter, Some(Expr::And(..)))); + } + + #[test] + fn hex_and_decimal_are_both_numbers() { + let q = parse("xrefs where to = 0x401000").expect("parses"); + let Some(Expr::Compare { value, .. }) = q.filter else { + panic!("expected a comparison"); + }; + assert_eq!(value, Value::Number(0x401000)); + } + + #[test] + fn a_misspelled_entity_says_what_there_is() { + let e = parse("functionz").unwrap_err(); + assert!(e.contains("functions"), "{e}"); + } + + #[test] + fn an_unclosed_string_is_an_error_not_a_match() { + assert!(parse("strings where text ~ \"oops").is_err()); + } + + #[test] + fn a_limit_is_parsed() { + let q = parse("strings limit 5").expect("parses"); + assert_eq!(q.limit, Some(5)); + } + + #[test] + fn a_call_query_names_its_callee() { + let q = parse("calls to \"memcpy\" limit 3").expect("parses"); + assert_eq!(q.entity, Entity::Calls); + assert_eq!(q.target.as_deref(), Some("memcpy")); + assert_eq!(q.limit, Some(3)); + } + + #[test] + fn an_argument_question_is_not_a_comparison() { + let q = parse("calls to \"memcpy\" where arg3 is not bounded").expect("parses"); + // `arg1` is the first argument, so the third is index two. + let Some(Expr::Not(inner)) = q.filter else { + panic!("expected a negation at the top"); + }; + assert!(matches!( + *inner, + Expr::Fact(Fact::Argument { + index: 2, + want: Want::Bounded + }) + )); + } + + #[test] + fn the_proved_form_is_its_own_question() { + let q = parse("calls where arg3 is unconstrained").expect("parses"); + assert!(matches!( + q.filter, + Some(Expr::Fact(Fact::Argument { + index: 2, + want: Want::Unconstrained + })) + )); + } + + #[test] + fn a_target_can_be_asked_how_it_is_reached() { + let q = parse("calls where target is indirect").expect("parses"); + assert!(matches!(q.filter, Some(Expr::Fact(Fact::Indirect(true))))); + let q = parse("calls where target is not indirect").expect("parses"); + assert!(matches!(q.filter, Some(Expr::Not(_)))); + } + + #[test] + fn a_value_query_says_which_value_of_which_call() { + let q = parse("values reaching arg1 of calls to \"system\"").expect("parses"); + assert_eq!(q.entity, Entity::Values); + assert_eq!(q.argument, Some(0)); + assert_eq!(q.target.as_deref(), Some("system")); + } + + #[test] + fn a_function_can_be_asked_what_it_hands_to_a_call() { + let q = + parse("functions where reads any argument of a call to \"system\"").expect("parses"); + assert!(matches!( + q.filter, + Some(Expr::Fact(Fact::Reads { every: false, .. })) + )); + let q = + parse("functions where reads every argument of a call to \"system\"").expect("parses"); + assert!(matches!( + q.filter, + Some(Expr::Fact(Fact::Reads { every: true, .. })) + )); + } + + #[test] + fn there_is_no_argument_zero() { + assert_eq!(argument_index("arg1"), Some(0)); + assert_eq!(argument_index("arg3"), Some(2)); + assert_eq!(argument_index("arg0"), None); + assert_eq!(argument_index("argument"), None); + assert_eq!(argument_index("arg99999999999999999999"), None); + } + + #[test] + fn a_number_compared_against_a_word_is_a_mistake_not_an_empty_answer() { + let q = parse("functions where insns > large").expect("parses"); + let e = check(q.filter.as_ref().expect("a filter"), Entity::Functions).unwrap_err(); + assert!(e.contains("insns") && e.contains("large"), "{e}"); + } + + #[test] + fn a_quoted_number_says_it_is_quoted() { + let q = parse("functions where insns > \"100\"").expect("parses"); + let e = check(q.filter.as_ref().expect("a filter"), Entity::Functions).unwrap_err(); + assert!(e.contains("quoted"), "{e}"); + assert!(e.contains("insns > 100"), "{e}"); + } + + #[test] + fn text_still_takes_any_literal() { + // `kind = call` and `name ~ 64` are searches for what the literal + // prints as, which is the one coercion worth keeping. + for text in ["xrefs where kind = call", "functions where name ~ 64"] { + let q = parse(text).expect("parses"); + let entity = q.entity; + check(q.filter.as_ref().expect("a filter"), entity).unwrap_or_else(|e| panic!("{e}")); + } + } + + #[test] + fn a_flag_is_compared_against_true_or_false_and_nothing_else() { + let q = parse("sections where exec = 3").expect("parses"); + let e = check(q.filter.as_ref().expect("a filter"), Entity::Sections).unwrap_err(); + assert!(e.contains("exec") && e.contains("true"), "{e}"); + // Ordering a flag is not a question, whatever it is compared against. + let q = parse("sections where exec > true").expect("parses"); + assert!(check(q.filter.as_ref().expect("a filter"), Entity::Sections).is_err()); + } + + #[test] + fn searching_a_number_for_text_is_refused() { + let q = parse("functions where insns ~ 10").expect("parses"); + let e = check(q.filter.as_ref().expect("a filter"), Entity::Functions).unwrap_err(); + assert!(e.contains("insns") && e.contains('~'), "{e}"); + } + + #[test] + fn a_bare_where_asks_for_a_filter() { + let e = parse("functions where").unwrap_err(); + assert!(e.contains("where"), "{e}"); + } + + #[test] + fn a_trailing_operator_names_the_field_it_left_hanging() { + let e = parse("functions where insns >").unwrap_err(); + assert!(e.contains("insns >"), "{e}"); + } + + #[test] + fn an_unclosed_quote_shows_the_text_it_swallowed() { + let e = parse("strings where text ~ \"oops").unwrap_err(); + assert!(e.contains("oops"), "{e}"); + } + + #[test] + fn a_dataflow_question_asked_of_the_wrong_thing_says_which_thing_answers_it() { + let q = parse("strings where arg3 is bounded").expect("parses"); + let e = check(q.filter.as_ref().expect("a filter"), Entity::Strings).unwrap_err(); + assert!(e.contains("calls"), "{e}"); + } +} diff --git a/crates/e5r-api/src/relocate.rs b/crates/e5r-api/src/relocate.rs new file mode 100644 index 0000000..d99847c --- /dev/null +++ b/crates/e5r-api/src/relocate.rs @@ -0,0 +1,184 @@ +//! Dynamic relocations, applied so a shared library's data reads the way the +//! loader will see it. +//! +//! A position independent library writes zero wherever a pointer goes and +//! leaves a relocation saying what belongs there. Every virtual table in +//! `/usr/lib/*/libstdc++.so.6` is zeroes in the file for that reason, and so is +//! every type-information pointer in one: 2,766 of its relocations name a +//! symbol and 1,056 are self-relative. Reading the bytes as they sit gives +//! nothing, and "this library has no classes" would be a wrong answer produced +//! with no warning. +//! +//! This is `e5r-format`'s job and belongs there, next to the relocatable-object +//! path that already does the same thing for `.o` files. It is here because +//! that path does not yet cover `ET_DYN`, and a class recovery that cannot read +//! a shared library is not worth measuring. +//! +//! Two kinds are applied and no more. A relative relocation needs nothing but +//! its addend. An absolute one needs the symbol, and only a symbol this file +//! defines has an address here: an undefined one is filled in by the loader +//! from some other library, and writing a guess would point a virtual table at +//! an address that is not the function. Jump slots are left alone, because the +//! container reader already resolves the PLT. + +use e5r_core::{Addr, Arch, Bits, Endian}; +use e5r_format::Object; + +/// What each architecture calls the relocations that can be applied here. +struct Kinds { + /// Add the addend to the load address. No symbol. + relative: u64, + /// Store a symbol's address plus the addend, pointer width. + absolute: u64, + /// A global data slot, computed the same way as an absolute one. + global: u64, +} + +/// Apply every dynamic relocation whose answer is in this file. +/// +/// Call it on a freshly loaded object, before `analyze`, so every later pass +/// sees the same bytes. Returns how many were applied, which is zero for a +/// non-relocatable executable, and zero for an architecture whose relocation +/// numbers are not listed here. +pub fn apply_relative_relocations(obj: &mut Object) -> usize { + let Some(kinds) = kinds_of(&obj.arch) else { + return 0; + }; + let wide = obj.bits == Bits::Bits64; + let little = obj.endian == Endian::Little; + let base = obj.image_base.get(); + let w = if wide { 8 } else { 4 }; + let symbols = dynamic_symbol_values(obj, wide, little); + + // `.rela.dyn` carries the addend in the entry; `.rel.dyn` leaves it in the + // word being relocated. + let mut applied = 0; + for (name, explicit_addend) in [(".rela.dyn", true), (".rel.dyn", false)] { + let Some(section) = obj.section(name) else { + continue; + }; + let step = match (wide, explicit_addend) { + (true, true) => 24, + (true, false) => 16, + (false, true) => 12, + (false, false) => 8, + }; + let start = section.range.start(); + let len = section.range.end().get().saturating_sub(start.get()); + let Some(bytes) = obj.memory.slice(start, len).map(<[u8]>::to_vec) else { + continue; + }; + + for entry in bytes.chunks_exact(step) { + let offset = int(&entry[..w], little); + let info = int(&entry[w..2 * w], little); + // The low part of `r_info` is the type and the high part is the + // symbol index, at different widths on the two pointer sizes. + let (ty, symbol) = if wide { + (info & 0xffff_ffff, info >> 32) + } else { + (info & 0xff, info >> 8) + }; + let at = Addr(base.wrapping_add(offset)); + let addend = if explicit_addend { + int(&entry[2 * w..3 * w], little) + } else { + match obj.memory.read_ptr(at, w as u64, little) { + Ok(v) => v, + Err(_) => continue, + } + }; + let value = if ty == kinds.relative { + base.wrapping_add(addend) + } else if ty == kinds.absolute || ty == kinds.global { + // Only a symbol this file defines. Zero means undefined, and + // the loader will fill it from somewhere this file cannot see. + match symbols.get(symbol as usize) { + Some(0) | None => continue, + Some(v) => base.wrapping_add(*v).wrapping_add(addend), + } + } else { + continue; + }; + let le = value.to_le_bytes(); + let be = value.to_be_bytes(); + let patch = if little { &le[..w] } else { &be[8 - w..] }; + if obj.memory.patch(at, patch) { + applied += 1; + } + } + } + applied +} + +/// The address of each dynamic symbol, by its index in `.dynsym`. +/// +/// A relocation names its symbol by index, and the loaded object's symbol list +/// is sorted by address, so the table has to be read again to get the order +/// back. Zero means the symbol is not defined here. +fn dynamic_symbol_values(obj: &Object, wide: bool, little: bool) -> Vec { + let Some(section) = obj.section(".dynsym") else { + return Vec::new(); + }; + let step = if wide { 24 } else { 16 }; + let start = section.range.start(); + let len = section.range.end().get().saturating_sub(start.get()); + let Some(bytes) = obj.memory.slice(start, len) else { + return Vec::new(); + }; + bytes + .chunks_exact(step) + .map(|e| { + // `st_shndx` of zero is an undefined symbol whatever `st_value` + // says, and on 64-bit the value sits after the index while on + // 32-bit it sits before it. + let (shndx, value) = if wide { + (int(&e[6..8], little), int(&e[8..16], little)) + } else { + (int(&e[14..16], little), int(&e[4..8], little)) + }; + if shndx == 0 { 0 } else { value } + }) + .collect() +} + +/// The relocation numbers an architecture's ABI publishes. +fn kinds_of(arch: &Arch) -> Option { + match arch { + Arch::X86_64 => Some(Kinds { + relative: 8, + absolute: 1, + global: 6, + }), + Arch::X86 => Some(Kinds { + relative: 8, + absolute: 1, + global: 6, + }), + Arch::AArch64 => Some(Kinds { + relative: 1027, + absolute: 257, + global: 1025, + }), + Arch::Arm | Arch::Thumb => Some(Kinds { + relative: 23, + absolute: 2, + global: 21, + }), + _ => None, + } +} + +fn int(bytes: &[u8], little: bool) -> u64 { + let mut value = 0u64; + if little { + for (i, b) in bytes.iter().enumerate() { + value |= (*b as u64) << (8 * i); + } + } else { + for b in bytes { + value = (value << 8) | *b as u64; + } + } + value +} diff --git a/crates/e5r-api/src/shapes.rs b/crates/e5r-api/src/shapes.rs new file mode 100644 index 0000000..347b88f --- /dev/null +++ b/crates/e5r-api/src/shapes.rs @@ -0,0 +1,59 @@ +//! What the pointers a function takes appear to point at. + +use std::collections::BTreeMap; + +use e5r_analysis::{Function, Program}; +use e5r_core::Addr; +use e5r_ir::shape::Shape; + +/// One pointer and what was seen through it. +#[derive(Debug, Clone)] +pub struct Pointer { + /// Which argument it is, when it is one. + pub argument: Option, + /// The register it arrived in, as a byte offset in the register file. + pub register: u64, + /// The fields the accesses imply: offset and width. + pub fields: Vec<(i64, u8)>, + /// The element size, when an index said what it was. + pub size: Option, + /// True when anything wrote through it. + pub written: bool, +} + +/// What each incoming pointer of a function appears to point at. +pub fn shapes_of(p: &Program, f: &Function) -> Vec { + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut ssa = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut ssa); + + let abi = e5r_ir::abi::of(&p.object.arch); + let mut out = Vec::new(); + for (location, shape) in e5r_ir::shape::shapes(&ssa) { + // The stack pointer points at the frame, which promotion already + // described; reporting it again as a structure says nothing. + if location.offset == abi.stack_pointer { + continue; + } + out.push(describe(&abi, location.offset, &shape)); + } + out.sort_by_key(|p| (p.argument.unwrap_or(usize::MAX), p.register)); + out +} + +fn describe(abi: &e5r_ir::abi::Abi, register: u64, shape: &Shape) -> Pointer { + Pointer { + argument: abi.integer_arguments.iter().position(|o| *o == register), + register, + fields: shape.fields(), + size: shape.size(), + written: shape.accesses.iter().any(|a| a.write), + } +} diff --git a/crates/e5r-api/src/signatures.rs b/crates/e5r-api/src/signatures.rs new file mode 100644 index 0000000..60dcf5d --- /dev/null +++ b/crates/e5r-api/src/signatures.rs @@ -0,0 +1,80 @@ +//! Building and applying function signatures. + +use e5r_analysis::{Function, Program}; +use e5r_db::anchor::Anchor; +use e5r_db::signature::{Library, Signature}; + +/// Anchor one function by its content. +pub fn anchor_of(p: &Program, f: &Function) -> Anchor { + let insns = p.instructions(f); + // The body is the bytes the blocks cover, not the hull, so a split + // function does not fold in whatever sits between its parts. + let mut body = Vec::with_capacity(f.cfg.covered_bytes() as usize); + for b in f.cfg.blocks.values() { + if let Some(bytes) = p.object.memory.slice(b.range.start(), b.range.len()) { + body.extend_from_slice(bytes); + } + } + Anchor::function(f.entry, &insns, &body) +} + +/// A signature for every function the program names. +/// +/// Only the named ones: a signature whose name is `sub_401000` identifies +/// nothing, and a library of them would match everything. +pub fn collect_signatures(p: &Program, source: &str) -> Library { + Library::build( + p.functions_by_address() + // An import thunk is four instructions that differ only in the + // offset they load, and that offset means something different in + // every binary. Two of them matching across builds is a + // coincidence, and the loader names them from the relocations + // anyway, so they are left out. + .filter(|f| f.name.is_some() && !is_thunk(f)) + .map(|f| { + let anchor = anchor_of(p, f); + Signature::new(&anchor, f.name.as_deref().unwrap_or_default(), source) + }) + .collect(), + ) +} + +/// True when a function is a stub that stands in for an imported one. +fn is_thunk(f: &Function) -> bool { + f.provenance.has(e5r_core::Evidence::ImportThunk) + || f.name.as_deref().is_some_and(|n| n.ends_with("@plt")) +} + +/// One function a library recognized. +#[derive(Debug, Clone)] +pub struct Identified { + /// Where it is. + pub addr: e5r_core::Addr, + /// What it was called before. + pub was: String, + /// What the library says it is. + pub name: String, + /// How the match was made. + pub resolution: e5r_db::anchor::Resolution, + /// Which library it came from. + pub source: String, +} + +/// Everything in a program a library recognizes. +pub fn identify(p: &Program, library: &Library) -> Vec { + let mut out = Vec::new(); + for f in p.functions_by_address() { + let anchor = anchor_of(p, f); + let Some(found) = library.identify(&anchor) else { + continue; + }; + out.push(Identified { + addr: f.entry, + was: f.display_name(), + name: found.signature.name.clone(), + resolution: found.resolution, + source: found.signature.source.clone(), + }); + } + out +} diff --git a/crates/e5r-api/src/vtables.rs b/crates/e5r-api/src/vtables.rs new file mode 100644 index 0000000..582d404 --- /dev/null +++ b/crates/e5r-api/src/vtables.rs @@ -0,0 +1,216 @@ +//! Virtual tables, and what they say about a binary's classes. +//! +//! Under the Itanium ABI a virtual table is an array of pointers preceded by +//! two words: how far the object's start is from this subobject, and a pointer +//! to the type information. The address a class's objects carry points past +//! those two, at the first function. +//! +//! Two ways to find them, and both are reported with what they rest on. A +//! `_ZTV` symbol names a table outright, which is a fact. Otherwise a run of +//! pointers into executable memory that something takes the address of is a +//! table by inference, and the inference is weaker the shorter the run. + +use e5r_analysis::Program; +use e5r_core::{Addr, Evidence}; + +/// One virtual table. +#[derive(Debug, Clone)] +pub struct VTable { + /// Where the table starts, which is the offset-to-top word. + pub addr: Addr, + /// The address objects of this class carry: the first function slot. + pub entry: Addr, + /// The class, demangled, when a symbol named it. + pub class: Option, + /// How far the complete object's start is from this subobject. + pub offset_to_top: i64, + /// Where the type information lives, when there is any. + pub typeinfo: Option, + /// The virtual functions, in slot order. + pub methods: Vec, + /// What the table rests on. + pub evidence: Evidence, +} + +/// The smallest run of code pointers that counts as a table without a symbol. +const MIN_INFERRED_SLOTS: usize = 3; + +/// Every virtual table the program appears to contain. +pub fn vtables(p: &Program) -> Vec { + let mut out: Vec = Vec::new(); + // The extent of every table already found, so the scan does not report a + // second table one word into the first. + let mut claimed: Vec<(u64, u64)> = Vec::new(); + + // Named tables first: a symbol is a fact and takes precedence over + // anything the scan would have guessed about the same address. + for symbol in &p.object.symbols { + if !symbol.name.starts_with("_ZTV") || symbol.addr == Addr::ZERO { + continue; + } + // A `_ZTV` symbol names the start of the table, which is the + // offset-to-top word; the address objects carry is two words later. + // The symbol's declared size says exactly how many slots there are, + // which matters for a class with pure virtual functions: those slots + // hold a stub the object file has not resolved, and a scan that stops + // at the first one would cut the table short. + let slots = (symbol.size >= 24).then(|| (symbol.size as usize - 16) / 8); + let table = read( + p, + symbol.addr.wrapping_offset(16), + Evidence::SymbolTable, + slots, + ) + // A class with virtual bases has its virtual base offsets in front of + // the offset-to-top word, so the address its objects carry is further + // in than two words and the straight read finds no functions there. + .or_else(|| { + let entry = address_point(p, symbol.addr, symbol.size)?; + // The size is the file's to choose, so one large enough to wrap + // would put the end of the table before its start. + let past = symbol.addr.get().checked_add(symbol.size)?; + let slots = past.checked_sub(entry.get()).map(|n| n as usize / 8); + read(p, entry, Evidence::SymbolTable, slots) + }); + let Some(table) = table else { + continue; + }; + claimed.push((table.addr.get(), extent(&table))); + out.push(VTable { + class: class_of(&symbol.name), + ..table + }); + } + + // Then the scan, over the sections a compiler puts tables in. + for section in &p.object.sections { + let relocated = section.name.starts_with(".data.rel.ro") + || section.name == ".rodata" + || section.name == ".data.rel.ro"; + if !relocated || section.range.is_empty() { + continue; + } + let mut at = section.range.start(); + while at.get() + 24 <= section.range.end().get() { + if claimed + .iter() + .any(|(lo, hi)| at.get() >= *lo && at.get() < *hi) + { + at = at.wrapping_offset(8); + continue; + } + match read(p, at.wrapping_offset(16), Evidence::DataPointer, None) { + Some(table) if table.methods.len() >= MIN_INFERRED_SLOTS => { + let next = extent(&table); + claimed.push((table.addr.get(), next)); + out.push(table); + at = Addr(next); + } + _ => at = at.wrapping_offset(8), + } + } + } + + out.sort_by_key(|t| t.addr); + out.dedup_by_key(|t| t.addr); + out +} + +/// One past the last byte a table occupies. +fn extent(t: &VTable) -> u64 { + t.entry.get() + t.methods.len() as u64 * 8 +} + +/// Read a table whose first function slot is at `entry`. +/// +/// `slots` is how many there are when something said; otherwise the run of +/// code pointers decides where it ends. +fn read(p: &Program, entry: Addr, evidence: Evidence, slots: Option) -> Option { + // The two words before the slots: the offset to the top of the object and + // the type information. + let addr = Addr(entry.get().checked_sub(16)?); + let offset_to_top = word(p, addr)? as i64; + let typeinfo = word(p, addr.wrapping_offset(8))?; + // A table has a small offset-to-top: a large one is not a table, it is two + // pointers that happen to sit next to each other. + if offset_to_top.unsigned_abs() > 1 << 20 { + return None; + } + + let mut methods = Vec::new(); + let mut slot = entry; + let limit = slots.unwrap_or(4096); + let mut code_slots = 0; + while methods.len() < limit { + let Some(target) = word(p, slot) else { break }; + if is_code(p, Addr(target)) { + code_slots += 1; + } else if target == 0 && slots.is_some() { + // A pure virtual function, or an entry whose symbol this file does + // not define. The declared size says it is still a slot. + } else { + break; + } + methods.push(Addr(target)); + slot = slot.wrapping_offset(8); + } + // At least one real function: a run of zeroes is not a table. + if code_slots == 0 { + return None; + } + Some(VTable { + addr, + entry, + class: None, + offset_to_top, + typeinfo: (typeinfo != 0).then_some(Addr(typeinfo)), + methods, + evidence, + }) +} + +fn word(p: &Program, at: Addr) -> Option { + let bytes = p.object.memory.slice(at, 8)?; + Some(u64::from_le_bytes(bytes.try_into().ok()?)) +} + +/// True when an address is inside something the container marked executable. +fn is_code(p: &Program, at: Addr) -> bool { + at != Addr::ZERO + && p.object + .sections + .iter() + .any(|s| s.exec && s.range.contains(at)) +} + +/// Where a virtual table group's address point is. +/// +/// The group a `_ZTV` symbol names is a run of sub-tables, and each is some +/// number of virtual call and virtual base offsets followed by the +/// offset-to-top word, the type information and the functions. Only a class +/// with virtual bases has anything in front, so this walks forward a word at a +/// time to the first place where two words are followed by a function. +fn address_point(p: &Program, start: Addr, size: u64) -> Option { + // A prefix of more than a few words would mean a class with dozens of + // virtual bases, and walking further than the symbol says is not walking + // inside this table any more. + let words = (size / 8).min(64); + for i in 1..words.saturating_sub(2) { + let at = start.wrapping_offset(i as i64 * 8); + if is_code(p, Addr(word(p, at.wrapping_offset(16))?)) { + return Some(at.wrapping_offset(16)); + } + } + None +} + +/// The class a vtable symbol names, demangled. +fn class_of(symbol: &str) -> Option { + // The mangled name of the class follows `_ZTV`, so putting a prefix the + // demangler recognizes back on it gives the class name. `_ZTI` rather + // than a wrapped nested name, because a class that is already nested + // carries its own `N...E` and wrapping that in another one is not a name. + let inner = symbol.strip_prefix("_ZTV")?; + let (_, demangled) = e5r_types::demangle(&format!("_ZTI{inner}"))?; + Some(demangled.strip_prefix("typeinfo for ")?.to_string()) +} diff --git a/crates/e5r-api/tests/asserted.rs b/crates/e5r-api/tests/asserted.rs new file mode 100644 index 0000000..d63b482 --- /dev/null +++ b/crates/e5r-api/tests/asserted.rs @@ -0,0 +1,397 @@ +//! The loop the tool exists for: decompile, assert what the engine could not +//! work out, decompile again and see the assertion in the output. +//! +//! An assertion that is parsed, validated, stored and then ignored by the +//! emitter is the same as no assertion at all, so every test here compares two +//! decompilations of the same function and pins what the declaration changed. +//! The last one pins what it did not change, which is the half that keeps an +//! assertion from being a licence to rewrite the body. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Function, Options, Program, analyze}; +use e5r_core::Addr; +use e5r_decomp::expr::Role; +use e5r_format::LoadOptions; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +fn function<'a>(p: &'a Program, name: &str) -> &'a Function { + p.functions_by_address() + .find(|f| f.display_name() == name) + .unwrap_or_else(|| panic!("no function called {name}")) +} + +/// Decompile one function, with an asserted declaration or without. +fn decompile(p: &Program, name: &str, declaration: Option<&str>) -> e5r_api::Decompiled { + let f = function(p, name); + let mut declared: BTreeMap = BTreeMap::new(); + if let Some(text) = declaration { + declared.insert(f.entry, text.to_string()); + } + e5r_api::decompile::decompile_function_with(p, f, &declared) +} + +/// The whole translation unit, so the definitions a declaration needs are in +/// view as well as the body. +fn unit(p: &Program, name: &str, declaration: Option<&str>) -> String { + let f = function(p, name); + let mut declared: BTreeMap = BTreeMap::new(); + if let Some(text) = declaration { + declared.insert(f.entry, text.to_string()); + } + e5r_api::decompile::decompile_program_with(p, &[f], &declared).text() +} + +#[test] +fn an_assertion_names_the_parameters_and_narrows_their_types() { + let Some(p) = open("dt-memory.x64.O2") else { + return; + }; + // Nothing declared this binary: the recovery can say that three argument + // registers arrive with values, and how wide each one is read. + let before = decompile(&p, "nestedoffset", None); + assert_eq!( + before.signature, + "uint64_t nestedoffset(uint64_t arg0, uint32_t arg1, uint32_t arg2)" + ); + assert!(before.text.contains("arg0 + ("), "{}", before.text); + + let after = decompile( + &p, + "nestedoffset", + Some( + "int nestedoffset(struct outer { int first; int pad; int array[16]; } *ptr, int a, int b)", + ), + ); + assert_eq!( + after.signature, + "int32_t nestedoffset(struct outer *ptr, int32_t a, int32_t b)" + ); + // The names reach the body, and the declared pointer is cast back to an + // integer wherever the machine used it as an address: C would scale the + // arithmetic and the machine has already done the scaling. + assert!(after.text.contains("(uint64_t)ptr + ("), "{}", after.text); + assert!( + after.text.contains("(uint32_t)a + (uint32_t)b"), + "{}", + after.text + ); + // The declared return type is the cast on the way out. + assert!(after.text.contains("return (int32_t)("), "{}", after.text); + assert!(!after.text.contains("arg0"), "{}", after.text); + + // A structure the declaration introduced is defined before it is used, so + // what comes out is a translation unit and not a fragment. + let text = unit( + &p, + "nestedoffset", + Some( + "int nestedoffset(struct outer { int first; int pad; int array[16]; } *ptr, int a, int b)", + ), + ); + assert!( + text.contains("struct outer { int32_t first; int32_t pad; int32_t array[16]; };"), + "{text}" + ); +} + +#[test] +fn an_asserted_void_return_drops_the_result_the_code_left_behind() { + let Some(p) = open("dt-memory.x64.O2") else { + return; + }; + let before = decompile(&p, "twodim", None); + assert!( + before.signature.starts_with("uint64_t "), + "{}", + before.signature + ); + assert!(before.text.contains("return ("), "{}", before.text); + + let after = decompile(&p, "twodim", Some("void twodim(int valin, int valout)")); + assert_eq!( + after.signature, + "void twodim(int32_t valin, int32_t valout)" + ); + // Nothing is returned and nothing pretends to be. + assert!(after.text.contains(" return;\n"), "{}", after.text); + assert!(!after.text.contains("return ("), "{}", after.text); + // The code did leave something in a register, and saying so is how an + // analyst finds out their declaration was the surprising one. + assert!( + after + .conflicts + .iter() + .any(|c| c.contains("returns nothing and the code leaves a value")), + "{:?}", + after.conflicts + ); +} + +#[test] +fn an_assertion_outranks_the_debug_information_and_the_conflict_is_recorded() { + let Some(p) = open("wide.x64.O0.o") else { + return; + }; + // The compiler recorded this one, and the analyst still wins. + let before = decompile(&p, "arith8", None); + assert_eq!(before.signature, "u8 arith8(u8 a, u8 b)"); + assert!(!before.asserted); + assert!(before.conflicts.is_empty()); + + let after = decompile( + &p, + "arith8", + Some("int arith8(unsigned char a, unsigned char b)"), + ); + assert_eq!(after.signature, "int32_t arith8(uint8_t a, uint8_t b)"); + assert!(after.asserted); + assert!(after.text.contains("return (int32_t)("), "{}", after.text); + // The declaration names two arguments and the code reads four registers. + // The declaration is honoured and what it overrode is reported rather than + // discarded, which is the whole of bet four. + assert!( + after + .conflicts + .iter() + .any(|c| c.contains("which the declaration does not name")), + "{:?}", + after.conflicts + ); +} + +#[test] +fn a_result_too_large_for_a_register_declares_the_pointer_the_caller_passes() { + let Some(p) = open("dt-memory.x64.O2") else { + return; + }; + let after = decompile( + &p, + "heapstring", + Some("struct big { long a; long b; long c; } heapstring(int n)"), + ); + // Twenty-four bytes do not fit the result registers, so the caller passes + // the address of the result in the first argument register and every + // declared parameter sits one register further along. Getting this wrong + // makes every name in the body wrong. + assert_eq!( + after.signature, + "void heapstring(struct big *__result, int32_t n)" + ); + assert!(after.text.contains("(uint64_t)__result"), "{}", after.text); +} + +#[test] +fn a_declaration_that_does_not_parse_changes_nothing() { + let Some(p) = open("dt-memory.x64.O2") else { + return; + }; + let before = decompile(&p, "nestedoffset", None); + let after = decompile(&p, "nestedoffset", Some("int nestedoffset(struct")); + assert_eq!(before.text, after.text); + assert!(!after.asserted); +} + +#[test] +fn an_assertion_changes_the_names_and_the_types_and_nothing_else() { + let Some(p) = open("dt-memory.x64.O2") else { + return; + }; + let declaration = + "int nestedoffset(struct outer { int first; int pad; int array[16]; } *ptr, int a, int b)"; + let before = decompile(&p, "nestedoffset", None); + let after = decompile(&p, "nestedoffset", Some(declaration)); + + // The structuring is untouched: a declaration says what the values are, + // never what the control flow is. + assert_eq!(before.gotos, after.gotos); + assert_eq!(before.lost, after.lost); + assert_eq!(before.locals, after.locals); + assert_eq!(before.unmodelled, after.unmodelled); + + let (old, new): (Vec<&str>, Vec<&str>) = + (before.text.lines().collect(), after.text.lines().collect()); + assert_eq!( + old.len(), + new.len(), + "{}\n----\n{}", + before.text, + after.text + ); + // Every line that changed has to be explained by something the + // declaration said. Anything else is the assertion being used as a licence + // to rewrite the body. + let introduced = ["ptr", "a", "b", "int32_t", "struct outer"]; + for (old, new) in old.iter().zip(new.iter()) { + if old == new { + continue; + } + assert!( + introduced.iter().any(|w| word(new, w)), + "line changed for no declared reason:\n {old}\n {new}" + ); + } +} + +#[test] +fn the_variables_come_back_with_their_names_and_types() { + let Some(p) = open("dt-memory.x64.O2") else { + return; + }; + let d = decompile(&p, "dupptr", None); + // A count of locals cannot be matched against anything. These can. + assert_eq!(d.variables.len(), d.locals + 2, "{:?}", d.variables); + let named = |name: &str| { + d.variables + .iter() + .find(|v| v.name == name) + .unwrap_or_else(|| panic!("no variable {name} in {:?}", d.variables)) + }; + assert_eq!(named("arg0").role, Role::Parameter); + assert_eq!(named("arg0").ty, "struct s_dupptr_arg0 *"); + assert_eq!(named("v2").role, Role::Local); + assert_eq!(named("v2").ty, "uint32_t"); + assert_eq!(named("v2").size, 4); + // Every variable the text declares is a variable the text mentions. + for v in &d.variables { + assert!(word(&d.text, &v.name), "{} is not in the body", v.name); + } + + // A declaration renames them, and the report renames with it. + let after = decompile( + &p, + "dupptr", + Some("long dupptr(unsigned int *table, int index)"), + ); + assert!( + after + .variables + .iter() + .any(|v| v.name == "table" && v.ty == "uint32_t *" && v.role == Role::Parameter) + ); +} + +#[test] +fn an_access_through_a_pointer_reads_as_a_field() { + let Some(p) = open("dt-memory.x64.O2") else { + return; + }; + // Nothing declared this pointer. The offsets the code touches through it + // are enough to say it points at a structure, and an access at a known + // offset then reads as the field it is rather than as arithmetic. + let text = unit(&p, "dupptr", None); + assert!(text.contains("struct s_dupptr_arg0 {"), "{text}"); + assert!(text.contains("arg0[v0].field_c"), "{text}"); + // The name says the offset, which is the honest thing for a field nobody + // declared: `field_c` is this crate's name for what is at twelve, not a + // name anyone wrote down. + assert!(!text.contains("->sub"), "{text}"); +} + +#[test] +fn a_declared_field_name_beats_the_one_the_offset_implies() { + use e5r_ir::op::Op; + use e5r_ir::ssa::SsaKind; + + let Some(p) = open("dt-memory.x64.O2") else { + return; + }; + let f = function(&p, "dupptr"); + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut ssa = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut ssa); + + let abi = e5r_ir::abi::of(&p.object.arch); + let first = abi.integer_arguments[0]; + let prototype = e5r_decomp::Prototype { + parameters: vec![e5r_decomp::Param { + name: "p".to_string(), + decl: "struct thing *p".to_string(), + floating: false, + pointer: true, + size: 8, + fields: vec![(4, 4), (12, 4)], + stride: Some(4), + stack: None, + }], + returns: Some("uint64_t".to_string()), + definitions: Vec::new(), + locals: BTreeMap::new(), + }; + let mut r = e5r_decomp::expr::Rebuilder::new(&ssa); + r.declare(&prototype); + + let load = ssa + .blocks + .values() + .flat_map(|b| b.ops.iter()) + .find(|op| { + op.kind == SsaKind::Op(Op::Load) && r.field_access(op.inputs.first(), op.size).is_some() + }) + .expect("a load through the pointer"); + + // With nothing declared, the field is named after the offset it is at, + // which says on its face that this crate made the name up. + assert!(r.expr(load).to_string().contains("field_c")); + + // With a declaration, the name a person wrote down is the one that prints. + r.field_names + .insert(first, BTreeMap::from([(12i64, "count".to_string())])); + let declared = r.expr(load).to_string(); + assert!(declared.contains("count"), "{declared}"); + assert!(!declared.contains("field_c"), "{declared}"); +} + +/// True when a name appears in a text as a whole word. +fn word(text: &str, name: &str) -> bool { + e5r_decomp::expr::mentions(text, name) +} + +/// Asking for one function gives the same body as asking for all of them. +/// +/// The callee table used to hold only what was asked for, so a call to +/// anything else rendered as an anonymous name with no arguments. That is two +/// different answers for one function depending on how the question was +/// phrased, and the one you get alone is the wrong one: it drops the +/// arguments, which is undefined behaviour in the emitted C. +#[test] +fn a_function_decompiles_the_same_alone_as_in_a_whole_program() { + for name in ["dt-calls.a64.O2", "driver.a64.O2", "dt-control.x64.O2"] { + let Some(p) = open(name) else { continue }; + let every: Vec<&e5r_analysis::Function> = p + .functions_by_address() + .filter(|f| f.is_complete()) + .collect(); + let whole = e5r_api::decompile_program(&p, &every); + assert!(!whole.functions.is_empty(), "{name} decompiled nothing"); + for d in &whole.functions { + let Some(f) = p.function(d.addr) else { + continue; + }; + let alone = e5r_api::decompile_function(&p, f); + assert_eq!( + alone.text, d.text, + "{name}: {} differs alone from how it reads in the whole program", + d.name + ); + } + } +} diff --git a/crates/e5r-api/tests/classes.rs b/crates/e5r-api/tests/classes.rs new file mode 100644 index 0000000..886ddd3 --- /dev/null +++ b/crates/e5r-api/tests/classes.rs @@ -0,0 +1,858 @@ +//! C++ class recovery, measured against a hierarchy known by construction. +//! +//! `fixtures/cpp/hierarchy.cpp` is the oracle: the expected hierarchy below is +//! written out from that source, not derived from what the tool found, so a +//! reader who disagrees with the test can settle it by reading the source. The +//! same program is built with type information and with `-fno-rtti`, at two +//! optimization levels on two architectures, which is what makes the +//! degradation a measurement rather than an assumption. +//! +//! Names are checked against `c++filt` where it is installed, because a +//! demangler measured against itself measures nothing. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_api::classes::{Abi, Basis, Class, Role, Rtti, TypeInfoKind}; +use e5r_core::Strength; +use e5r_format::LoadOptions; + +/// The hierarchy `fixtures/cpp/hierarchy.cpp` declares: for each class, its +/// direct bases with the byte offset of the base subobject. +/// +/// A virtual base has no such offset. What the ABI stores for one is the place +/// in the virtual table where the real offset is found at run time, because a +/// virtual base moves depending on the complete object, so the expectation for +/// one of those is that it comes back marked virtual and measured backwards +/// into the table. Everything else is the offset the compiler must lay out: +/// `Leftish` is a vtable pointer plus one word, so `Rightish` starts at 16. +/// One class and what it inherits: the base's name, the byte offset of the +/// base subobject, and whether the inheritance is virtual. +type Bases = &'static [(&'static str, i64, bool)]; + +const HIERARCHY: &[(&str, Bases)] = &[ + ("Base", &[]), + ("Single", &[("Base", 0, false)]), + ("Leftish", &[]), + ("Rightish", &[]), + ("Pair", &[("Leftish", 0, false), ("Rightish", 16, false)]), + ("Root", &[]), + ("ViaA", &[("Root", 0, true)]), + ("ViaB", &[("Root", 0, true)]), + ("Join", &[("ViaA", 0, false), ("ViaB", 16, false)]), +]; + +/// Which of the three type-information classes each one has to be, which +/// follows from the source: no bases, one public non-virtual base at zero, or +/// anything else. +const KINDS: &[(&str, TypeInfoKind)] = &[ + ("Base", TypeInfoKind::Class), + ("Leftish", TypeInfoKind::Class), + ("Rightish", TypeInfoKind::Class), + ("Root", TypeInfoKind::Class), + ("Single", TypeInfoKind::SingleInheritance), + ("Pair", TypeInfoKind::MultipleInheritance), + ("ViaA", TypeInfoKind::MultipleInheritance), + ("ViaB", TypeInfoKind::MultipleInheritance), + ("Join", TypeInfoKind::MultipleInheritance), +]; + +/// Every build of the fixture that carries type information. +const WITH_RTTI: &[&str] = &[ + "cpp-hierarchy.a64.O0.rtti", + "cpp-hierarchy.a64.O2.rtti", + "cpp-hierarchy.x64.O0.rtti", + "cpp-hierarchy.x64.O2.rtti", +]; + +/// The same source in the Microsoft C++ ABI, linked as a PE, on both machines. +const MSVC: &[&str] = &[ + "cpp-hierarchy.win-x64.O0.rtti.exe", + "cpp-hierarchy.win-x64.O2.rtti.exe", + "cpp-hierarchy.win-a64.O0.rtti.exe", + "cpp-hierarchy.win-a64.O2.rtti.exe", +]; + +/// The same program with `-fno-rtti`. +const WITHOUT_RTTI: &[&str] = &[ + "cpp-hierarchy.a64.O0.nortti", + "cpp-hierarchy.a64.O2.nortti", + "cpp-hierarchy.x64.O0.nortti", + "cpp-hierarchy.x64.O2.nortti", +]; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let path = corpus()?.join(name); + let data = std::fs::read(path).ok()?; + let mut obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + // A shared library writes its tables at load time; these fixtures are + // static and need none, but taking the same path as a real library means + // the test exercises what a user's binary will. + e5r_api::apply_relative_relocations(&mut obj); + Some(analyze(obj, &Options::default())) +} + +/// Recovering the fields through `this` lifts every member function, which is +/// the expensive half and not what most of these tests are about. +fn quick() -> e5r_api::classes::Options { + e5r_api::classes::Options { + stores: true, + fields: false, + } +} + +/// The hierarchy, against the source, whichever ABI wrote it down. +/// +/// `into_the_table` says whether a virtual base's stored number is an offset +/// backwards into the virtual table, which is what the Itanium ABI puts there. +/// The Microsoft ABI puts the displacement of the virtual base table instead, +/// and neither number is an offset the source declared, so neither is compared +/// against one. +fn check_hierarchy(fixture: &str, classes: &[Class], into_the_table: bool) { + let by_name: BTreeMap<&str, &Class> = classes + .iter() + .filter_map(|c| c.name.as_deref().map(|n| (n, c))) + .collect(); + + for (class, bases) in HIERARCHY { + let c = by_name + .get(class) + .unwrap_or_else(|| panic!("{fixture}: {class} was not recovered at all")); + let got: Vec<(&str, i64, bool)> = c + .bases + .iter() + .map(|b| (b.name.as_str(), b.offset, b.is_virtual)) + .collect(); + assert_eq!( + got.len(), + bases.len(), + "{fixture}: {class} came back with bases {got:?}, the source declares {bases:?}" + ); + for (got, want) in got.iter().zip(bases.iter()) { + assert_eq!(got.0, want.0, "{fixture}: {class} base name"); + assert_eq!(got.2, want.2, "{fixture}: {class} base {} virtual", want.0); + if want.2 { + if into_the_table { + assert!( + got.1 < 0, + "{fixture}: {class} virtual base {} has table offset {}, \ + which does not point into the table", + want.0, + got.1 + ); + } + } else { + assert_eq!( + got.1, want.1, + "{fixture}: {class} base {} is at offset {}, the source puts it at {}", + want.0, got.1, want.1 + ); + } + } + for b in &c.bases { + assert!(b.is_public, "{fixture}: {class} base {} is public", b.name); + assert_eq!( + b.basis.strength(), + Strength::Proven, + "{fixture}: {class} base {} came back as {}, and a base the type \ + information states is not a guess", + b.name, + b.basis.as_str() + ); + } + } +} + +#[test] +fn the_hierarchy_is_the_one_the_source_declares() { + let mut ran = 0; + for fixture in WITH_RTTI { + let Some(p) = open(fixture) else { continue }; + ran += 1; + let found = e5r_api::classes_with(&p, &quick()); + assert_eq!( + found.rtti, + Rtti::Present, + "{fixture}: built with type information but none was read" + ); + assert_eq!(found.abi, Abi::Itanium, "{fixture}"); + check_hierarchy(fixture, &found.classes, true); + } + assert!(ran > 0, "no fixture with type information was built"); +} + +/// The same source through the Microsoft ABI, whose type information is a +/// different set of structures reached by relative addresses, and whose base +/// array lists every base in the lattice rather than the direct ones. +#[test] +fn the_microsoft_abi_gives_the_same_hierarchy() { + let mut ran = 0; + for fixture in MSVC { + let Some(p) = open(fixture) else { continue }; + ran += 1; + let found = e5r_api::classes_with(&p, &quick()); + assert_eq!(found.rtti, Rtti::Present, "{fixture}"); + assert_eq!(found.abi, Abi::Msvc, "{fixture}"); + check_hierarchy(fixture, &found.classes, false); + // Nothing in a linked PE with no symbols named any of this. + assert_eq!(found.named_by_symbol, 0, "{fixture}"); + assert!(found.named_by_rtti > 0, "{fixture}"); + for c in &found.classes { + assert_eq!( + c.kind, + None, + "{fixture}: {} has an Itanium type-information kind", + c.display_name() + ); + } + } + assert!(ran > 0, "no Microsoft ABI fixture was built"); +} + +/// Without type information a PE gives nothing at all, and says so. +/// +/// The Itanium scan finds a table by its offset-to-top and type-information +/// words; a Microsoft vftable has neither, so with the locators gone there is +/// nothing left that tells a table of function pointers from any other array +/// of them. That is the honest answer and it is worse than the Itanium one, +/// where the tables are still found and only the hierarchy is lost. +#[test] +fn a_microsoft_binary_without_rtti_gives_nothing() { + for fixture in [ + "cpp-hierarchy.win-x64.O0.nortti.exe", + "cpp-hierarchy.win-x64.O2.nortti.exe", + "cpp-hierarchy.win-a64.O0.nortti.exe", + "cpp-hierarchy.win-a64.O2.nortti.exe", + ] { + let Some(p) = open(fixture) else { continue }; + let found = e5r_api::classes_with(&p, &quick()); + assert_eq!(found.abi, Abi::Unknown, "{fixture}"); + assert!(found.classes.is_empty(), "{fixture}"); + assert_eq!(found.rtti, Rtti::NoClasses, "{fixture}"); + } +} + +/// The Microsoft ABI's constructors, found the same way the Itanium ones are: +/// by the vftable pointer the function writes into its own object. +/// +/// Six of the nine classes come back on the AArch64 PE. The three missing are +/// `ViaA`, `ViaB` and `Join`, the ones with a virtual base: the Microsoft ABI +/// puts their pointer at an offset the constructor loads out of a virtual base +/// table at run time, so the store is not at a constant place in the object +/// and nothing static says which class it belongs to. The x86-64 PE gives +/// fewer still, and that is not about the ABI: at `-O0` the compiler spills +/// `this` and reloads it across the base constructor call, and the stack +/// promotion in `e5r-ir` does not follow a reload past a call. +#[test] +fn the_microsoft_constructors_come_out_of_what_they_write() { + let declared: BTreeSet<&str> = HIERARCHY.iter().map(|(name, _)| *name).collect(); + let mut ran = 0; + for fixture in ["cpp-hierarchy.win-a64.O0.rtti.exe"] { + let Some(p) = open(fixture) else { continue }; + ran += 1; + let found = e5r_api::classes_with(&p, &quick()); + let mut built: BTreeSet<&str> = BTreeSet::new(); + for c in &found.classes { + for m in c.constructors() { + assert_eq!( + m.basis, + Basis::VptrStore, + "{fixture}: {} at {} is a constructor because a name said so, and a \ + linked PE with no symbols has no names", + m.class, + m.addr + ); + assert_eq!(m.basis.strength(), Strength::Inferred, "{fixture}"); + let this = m.this.as_ref().expect("a member has a typed this"); + assert_eq!(this.declaration, format!("{} *this", m.class), "{fixture}"); + assert!( + declared.contains(m.class.as_str()), + "{fixture}: a constructor of {}, which the source does not declare", + m.class + ); + built.insert(c.name.as_deref().unwrap_or_default()); + } + } + assert!( + built.len() >= 6, + "{fixture}: {} classes got a constructor out of the stores, six were \ + expected: {built:?}", + built.len() + ); + } + assert!(ran > 0, "no Microsoft ABI fixture was built"); +} + +#[test] +fn each_class_has_the_type_information_kind_its_shape_requires() { + for fixture in WITH_RTTI { + let Some(p) = open(fixture) else { continue }; + let found = e5r_api::classes_with(&p, &quick()); + for (class, kind) in KINDS { + let c = found + .classes + .iter() + .find(|c| c.name.as_deref() == Some(*class)) + .unwrap_or_else(|| panic!("{fixture}: {class} was not recovered")); + assert_eq!( + c.kind, + Some(*kind), + "{fixture}: {class} came back as {:?}, the ABI requires {}", + c.kind.map(TypeInfoKind::as_str), + kind.as_str() + ); + } + } +} + +/// The same hierarchy out of a binary with no symbols at all. +/// +/// With the ABI's own `__class_type_info` symbols gone there is nothing to +/// compare a type-information object's first word against, so which of the +/// three kinds it is has to come out of the structure. That is a conclusion +/// and not a reading, and the strength says so. +#[test] +fn a_stripped_binary_still_gives_the_hierarchy_and_says_it_is_inferred() { + let mut ran = 0; + for fixture in [ + "cpp-hierarchy.a64.O0.rtti.stripped", + "cpp-hierarchy.a64.O2.rtti.stripped", + ] { + let Some(p) = open(fixture) else { continue }; + ran += 1; + let found = e5r_api::classes_with(&p, &quick()); + assert_eq!(found.rtti, Rtti::Present, "{fixture}"); + assert_eq!( + found.named_by_symbol, 0, + "{fixture}: a stripped binary has no symbols to name a table" + ); + for (class, bases) in HIERARCHY { + let Some(c) = found + .classes + .iter() + .find(|c| c.name.as_deref() == Some(*class)) + else { + panic!("{fixture}: {class} was not recovered"); + }; + let got: Vec<(&str, bool)> = c + .bases + .iter() + .map(|b| (b.name.as_str(), b.is_virtual)) + .collect(); + let want: Vec<(&str, bool)> = bases.iter().map(|b| (b.0, b.2)).collect(); + assert_eq!(got, want, "{fixture}: {class} bases"); + for b in &c.bases { + assert_eq!( + b.basis, + Basis::RttiShape, + "{fixture}: {class} base {} claims to rest on {}, but nothing in a \ + stripped binary states the kind", + b.name, + b.basis.as_str() + ); + assert_eq!(b.basis.strength(), Strength::Inferred); + } + } + } + assert!(ran > 0, "no stripped fixture was built"); +} + +/// The degradation, measured rather than assumed. +#[test] +fn without_rtti_there_is_no_hierarchy_and_the_answer_says_so() { + let mut ran = 0; + for fixture in WITHOUT_RTTI { + let Some(p) = open(fixture) else { continue }; + ran += 1; + let found = e5r_api::classes_with(&p, &quick()); + assert_eq!( + found.rtti, + Rtti::Absent, + "{fixture}: built with -fno-rtti, so there is nothing to read" + ); + assert!( + found.tables > 0, + "{fixture}: the tables are still there without type information" + ); + for c in &found.classes { + assert!( + c.bases.is_empty(), + "{fixture}: {} came back with bases {:?}, and nothing in this binary \ + says what they are", + c.display_name(), + c.bases.iter().map(|b| &b.name).collect::>() + ); + assert_eq!(c.kind, None, "{fixture}: {}", c.display_name()); + assert!(c.typeinfo.is_none(), "{fixture}: {}", c.display_name()); + } + // The names that survive are the ones the symbol table carries, and + // they are proven for that reason and no other. + for c in found.classes.iter().filter(|c| c.name.is_some()) { + assert_eq!(c.basis, Basis::MangledName, "{fixture}: {:?}", c.name); + } + } + assert!(ran > 0, "no fixture without type information was built"); +} + +/// What the two builds cost each other, stated as numbers so a regression in +/// either direction shows up. +#[test] +fn the_rtti_build_recovers_strictly_more_than_the_one_without() { + for (with, without) in WITH_RTTI.iter().zip(WITHOUT_RTTI.iter()) { + let (Some(a), Some(b)) = (open(with), open(without)) else { + continue; + }; + let rtti = e5r_api::classes_with(&a, &quick()); + let none = e5r_api::classes_with(&b, &quick()); + let bases: usize = rtti.classes.iter().map(|c| c.bases.len()).sum(); + // Base under Single, Leftish and Rightish under Pair, Root under + // each of ViaA and ViaB, and ViaA and ViaB under Join. + assert_eq!( + bases, 7, + "{with}: the source declares seven base relationships, {bases} came back" + ); + assert_eq!( + none.classes.iter().map(|c| c.bases.len()).sum::(), + 0, + "{without}" + ); + let named = |c: &e5r_api::Classes| c.classes.iter().filter(|x| x.name.is_some()).count(); + assert!( + named(&rtti) >= named(&none), + "{with}: type information named {} classes, without it {} were named", + named(&rtti), + named(&none) + ); + assert!( + rtti.named_by_rtti > 0, + "{with}: no table got its name from type information, so nothing was gained" + ); + assert_eq!( + none.named_by_rtti, 0, + "{without}: there is no type information to name anything" + ); + } +} + +/// Every class name, against `c++filt` on the fixture's own `_ZTI` symbols. +#[test] +fn the_class_names_are_the_ones_cxxfilt_gives() { + let Some(dir) = corpus() else { return }; + for fixture in WITH_RTTI { + let path = dir.join(fixture); + if !path.exists() { + continue; + } + let Some(symbols) = symbol_names(&path) else { + return; + }; + let mangled: Vec = symbols + .iter() + .filter(|s| s.starts_with("_ZTI")) + .cloned() + .collect(); + if mangled.is_empty() { + continue; + } + let Some(expected) = cxxfilt(&mangled) else { + return; + }; + let expected: BTreeSet = expected + .iter() + .filter_map(|t| t.strip_prefix("typeinfo for ").map(str::to_string)) + // The ABI's own type-information classes are named here too; the + // fixture defines their vtable symbols but not their type + // information, so they are not classes of this program. + .filter(|n| !n.starts_with("__cxxabiv1::")) + .collect(); + + let Some(p) = open(fixture) else { continue }; + let found: BTreeSet = e5r_api::classes_with(&p, &quick()) + .classes + .iter() + .filter_map(|c| c.name.clone()) + .collect(); + for name in &expected { + assert!( + found.contains(name), + "{fixture}: c++filt names a class {name:?} that did not come back; got {found:?}" + ); + } + } +} + +/// Constructors and destructors, by name and by what they write. +/// +/// The names are the oracle for the shapes: what a stripped binary infers has +/// to be a subset of what the symbols prove, at the same addresses and with +/// the same roles. A shape that finds something the names do not is a false +/// positive, and this is where it would show. +#[test] +fn what_the_shapes_find_is_what_the_names_prove() { + let mut ran = 0; + for (named, stripped, least) in [ + ( + "cpp-hierarchy.a64.O0.rtti", + "cpp-hierarchy.a64.O0.rtti.stripped", + 8, + ), + // At -O2 the compiler inlines every constructor in this program into + // its one caller and drops the vtable-pointer store from every + // destructor, because nothing reads it back. There is then nothing of + // the shape to find, and the number that matters is that nothing + // wrong is claimed either. + ( + "cpp-hierarchy.a64.O2.rtti", + "cpp-hierarchy.a64.O2.rtti.stripped", + 0, + ), + ] { + let (Some(a), Some(b)) = (open(named), open(stripped)) else { + continue; + }; + ran += 1; + let proven = e5r_api::classes_with(&a, &quick()); + let inferred = e5r_api::classes_with(&b, &quick()); + + // What the symbols say, by address. + let mut truth: BTreeMap = BTreeMap::new(); + for c in &proven.classes { + for m in &c.members { + // A member with a C++ symbol is proven by it. The exception is + // `__cxa_pure_virtual`, which sits in tables without being a + // member of anything, and which a slot is all there is for. + if m.name.as_deref().is_some_and(|n| n.contains("::")) { + assert_eq!( + m.basis, + Basis::MangledName, + "{named}: {:?} at {} rests on {}, but its symbol is right there", + m.name, + m.addr, + m.basis.as_str() + ); + } + truth.insert(m.addr.get(), (m.class.clone(), m.role)); + } + } + if least > 0 { + assert!( + truth.values().any(|(_, r)| *r == Role::Constructor), + "{named}: no constructor came out of the symbol table" + ); + } + + let mut checked = 0; + for c in &inferred.classes { + for m in c.members.iter().filter(|m| { + matches!( + m.role, + Role::Constructor | Role::Destructor | Role::DeletingDestructor + ) + }) { + assert_eq!( + m.basis.strength(), + Strength::Inferred, + "{stripped}: {} at {} claims to be {}, and nothing in a stripped \ + binary proves that", + m.class, + m.addr, + m.basis.as_str() + ); + let (class, role) = truth.get(&m.addr.get()).unwrap_or_else(|| { + panic!( + "{stripped}: called {} at {} a {}, and the symbols of {named} \ + say nothing is there", + m.class, + m.addr, + m.role.as_str() + ) + }); + assert_eq!( + (&m.class, m.role), + (class, *role), + "{stripped}: {} at {} is a {} of {}, the symbols say a {} of {}", + m.class, + m.addr, + m.role.as_str(), + m.class, + role.as_str(), + class + ); + checked += 1; + } + } + assert!( + checked >= least, + "{stripped}: only {checked} constructors and destructors were inferred, \ + and {least} were expected" + ); + } + assert!(ran > 0, "neither half of the pair was built"); +} + +/// Every class with a virtual destructor spends its first two table slots on +/// it, complete then deleting. That is the ABI, and it is what has to come +/// back. +#[test] +fn the_two_destructor_slots_come_back_as_the_two_destructors() { + // At -O0 only: at -O2 the vtable-pointer store a destructor makes is dead + // and the compiler drops it, so nothing identifies slot zero and the rule + // that keys off it never fires. That is measured in the subset gate above. + for fixture in ["cpp-hierarchy.a64.O0.rtti.stripped"] { + let Some(p) = open(fixture) else { continue }; + let found = e5r_api::classes_with(&p, &quick()); + let mut pairs = 0; + for c in &found.classes { + let complete = c.members.iter().find(|m| m.slot == Some(0)); + let deleting = c.members.iter().find(|m| m.slot == Some(1)); + let (Some(complete), Some(deleting)) = (complete, deleting) else { + continue; + }; + if complete.role != Role::Destructor { + continue; + } + assert_eq!( + deleting.role, + Role::DeletingDestructor, + "{fixture}: {} has a destructor in slot 0 and {} in slot 1", + c.display_name(), + deleting.role.as_str() + ); + pairs += 1; + } + assert!(pairs >= 5, "{fixture}: only {pairs} destructor pairs found"); + } +} + +/// `this` typing: every member function of a class takes a pointer to it, and +/// what the code touches through that pointer is the class's fields. +/// +/// The counts differ by architecture and that is not about the typing. On +/// AArch64 the `this` that a -O0 prologue spills and reloads is promoted back +/// to the incoming argument; on x86-64 that promotion does not survive a +/// function that calls, so a constructor that builds its bases first comes +/// back with no fields at all. The class and the declaration are right either +/// way, and the number here is what says when that gap closes. +#[test] +fn every_member_takes_a_this_of_its_own_class() { + let mut ran = 0; + for (fixture, least) in [ + ("cpp-hierarchy.a64.O0.rtti", 9), + ("cpp-hierarchy.x64.O0.rtti", 4), + ] { + let Some(p) = open(fixture) else { continue }; + ran += 1; + let found = e5r_api::classes(&p); + let mut described = 0; + for c in &found.classes { + for m in &c.members { + let this = m + .this + .as_ref() + .unwrap_or_else(|| panic!("{fixture}: {:?} has no typed this", m.name)); + assert_eq!(this.class, m.class, "{fixture}: {:?}", m.name); + assert_eq!( + this.declaration, + format!("{} *this", c.name.clone().unwrap_or_default()), + "{fixture}: {:?}", + m.name + ); + // A typed `this` is never stronger than whatever named the + // class it points at. + assert!(this.basis.strength() >= Strength::Inferred); + } + // Every class here has virtual functions, so offset zero of one of + // its objects is the vtable pointer, and a constructor of it + // writes exactly that. A constructor whose fields did not come + // back at all says nothing either way. + for ctor in c.constructors() { + let fields = &ctor.this.as_ref().unwrap().fields; + if fields.is_empty() { + continue; + } + assert!( + fields.iter().any(|f| f.offset == 0 && f.written), + "{fixture}: {} touched {fields:?} through its own object and wrote \ + nothing at offset zero, where its vtable pointer goes", + ctor.name.clone().unwrap_or_default() + ); + described += 1; + } + } + assert!( + described >= least, + "{fixture}: {described} constructors came back with fields, {least} were expected" + ); + } + assert!(ran > 0, "no fixture was built"); +} + +/// The fields a class's own code touches, against what the source declares. +/// +/// `Single` is a vtable pointer, `Base::id` and `Single::extra`, so its +/// members reach offsets 0, 8 and 16 and nothing past them. +#[test] +fn the_fields_are_inside_the_object_the_source_declares() { + for fixture in ["cpp-hierarchy.a64.O0.rtti", "cpp-hierarchy.x64.O0.rtti"] { + let Some(p) = open(fixture) else { continue }; + let found = e5r_api::classes(&p); + let c = found + .classes + .iter() + .find(|c| c.name.as_deref() == Some("Single")) + .unwrap(); + for f in &c.fields { + assert!( + (0..=16).contains(&f.offset), + "{fixture}: Single has a field at {}, and the source declares three words", + f.offset + ); + assert_eq!(f.basis, Basis::Accesses); + } + assert_eq!( + c.size(), + Some(24), + "{fixture}: Single is a vtable pointer, an id and an extra" + ); + } +} + +/// Over a real library, what the shapes infer has to agree with what the +/// symbols prove, on the class as well as on the role. +/// +/// `libstdc++` is where this is worth measuring: 283 tables, 337 classes and +/// enough virtual inheritance and enough polymorphic members to break a rule +/// that only looks at whether a vtable pointer was written. Eighteen functions +/// used to come back a constructor of the class of a member they set the +/// pointer of, `~basic_ofstream` as a `basic_filebuf` among them, which is the +/// wrong class and the wrong role at once. Requiring the store to land at the +/// offset the table itself says its subobject begins at is what rules those +/// out, and this is the gate that keeps them out. +/// +/// The symbols are cleared after the analysis rather than before, so function +/// discovery still had them. That makes this a test of the class rules with no +/// names, not a simulation of a stripped file end to end. +#[test] +fn nothing_inferred_from_a_real_library_contradicts_its_symbols() { + let path = Path::new("/usr/lib/aarch64-linux-gnu/libstdc++.so.6"); + let Ok(data) = std::fs::read(path) else { + return; + }; + let Ok(mut obj) = e5r_format::load(&data, &LoadOptions::default()) else { + return; + }; + // Every virtual table in a shared library is zeroes in the file until the + // loader fills it in, so without this there is nothing to read at all. + assert!(e5r_api::apply_relative_relocations(&mut obj) > 0); + let p = analyze(obj, &Options::default()); + + let named = e5r_api::classes_with(&p, &quick()); + assert_eq!(named.rtti, Rtti::Present); + assert!(named.classes.len() > 200, "{}", named.classes.len()); + let truth: BTreeMap = named + .classes + .iter() + .flat_map(|c| c.members.iter()) + .map(|m| (m.addr.get(), (m.class.clone(), m.role))) + .collect(); + + let mut bare = analyze( + e5r_format::load(&data, &LoadOptions::default()).unwrap(), + &Options::default(), + ); + assert!(e5r_api::apply_relative_relocations(&mut bare.object) > 0); + bare.object.symbols.clear(); + let stripped = e5r_api::classes_with(&bare, &quick()); + assert_eq!(stripped.named_by_symbol, 0); + + let mut checked = 0; + for m in stripped.classes.iter().flat_map(|c| c.members.iter()) { + if !matches!( + m.role, + Role::Constructor | Role::Destructor | Role::DeletingDestructor + ) { + continue; + } + assert_eq!(m.basis.strength(), Strength::Inferred, "{}", m.addr); + let Some((class, role)) = truth.get(&m.addr.get()) else { + panic!( + "called {} at {} a {}, and the symbols say nothing is there", + m.class, + m.addr, + m.role.as_str() + ); + }; + assert_eq!( + (&m.class, m.role), + (class, *role), + "{} at {} is a {} of {}, the symbols say a {} of {}", + m.class, + m.addr, + m.role.as_str(), + m.class, + role.as_str(), + class + ); + checked += 1; + } + assert!( + checked >= 200, + "only {checked} constructors and destructors were inferred out of a library \ + whose symbols prove hundreds" + ); +} + +/// A C program has no classes, and nothing here invents any. +#[test] +fn a_c_program_has_no_classes() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let found = e5r_api::classes(&p); + assert_eq!(found.rtti, Rtti::NoClasses); + assert!( + found.classes.is_empty(), + "found {:?} in a C program", + found + .classes + .iter() + .map(|c| c.display_name()) + .collect::>() + ); +} + +/// Symbol names out of `readelf`, which is the file's own account of itself. +fn symbol_names(path: &Path) -> Option> { + let out = Command::new("readelf").arg("-sW").arg(path).output().ok()?; + if !out.status.success() { + return None; + } + Some( + String::from_utf8_lossy(&out.stdout) + .lines() + .filter_map(|l| l.split_whitespace().nth(7)) + .map(|s| s.split('@').next().unwrap_or(s).to_string()) + .collect(), + ) +} + +/// `c++filt`, one name per line, in order. +fn cxxfilt(names: &[String]) -> Option> { + let out = Command::new("c++filt").args(names).output().ok()?; + if !out.status.success() { + return None; + } + Some( + String::from_utf8_lossy(&out.stdout) + .lines() + .map(str::to_string) + .collect(), + ) +} diff --git a/crates/e5r-api/tests/dataflow.rs b/crates/e5r-api/tests/dataflow.rs new file mode 100644 index 0000000..80de4e7 --- /dev/null +++ b/crates/e5r-api/tests/dataflow.rs @@ -0,0 +1,415 @@ +//! The dataflow queries, against calls whose answers are known by +//! construction. +//! +//! `fixtures/dataflow/bounds.c` holds one wrapper per way of bounding a +//! length: a literal, a comparison that guards the call, a mask, a read +//! straight out of memory, and an argument that arrives from the caller. The +//! answer for each is decided by the source rather than by what the analysis +//! happens to manage, so this asserts the whole table rather than that the +//! query returned something. +//! +//! Every expected address is worked out from the cross reference index or by +//! decoding the function, which is a different path through the program than +//! the query takes. A test that asked the query where the call was and then +//! checked the query agreed would be checking nothing. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, XrefKind, analyze}; +use e5r_core::Addr; +use e5r_format::LoadOptions; + +/// The four builds: two architectures, two optimization levels. +const FIXTURES: [&str; 4] = [ + "df-bounds.a64.O0", + "df-bounds.a64.O2", + "df-bounds.x64.O0", + "df-bounds.x64.O2", +]; + +fn build() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(build()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// One row as a map from column to what it prints as. +type Answer = Vec>; + +fn ask(p: &Program, query: &str) -> Answer { + let answer = e5r_api::query::run(p, query).unwrap_or_else(|e| panic!("{query:?}: {e}")); + answer + .rows + .iter() + .map(|row| { + row.values + .iter() + .map(|(name, value)| (name.to_string(), value.to_string())) + .collect() + }) + .collect() +} + +fn field(row: &BTreeMap, name: &str) -> String { + row.get(name) + .unwrap_or_else(|| panic!("no column {name:?}")) + .clone() +} + +/// The one call from `caller` to `callee`, found through the cross references +/// rather than through the query being tested. +fn call_from(p: &Program, caller: &str, callee: &str) -> Addr { + let target = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(callee)) + .unwrap_or_else(|| panic!("no function named {callee}")); + let mut found: Vec = p + .xrefs + .to(target.entry) + .iter() + .filter(|x| x.kind == XrefKind::Call) + .filter(|x| { + p.function_at(x.from) + .and_then(|f| f.name.clone()) + .as_deref() + == Some(caller) + }) + .map(|x| x.from) + .collect(); + found.sort(); + found.dedup(); + assert_eq!( + found.len(), + 1, + "{caller} should hold exactly one call to {callee}, found {found:?}" + ); + found[0] +} + +/// The one indirect call in a function, found by decoding it. +fn indirect_call_in(p: &Program, caller: &str) -> Addr { + let f = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(caller)) + .unwrap_or_else(|| panic!("no function named {caller}")); + // By the decoder's own classification of the instruction, which is a + // different path to the answer than lifting it. Matched by name because + // the architecture crate is not a dependency of this one. + let found: Vec = p + .instructions(f) + .iter() + .filter(|i| format!("{:?}", i.flow) == "IndirectCall") + .map(|i| i.addr) + .collect(); + assert_eq!( + found.len(), + 1, + "{caller} should hold exactly one indirect call, found {found:?}" + ); + found[0] +} + +/// What one call to `memcpy` should say about its third argument. +struct Expect { + function: &'static str, + verdict: &'static str, + basis: &'static str, + bound: &'static str, + strength: &'static str, +} + +/// The whole table, per build. +/// +/// Only one cell differs between builds, and it differs for a reason worth +/// pinning: at `-O0` on x86-64 the length lives in a frame slot that stack +/// promotion refuses, so the comparison that guards the call is a compare +/// against memory and the guard is not visible. The answer then has to be +/// `unknown`, which is the point. Claiming `unconstrained` there would be +/// reporting a limit of the analysis as a fact about the program. If +/// promotion learns that frame, this cell becomes the same `bounded` as the +/// others and this table is what says so. +fn expected(fixture: &str) -> Vec { + let spilled = fixture == "df-bounds.x64.O0"; + vec![ + Expect { + function: "copy_literal_length", + verdict: "bounded", + basis: "literal", + bound: "32", + strength: "proven", + }, + Expect { + function: "copy_checked_length", + verdict: if spilled { "unknown" } else { "bounded" }, + basis: if spilled { "no evidence" } else { "guard" }, + bound: if spilled { "-" } else { "0..=64" }, + strength: if spilled { "heuristic" } else { "inferred" }, + }, + Expect { + function: "copy_masked_length", + verdict: "bounded", + basis: "narrowed", + bound: "0..=63", + strength: "inferred", + }, + Expect { + function: "copy_untrusted_length", + verdict: "unconstrained", + basis: "no constraint", + bound: "-", + strength: "inferred", + }, + Expect { + function: "copy_argument_length", + verdict: "unknown", + basis: if spilled { + "no evidence" + } else { + "from the caller" + }, + bound: "-", + strength: "heuristic", + }, + ] +} + +#[test] +fn every_memcpy_length_gets_the_answer_its_source_dictates() { + let mut ran = 0; + for fixture in FIXTURES { + let Some(p) = open(fixture) else { continue }; + ran += 1; + let want = expected(fixture); + + // The two halves of the question, asked separately: a call is in one + // or the other and never in both. + let bounded = ask(&p, "calls to \"memcpy\" where arg3 is bounded"); + let unbounded = ask(&p, "calls to \"memcpy\" where arg3 is not bounded"); + + for case in &want { + let rows = if case.verdict == "bounded" { + &bounded + } else { + &unbounded + }; + let matching: Vec<&BTreeMap> = rows + .iter() + .filter(|r| field(r, "function") == case.function) + .collect(); + assert_eq!( + matching.len(), + 1, + "{fixture}: {} should appear once in the {} half, appeared {} times", + case.function, + case.verdict, + matching.len() + ); + let row = matching[0]; + let at = call_from(&p, case.function, "memcpy"); + assert_eq!( + field(row, "address"), + format!("{at}"), + "{fixture}: {} names the wrong call site", + case.function + ); + assert_eq!(field(row, "target"), "memcpy"); + assert_eq!(field(row, "argument"), "arg3"); + assert_eq!( + field(row, "verdict"), + case.verdict, + "{fixture}: {}", + case.function + ); + assert_eq!( + field(row, "basis"), + case.basis, + "{fixture}: {}", + case.function + ); + assert_eq!( + field(row, "bound"), + case.bound, + "{fixture}: {}", + case.function + ); + assert_eq!( + field(row, "strength"), + case.strength, + "{fixture}: {}", + case.function + ); + } + + // Exactly these calls and no others: a query that also returned the + // call in `_start` or one inside `memcpy` would be wrong in a way the + // per-case checks above cannot see. + let mut seen: Vec = bounded + .iter() + .chain(unbounded.iter()) + .map(|r| field(r, "function")) + .collect(); + seen.sort(); + let mut all: Vec = want.iter().map(|c| c.function.to_string()).collect(); + all.sort(); + assert_eq!(seen, all, "{fixture}: the two halves are not the whole"); + } + assert!(ran > 0, "no dataflow fixture was built"); +} + +#[test] +fn the_two_ways_of_not_being_bounded_are_never_confused() { + for fixture in FIXTURES { + let Some(p) = open(fixture) else { continue }; + // The proved half on its own. `copy_untrusted_length` reads the length + // out of memory and hands it over untouched; `copy_argument_length` + // takes it from its caller, which this function cannot see, and that + // is a different sentence. + let proved = ask(&p, "calls to \"memcpy\" where arg3 is unconstrained"); + let names: Vec = proved.iter().map(|r| field(r, "function")).collect(); + assert_eq!( + names, + vec!["copy_untrusted_length".to_string()], + "{fixture}: only the value read from memory is proved unbounded" + ); + assert_eq!( + field(&proved[0], "address"), + format!("{}", call_from(&p, "copy_untrusted_length", "memcpy")) + ); + + let unknown = ask(&p, "calls to \"memcpy\" where arg3 is unknown"); + let names: Vec = unknown.iter().map(|r| field(r, "function")).collect(); + assert!( + names.contains(&"copy_argument_length".to_string()), + "{fixture}: a length that arrives from the caller is unknown, got {names:?}" + ); + assert!( + !names.contains(&"copy_untrusted_length".to_string()), + "{fixture}: a proved answer must not also be reported as unknown" + ); + } +} + +#[test] +fn an_indirect_call_is_found_and_a_direct_one_is_named() { + for fixture in FIXTURES { + let Some(p) = open(fixture) else { continue }; + + let rows = ask(&p, "calls where target is indirect"); + let names: Vec = rows.iter().map(|r| field(r, "function")).collect(); + assert_eq!( + names, + vec!["copy_through_pointer".to_string()], + "{fixture}: the only computed call in the image is the one through \ + the function pointer" + ); + assert_eq!( + field(&rows[0], "address"), + format!("{}", indirect_call_in(&p, "copy_through_pointer")), + "{fixture}: the indirect call is at the wrong address" + ); + assert_eq!(field(&rows[0], "target"), "indirect"); + // That the machine computes its target is what the encoding says. + assert_eq!(field(&rows[0], "strength"), "proven"); + + let rows = ask(&p, "calls to \"strcpy\""); + let names: Vec = rows.iter().map(|r| field(r, "function")).collect(); + assert_eq!(names, vec!["copy_string".to_string()], "{fixture}"); + assert_eq!( + field(&rows[0], "address"), + format!("{}", call_from(&p, "copy_string", "strcpy")), + "{fixture}" + ); + } +} + +#[test] +fn a_function_that_hands_a_call_something_it_read_is_the_one_reported() { + for fixture in FIXTURES { + let Some(p) = open(fixture) else { continue }; + let rows = ask( + &p, + "functions where reads any argument of a call to \"system\"", + ); + let names: Vec = rows.iter().map(|r| field(r, "name")).collect(); + // `run_fixed` passes a command the program carries, which is not a + // value it read from anywhere. + assert_eq!( + names, + vec!["run_from_input".to_string()], + "{fixture}: only the function that passes on what it was given" + ); + } +} + +#[test] +fn the_values_reaching_an_argument_are_named_with_their_strength() { + for fixture in FIXTURES { + let Some(p) = open(fixture) else { continue }; + let rows = ask(&p, "values reaching arg1 of calls to \"system\""); + let by_function: BTreeMap> = + rows.iter().map(|r| (field(r, "function"), r)).collect(); + assert_eq!( + by_function.keys().cloned().collect::>(), + vec!["run_fixed".to_string(), "run_from_input".to_string()], + "{fixture}: both calls to system have a value reaching their argument" + ); + + let fixed = by_function["run_fixed"]; + assert_eq!(field(fixed, "argument"), "arg1"); + assert_eq!(field(fixed, "source"), "string", "{fixture}"); + assert!( + field(fixed, "detail").contains("/bin/echo hi"), + "{fixture}: the command should be read out of the image, got {:?}", + field(fixed, "detail") + ); + // A literal is in the encoding, which is as known as anything gets. + assert_eq!(field(fixed, "strength"), "proven", "{fixture}"); + assert_eq!( + field(fixed, "address"), + format!("{}", call_from(&p, "run_fixed", "system")), + "{fixture}" + ); + + let passed = by_function["run_from_input"]; + assert_ne!( + field(passed, "source"), + "string", + "{fixture}: a command that arrives from outside is not a literal" + ); + assert_eq!(field(passed, "strength"), "inferred", "{fixture}"); + assert_eq!( + field(passed, "address"), + format!("{}", call_from(&p, "run_from_input", "system")), + "{fixture}" + ); + } +} + +#[test] +fn a_query_about_an_argument_ignores_calls_that_have_no_such_argument() { + for fixture in FIXTURES { + let Some(p) = open(fixture) else { continue }; + // `system` takes one argument, so nothing it is passed can answer a + // question about a third one. The answer is empty rather than every + // call to it, which is what a missing argument would give if the + // negation were taken at face value. + let rows = ask(&p, "calls to \"system\" where arg3 is not bounded"); + assert!( + rows.is_empty(), + "{fixture}: a one-argument call has no third argument to be unbounded, \ + got {} rows", + rows.len() + ); + let rows = ask(&p, "calls to \"system\" where arg1 is not bounded"); + assert!( + !rows.is_empty(), + "{fixture}: the one argument it does take is still a question" + ); + } +} diff --git a/crates/e5r-api/tests/emulate.rs b/crates/e5r-api/tests/emulate.rs new file mode 100644 index 0000000..bec9ed0 --- /dev/null +++ b/crates/e5r-api/tests/emulate.rs @@ -0,0 +1,175 @@ +//! Emulating functions out of object files, against what the processor said. +//! +//! The same case table the lifter's oracle uses, run against the relocatable +//! objects rather than the linked drivers. It exercises a different path: an +//! object's call targets are written as zero and filled in by relocations, so +//! a wrong relocation makes every call land on itself and the answers diverge. + +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_format::LoadOptions; + +fn build() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(build()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// One case: what to call, with what, and what the processor answered. +struct Case { + name: String, + kind: String, + args: Vec, + expected: u64, +} + +fn cases() -> Vec { + let Some(dir) = build() else { + return Vec::new(); + }; + let table = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/portable/cases.txt"); + let Ok(text) = std::fs::read_to_string(table) else { + return Vec::new(); + }; + let Ok(recorded) = std::fs::read(dir.join("driver.a64.O2.out")) else { + return Vec::new(); + }; + + let mut out = Vec::new(); + for (n, line) in text + .lines() + .map(|l| l.split('#').next().unwrap_or("").trim()) + .filter(|l| !l.is_empty()) + .enumerate() + { + let mut parts = line.split_whitespace(); + let head = parts.next().unwrap_or_default(); + let (name, kind) = head.split_once(':').unwrap_or((head, "u64")); + // Only the cases whose arguments are numbers: a case that points at a + // global belongs to the driver that defines it. + let mut args = Vec::new(); + let mut usable = true; + for token in parts { + match parse(token) { + Some(v) => args.push(v), + None => usable = false, + } + } + // Floating results and floating arguments go through other registers, + // which this surface does not set. + if !usable || kind.starts_with('f') || line.contains('.') { + continue; + } + let Some(bytes) = recorded.get(n * 8..n * 8 + 8) else { + continue; + }; + out.push(Case { + name: name.to_string(), + kind: kind.to_string(), + args, + expected: u64::from_le_bytes(bytes.try_into().unwrap()), + }); + } + out +} + +fn parse(token: &str) -> Option { + if token.starts_with('&') { + return None; + } + if let Some(hex) = token.strip_prefix("0x") { + return u64::from_str_radix(hex, 16).ok(); + } + if let Some(neg) = token.strip_prefix('-') { + return Some((neg.parse::().ok()?).wrapping_neg() as u64); + } + token.parse::().ok() +} + +/// The value the driver would have recorded, given what the register held. +fn widen(raw: u64, kind: &str) -> u64 { + let (bits, signed) = match kind { + "u8" => (8, false), + "i8" => (8, true), + "u16" => (16, false), + "i16" => (16, true), + "u32" => (32, false), + "i32" => (32, true), + "i64" => (64, true), + _ => (64, false), + }; + if bits == 64 { + return raw; + } + let truncated = raw & ((1u64 << bits) - 1); + if signed && truncated >> (bits - 1) == 1 { + truncated | !((1u64 << bits) - 1) + } else { + truncated + } +} + +fn check(fixture: &str) { + let Some(p) = open(fixture) else { return }; + let cases = cases(); + if cases.is_empty() { + return; + } + + let mut wrong = Vec::new(); + let mut ran = 0; + for case in &cases { + let Some(f) = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(case.name.as_str())) + else { + continue; + }; + let setup = e5r_api::Setup { + arguments: case.args.clone(), + depth: 4096, + ..Default::default() + }; + let run = e5r_api::emulate::run(&p, f, &setup); + if run.stop != e5r_ir::Stop::Returned { + wrong.push(format!("{}{:?}: {:?}", case.name, case.args, run.stop)); + continue; + } + ran += 1; + let got = widen(run.result, &case.kind); + if got != case.expected { + wrong.push(format!( + "{}{:?} = {got:#x}, the processor said {:#x}", + case.name, case.args, case.expected + )); + } + } + assert!( + wrong.is_empty(), + "{fixture}: {} of {} cases disagree:\n {}", + wrong.len(), + cases.len(), + wrong.join("\n ") + ); + assert!(ran > 10, "{fixture}: only {ran} cases ran"); + println!("{fixture}: {ran} cases agree with the processor"); +} + +#[test] +fn object_files_emulate_to_the_same_answers() { + for fixture in [ + "wide.a64.O0.o", + "wide.a64.O1.o", + "wide.a64.O2.o", + "wide.x64.O0.o", + "wide.x64.O2.o", + ] { + check(fixture); + } +} diff --git a/crates/e5r-api/tests/paths.rs b/crates/e5r-api/tests/paths.rs new file mode 100644 index 0000000..9bd609b --- /dev/null +++ b/crates/e5r-api/tests/paths.rs @@ -0,0 +1,410 @@ +//! Emulating selected paths, measured against what the processor did. +//! +//! `fixtures/emulate/paths.c` is compiled for both architectures and run for +//! real at build time: natively here, under qemu for the other. Its `_start` +//! records every answer it computed. These tests run the same functions in the +//! interpreter and compare against that recording, which is a stronger +//! statement than checking the emulator against itself. +//! +//! The layout of the recording is fixed by `_start`: twenty-five little-endian +//! words, then the twenty-seven plaintext bytes the decryptor produced. + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Function, Options, Program, analyze}; +use e5r_api::{Ending, Place, Setup, Span, emulate}; +use e5r_core::Addr; +use e5r_format::LoadOptions; + +/// Somewhere that is neither the image nor the stack, so a buffer placed there +/// is visibly the caller's doing. +const SCRATCH: u64 = 0x6000_0000; + +/// How many plaintext bytes the decryptor produces. +const PLAIN: usize = 27; + +/// Every fixture this file runs against. Two optimization levels each, because +/// which shape a compiler emits for a switch changes between them and the +/// point is to check the shape that got emitted. +const FIXTURES: [&str; 4] = [ + "em-paths.a64.O1", + "em-paths.a64.O2", + "em-paths.x64.O1", + "em-paths.x64.O2", +]; + +fn build() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(build()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// What the processor recorded: the words, then the plaintext. +fn recorded(name: &str) -> Option<(Vec, Vec)> { + let data = std::fs::read(build()?.join(format!("{name}.out"))).ok()?; + if data.len() < PLAIN || (data.len() - PLAIN) % 8 != 0 { + return None; + } + let split = data.len() - PLAIN; + let words = data[..split] + .chunks_exact(8) + .map(|c| u64::from_le_bytes(c.try_into().unwrap())) + .collect(); + Some((words, data[split..].to_vec())) +} + +fn func<'a>(p: &'a Program, name: &str) -> &'a Function { + p.functions_by_address() + .find(|f| f.name.as_deref() == Some(name)) + .unwrap_or_else(|| panic!("no function named {name}")) +} + +fn address_of(p: &Program, name: &str) -> Addr { + func(p, name).entry +} + +/// Run every fixture that exists, and insist at least one did. +/// +/// Returns how many ran, so a caller that counts something inside can tell a +/// real zero from a workspace with no fixtures built. +fn over_fixtures(what: &str, mut body: impl FnMut(&str, &Program, &[u64], &[u8])) -> usize { + let mut ran = 0; + for name in FIXTURES { + let (Some(p), Some((words, plain))) = (open(name), recorded(name)) else { + continue; + }; + body(name, &p, &words, &plain); + ran += 1; + } + if build().is_some_and(|d| d.join(FIXTURES[0]).exists()) { + assert!(ran > 0, "{what}: no fixture ran"); + } + ran +} + +/// 1. A string that only exists once the code has run. +/// +/// The ciphertext is in the image and the key rolls with it, so no entry means +/// anything on its own. The caller places a buffer, points the decryptor at it, +/// and reads it back; the answer is compared against the bytes the processor +/// itself produced from the same routine. +#[test] +fn a_decrypted_string_comes_back_as_the_processor_produced_it() { + over_fixtures("decryption", |name, p, words, plain| { + let setup = Setup { + arguments: vec![SCRATCH], + watch: vec![Span { + at: SCRATCH, + len: 64, + }], + ..Default::default() + }; + let run = emulate::run(p, func(p, "decrypt"), &setup); + assert_eq!(run.ending, Ending::Returned, "{name}"); + assert!(run.clean(), "{name}: {}", run.ending); + assert_eq!(run.result, words[0], "{name}: length"); + assert_eq!(run.result, PLAIN as u64, "{name}: length"); + + let got = run.region(SCRATCH).expect("watched span"); + assert_eq!(&got[..PLAIN], plain, "{name}: plaintext"); + assert_eq!( + run.text(SCRATCH).as_deref(), + Some(std::str::from_utf8(plain).unwrap()), + "{name}" + ); + // The buffer was not in the image, so every byte of it is a byte the + // run wrote, and `written` has to agree with the watched span. + for (i, b) in plain.iter().enumerate() { + assert_eq!(run.written.get(&(SCRATCH + i as u64)), Some(b), "{name}"); + } + }); +} + +/// 2. A control flow that is computed. +/// +/// `dispatch` builds a masked table on its stack and indexes it with its +/// argument, so the image says nothing about where the branch goes. Running it +/// settles it, and the set of targets over the inputs is exactly the three +/// functions the source names. +#[test] +fn a_computed_branch_resolves_to_exactly_the_three_targets() { + over_fixtures("dispatch", |name, p, words, _| { + let f = func(p, "dispatch"); + let want: BTreeSet = ["step_a", "step_b", "step_c"] + .iter() + .map(|n| address_of(p, n)) + .collect(); + // Static analysis settled none of the three, which is why running it is + // worth anything. If a compiler ever folds the mask away, this is the + // assertion that says the fixture stopped testing what it claims to. + assert!( + f.cfg.tables.is_empty() && !f.cfg.calls.iter().any(|c| want.contains(c)), + "{name}: dispatch's targets are already known statically" + ); + + let inputs: Vec> = (0..6u64).map(|i| vec![i]).collect(); + let edges = emulate::resolve(p, f, &Setup::default(), &inputs); + let inside: Vec<_> = edges + .iter() + .filter(|e| f.range.contains(e.at)) + .cloned() + .collect(); + let got: BTreeSet = inside.iter().map(|e| e.to).collect(); + assert_eq!(got, want, "{name}: targets"); + + // Every edge carries the inputs it was seen under. That is the whole + // of the evidence for it: an executed edge is a claim about those + // inputs and not about the branch. + for e in &inside { + assert!(!e.under.is_empty(), "{name}: edge with no evidence"); + assert!(e.under.iter().all(|u| u.len() == 1), "{name}"); + } + assert_eq!(inside.len(), 3, "{name}: one edge per target"); + + // And the answers agree with the processor, seed for seed. + for seed in 0..6u64 { + let run = emulate::run( + p, + f, + &Setup { + arguments: vec![seed], + ..Default::default() + }, + ); + assert_eq!(run.ending, Ending::Returned, "{name}: dispatch({seed})"); + assert_eq!( + run.result, + words[19 + seed as usize], + "{name}: dispatch({seed})" + ); + } + }); +} + +/// 3. A recovered jump table, confirmed by running the branch. +/// +/// `pick` has eleven cases and `pick2`, right behind it in the image, has +/// seven, and the compilers here put the two tables back to back. A scan that +/// is not bounded by the guard runs out of the first into the second and +/// reports eighteen plausible targets for a switch with eleven. Running the +/// branch once per index is the check that catches it: a target nothing selects +/// is reported rather than believed. +/// +/// Recovery does not find a table in every one of these functions, and where it +/// does not there is nothing to confirm. Those are skipped and counted, and the +/// count has a floor, so a recovery that quietly stopped finding tables at all +/// would fail here rather than pass by vacuum. +#[test] +fn a_jump_table_is_confirmed_index_by_index() { + let mut checked = 0; + let ran = over_fixtures("jump table", |name, p, words, _| { + for (fname, cases, base) in [("pick", 11usize, 1usize), ("pick2", 7, 12)] { + let f = func(p, fname); + // Where recovery found a table, running the branch checks it. Where + // it did not there is nothing to confirm, and that is a gap in + // recovery rather than a failure of this gate. + if let Some(table) = f.cfg.tables.first() { + checked += 1; + assert_eq!(table.targets.len(), cases, "{name}: {fname} table size"); + assert!(!table.bounded_by_scan, "{name}: {fname} sized by scanning"); + + // Past the last case as well, so the guard rejecting an index + // is observed rather than assumed. + let indices: Vec = (0..cases as u64 + 5).collect(); + let setup = Setup { + arguments: vec![0, 3], + ..Default::default() + }; + let c = emulate::confirm(p, f, table, &setup, 0, &indices); + + assert_eq!(c.taken.len(), cases, "{name}: {fname} indices reaching"); + assert_eq!( + c.rejected, + (cases as u64..cases as u64 + 5).collect::>(), + "{name}: {fname} guard" + ); + assert!( + c.agrees(), + "{name}: {fname} unreached {:?}, extra {:?}", + c.unreached, + c.extra + ); + assert_eq!(c.reached.len(), cases, "{name}: {fname} distinct targets"); + } + + // The answers behind the cases, against what the processor computed + // for the same calls. This holds table or no table: emulation + // follows the branch either way. + for i in 0..cases { + let run = emulate::run( + p, + f, + &Setup { + arguments: vec![i as u64, 3], + ..Default::default() + }, + ); + assert_eq!(run.ending, Ending::Returned, "{name}: {fname}({i})"); + assert_eq!(run.result, words[base + i], "{name}: {fname}({i})"); + } + + // And the default arm, which the table does not cover. + let run = emulate::run( + p, + f, + &Setup { + arguments: vec![cases as u64, 3], + ..Default::default() + }, + ); + let fallback = if fname == "pick" { 0xdead } else { 0xbeef }; + assert_eq!(run.result, fallback, "{name}: {fname} default arm"); + } + + // The hazard the confirmation exists for, made concrete: where both + // tables were recovered they are adjacent, so nothing in the bytes + // marks where the first one ends. + if let (Some(a), Some(b)) = ( + func(p, "pick").cfg.tables.first(), + func(p, "pick2").cfg.tables.first(), + ) { + let end = a.table.get() + a.entry_size * a.targets.len() as u64; + assert_eq!( + end, + b.table.get(), + "{name}: the two tables are not adjacent" + ); + } + }); + if ran > 0 { + assert!( + checked >= 3, + "only {checked} recovered tables were confirmed" + ); + } +} + +/// A stubbed `write` records the bytes and writes nowhere. +#[test] +fn a_write_is_answered_and_recorded() { + over_fixtures("write", |name, p, _, _| { + let setup = Setup { + arguments: vec![SCRATCH, 5], + inputs: vec![Place { + at: SCRATCH, + bytes: b"hello".to_vec(), + }], + ..Default::default() + }; + let run = emulate::run(p, func(p, "shout"), &setup); + assert_eq!(run.ending, Ending::Returned, "{name}: {}", run.ending); + assert_eq!(run.result, 5, "{name}"); + assert_eq!(run.written_out(), b"hello", "{name}"); + assert_eq!(run.syscalls.len(), 1, "{name}"); + assert!(run.syscalls[0].served(), "{name}"); + assert_eq!(run.syscalls[0].name, Some("write"), "{name}"); + assert_eq!(run.output[0].fd, 1, "{name}"); + // A run that took a stub is not a clean reading of the code alone, and + // has to say so even though it returned. + assert!(run.stubbed(), "{name}"); + assert!(!run.clean(), "{name}"); + }); +} + +/// A stubbed `read` hands back the caller's bytes, and end of file after that. +#[test] +fn a_read_hands_back_what_the_caller_supplied() { + over_fixtures("read", |name, p, _, _| { + let mut setup = Setup { + arguments: vec![SCRATCH, 8], + watch: vec![Span { + at: SCRATCH, + len: 8, + }], + ..Default::default() + }; + setup.kernel.input = b"abcdefgh".to_vec(); + let run = emulate::run(p, func(p, "slurp"), &setup); + assert_eq!(run.ending, Ending::Returned, "{name}: {}", run.ending); + assert_eq!(run.result, 8, "{name}"); + assert_eq!(run.region(SCRATCH), Some(&b"abcdefgh"[..]), "{name}"); + + // Nothing supplied means end of file, not an invented byte. + let empty = Setup { + arguments: vec![SCRATCH, 8], + ..Default::default() + }; + let run = emulate::run(p, func(p, "slurp"), &empty); + assert_eq!(run.result, 0, "{name}: end of file"); + assert!(run.syscalls[0].served(), "{name}"); + }); +} + +/// A call with no stub stops the run and is reported by number. +#[test] +fn an_unmodelled_call_is_refused_by_number() { + over_fixtures("refusal", |name, p, _, _| { + let run = emulate::run(p, func(p, "unmodelled"), &Setup::default()); + match &run.ending { + Ending::Syscall { number, .. } => assert_eq!(*number, 424242, "{name}"), + other => panic!("{name}: {other}"), + } + assert_eq!(run.syscalls.len(), 1, "{name}"); + assert!(!run.syscalls[0].served(), "{name}"); + assert_eq!(run.syscalls[0].returned, None, "{name}"); + // No plausible value was put in the result register on the way out. + assert!(run.output.is_empty(), "{name}"); + }); +} + +/// A budget that stops a run stops it, in bounded time, and says which of the +/// two things happened. +#[test] +fn a_budget_stops_a_run_and_says_so() { + over_fixtures("budget", |name, p, _, _| { + let budget = 200_000; + let started = std::time::Instant::now(); + let run = emulate::run( + p, + func(p, "forever"), + &Setup { + arguments: vec![1], + budget, + ..Default::default() + }, + ); + let took = started.elapsed(); + assert_eq!(run.ending, Ending::Budget, "{name}"); + assert!(!run.ending.finished(), "{name}"); + // The budget is checked between instructions, so the last one finishes: + // the bound is the budget plus at most one instruction's worth of + // operations, not the budget exactly. + assert!( + run.ops >= budget && run.ops < budget + 256, + "{name}: {} operations for a budget of {budget}", + run.ops + ); + assert!(took.as_secs() < 30, "{name}: took {took:?}"); + + // The same budget, a function that terminates: the reason has to be a + // different one, or the reason is not saying anything. + let done = emulate::run( + p, + func(p, "decrypt"), + &Setup { + arguments: vec![SCRATCH], + budget, + ..Default::default() + }, + ); + assert_eq!(done.ending, Ending::Returned, "{name}"); + assert!(done.ending.finished(), "{name}"); + assert!(done.ops < budget, "{name}"); + }); +} diff --git a/crates/e5r-api/tests/prototypes.rs b/crates/e5r-api/tests/prototypes.rs new file mode 100644 index 0000000..543b016 --- /dev/null +++ b/crates/e5r-api/tests/prototypes.rs @@ -0,0 +1,318 @@ +//! Prototype recovery, measured against what the compiler recorded. +//! +//! The fixtures carry debug information, so the number of parameters each +//! function takes and whether it returns anything are known. The recovery runs +//! without looking at any of it: a register read before this function wrote it +//! arrived with a value, and one written on the way to a return is left for +//! the caller. +//! +//! A recovered count that is too low loses an argument at every call site, so +//! that is a failure. One that is too high is a function whose argument the +//! compiler passed and never used, which the machine code cannot distinguish +//! from one it uses; those are counted and held under a ceiling. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::Addr; +use e5r_format::LoadOptions; +use e5r_types::ctype::Type; + +/// Ceiling on the share of functions where the recovery counts more arguments +/// than were declared. Only lowered. +const MAX_OVERCOUNT: f64 = 0.14; + +/// Ceiling on the share of void functions credited with a result. Only +/// lowered, and it comes down when the feedback edges land: a caller that +/// never reads the result is the evidence this cannot see from inside. +const MAX_FALSE_RETURN: f64 = 0.95; + +/// The same, once the callers have had their say. Lower, because a function +/// every caller ignores did not return anything whatever it left behind. +const MAX_FALSE_RETURN_WITH_CALLERS: f64 = 0.60; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// Recover one function's prototype. +fn recover(p: &Program, f: &e5r_analysis::Function) -> e5r_ir::proto::Prototype { + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut ssa = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut ssa); + e5r_ir::proto::recover(&ssa, &e5r_ir::abi::of(&p.object.arch)) +} + +/// What the debug information declared: how many integer and floating +/// parameters, and whether anything comes back. +fn declared(p: &Program, at: Addr) -> Option<(usize, usize, bool)> { + let d = p.object.debug.as_ref()?; + let f = d.functions.get(&at)?; + let mut integers = 0; + let mut floats = 0; + for (_, ty) in &f.signature.parameters { + match d.types.get(d.types.resolve(*ty)) { + Some(Type::Float { .. }) => floats += 1, + _ => integers += 1, + } + } + Some((integers, floats, f.signature.returns.is_some())) +} + +/// Running totals, so the ratio is measured over a sample big enough to mean +/// something rather than over three functions. +#[derive(Default)] +struct Totals { + checked: usize, + overcounted: usize, +} + +fn check(fixture: &str, totals: &mut Totals) { + let Some(p) = open(fixture) else { return }; + if p.object.debug.is_none() { + return; + } + + let mut checked = 0; + let mut overcounted = 0; + let mut short = Vec::new(); + let mut missing_return = Vec::new(); + + for f in p.functions_by_address() { + if !f.is_complete() { + continue; + } + let Some((integers, floats, returns)) = declared(&p, f.entry) else { + continue; + }; + let recovered = recover(&p, f); + checked += 1; + let name = f.display_name(); + + // Arguments on the stack count towards the total the caller passes. + let recovered_integers = recovered.integer_arguments + recovered.stack_arguments.len(); + if recovered_integers < integers || recovered.float_arguments < floats { + short.push(format!( + "{name}: recovered {recovered_integers} integer and {} float, \ + declared {integers} and {floats}", + recovered.float_arguments + )); + } else if recovered_integers > integers || recovered.float_arguments > floats { + overcounted += 1; + } + + if returns && recovered.returns.is_none() { + missing_return.push(name); + } + } + + if checked == 0 { + return; + } + assert!( + short.is_empty(), + "{fixture}: {} function(s) lost an argument:\n {}", + short.len(), + short.join("\n ") + ); + assert!( + missing_return.is_empty(), + "{fixture}: {} function(s) return a value the recovery did not see: {}", + missing_return.len(), + missing_return.join(", ") + ); + totals.checked += checked; + totals.overcounted += overcounted; + println!("{fixture}: {checked} prototypes, {overcounted} over-counted"); +} + +#[test] +fn recovered_prototypes_agree_with_the_debug_information() { + let mut totals = Totals::default(); + for fixture in [ + "wide.a64.O0.o", + "wide.a64.O1.o", + "wide.a64.O2.o", + "wide.x64.O0.o", + "wide.x64.O2.o", + "shapes.a64.O0.o", + "shapes.x64.O2.o", + "hello.a64.O0", + ] { + check(fixture, &mut totals); + } + if totals.checked == 0 { + return; + } + let share = totals.overcounted as f64 / totals.checked as f64; + assert!( + share <= MAX_OVERCOUNT, + "{} of {} prototypes counted too many arguments ({share:.2}), \ + ceiling is {MAX_OVERCOUNT}", + totals.overcounted, + totals.checked + ); + println!( + "{} prototypes, {} over-counted ({share:.2})", + totals.checked, totals.overcounted + ); +} + +/// How often a void function is credited with a result. +/// +/// Recovery never misses a real return, which is the direction that costs a +/// caller something. The other direction it cannot always settle: a function +/// whose last act is to compute an address into the first argument register +/// has left a value there, and only its callers know whether anyone reads it. +/// Settling that is what the roadmap's feedback edges are for; until then the +/// rate is measured and held under a ceiling. +#[test] +fn a_result_is_claimed_only_when_something_was_left_behind() { + let mut void_functions = 0; + let mut credited = 0; + for fixture in [ + "wide.a64.O0.o", + "wide.a64.O2.o", + "wide.x64.O0.o", + "wide.x64.O2.o", + ] { + let Some(p) = open(fixture) else { continue }; + let Some(d) = p.object.debug.as_ref() else { + continue; + }; + for f in p.functions_by_address().filter(|f| f.is_complete()) { + let Some(df) = d.functions.get(&f.entry) else { + continue; + }; + if df.signature.returns.is_some() { + continue; + } + void_functions += 1; + if recover(&p, f).returns.is_some() { + credited += 1; + } + } + } + if void_functions == 0 { + return; + } + let share = credited as f64 / void_functions as f64; + assert!( + share <= MAX_FALSE_RETURN, + "{credited} of {void_functions} functions that return nothing were \ + credited with a result ({share:.2}), ceiling is {MAX_FALSE_RETURN}" + ); + println!("{void_functions} void functions, {credited} credited with a result ({share:.2})"); +} + +#[test] +fn a_function_that_keeps_the_conventions_promises_says_so() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let mut standard = 0; + let mut total = 0; + for f in p.functions_by_address().filter(|f| f.is_complete()) { + total += 1; + if recover(&p, f).standard { + standard += 1; + } + } + assert!(total > 10, "only {total} functions"); + // Compiled C keeps the convention; a fixture where most functions did not + // would mean the check is measuring something else. + assert!( + standard * 4 >= total * 3, + "only {standard} of {total} functions keep the calling convention" + ); +} + +/// The callers settle what a function alone could not. +/// +/// A function that leaves a value in the result register which every caller +/// ignores did not return anything, whatever the register held. This is the +/// measurement the single-function ceiling above cannot make, so it is made +/// here on binaries where the callers are present. +#[test] +fn callers_settle_whether_a_function_returns() { + let mut void_functions = 0; + let mut credited = 0; + let mut checked = 0; + for fixture in ["shapes.a64.O0.cpp", "shapes.a64.O2.cpp", "hello.a64.O0"] { + let Some(p) = open(fixture) else { continue }; + let Some(d) = p.object.debug.as_ref() else { + continue; + }; + let recovered = e5r_api::prototypes(&p); + for f in p.functions_by_address().filter(|f| f.is_complete()) { + let Some(df) = d.functions.get(&f.entry) else { + continue; + }; + let Some(found) = recovered.get(&f.entry) else { + continue; + }; + checked += 1; + // A real return must survive: losing one costs every caller. + if df.signature.returns.is_some() { + assert!( + found.prototype.returns.is_some() || found.callers == 0, + "{fixture}: {} returns a value and the recovery dropped it", + f.display_name() + ); + continue; + } + void_functions += 1; + if found.prototype.returns.is_some() { + credited += 1; + } + } + } + if checked == 0 { + return; + } + let share = credited as f64 / void_functions.max(1) as f64; + assert!( + share <= MAX_FALSE_RETURN_WITH_CALLERS, + "{credited} of {void_functions} void functions are still credited with \ + a result once the callers have spoken ({share:.2}), ceiling is \ + {MAX_FALSE_RETURN_WITH_CALLERS}" + ); + println!( + "{checked} prototypes across whole programs, \ + {credited} of {void_functions} void functions still credited ({share:.2})" + ); +} + +#[test] +fn a_function_nothing_calls_keeps_its_own_answer() { + let Some(p) = open("wide.a64.O0.o") else { + return; + }; + let recovered = e5r_api::prototypes(&p); + // Nothing in an object file of library functions calls `arith64`, so the + // callers say nothing and its own answer stands. + let Some(f) = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some("arith64")) + else { + return; + }; + let found = recovered.get(&f.entry).expect("recovered"); + assert_eq!(found.callers, 0); + assert!(found.prototype.returns.is_some()); +} diff --git a/crates/e5r-api/tests/query_language.rs b/crates/e5r-api/tests/query_language.rs new file mode 100644 index 0000000..a61919f --- /dev/null +++ b/crates/e5r-api/tests/query_language.rs @@ -0,0 +1,538 @@ +//! What the query language does with input that is not a query. +//! +//! Two properties, both of which a person typing at a prompt finds out about +//! immediately. A query it does not understand has to say which part it did +//! not understand, because "no results" for a misspelling is worse than an +//! error. And nothing typed at it may panic or take an unbounded time, since +//! the same parser is behind `e5r query`, where an agent can put anything it +//! likes in front of it. + +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_api::query::{self, Field}; +use e5r_format::LoadOptions; + +/// Every form the language accepts, which is also the seed corpus for the +/// mutations and truncations below. +const GOOD: [&str; 22] = [ + "functions", + "functions where insns > 100 and name ~ \"crypt\"", + "functions where named = false and insns > 40 limit 20", + "functions where (callers = 0 or callees > 8) and complete = true", + "strings where length >= 20", + "strings where text ~ \"/etc/\" and section = \".rodata\"", + "symbols where kind = function and dynamic = true", + "imports where library ~ \"libc\"", + "exports where name !~ \"_\"", + "sections where exec = true", + "xrefs where kind = call and to = 0x401000", + "calls", + "calls to \"memcpy\"", + "calls to \"memcpy\" where arg3 is not bounded", + "calls to \"memcpy\" where arg3 is bounded", + "calls to \"memcpy\" where arg3 is unconstrained", + "calls to \"memcpy\" where arg3 is unknown limit 5", + "calls to \"memcpy\" where arg1 is constant and arg3 is not bounded", + "calls where target is indirect", + "calls where target is direct and function ~ \"copy\"", + "functions where reads any argument of a call to \"system\"", + "values reaching arg1 of calls to \"system\"", +]; + +fn program() -> Option { + let dir: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + let data = std::fs::read(dir.join("df-bounds.a64.O2")).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// Parse and check, which is every refusal that does not need a program. +fn accept(text: &str) -> Result { + let q = query::parse(text)?; + q.validate()?; + Ok(q) +} + +#[test] +fn every_accepted_form_parses() { + for text in GOOD { + accept(text).unwrap_or_else(|e| panic!("{text:?} is documented as accepted: {e}")); + } +} + +#[test] +fn nonsense_says_what_it_did_not_understand() { + // Each case pairs a query with the word the message has to contain. A + // message that does not name the part that was wrong sends the reader + // back to the manual. The categories are the ones a person hits: a + // misspelling, a field that does not exist, a comparison against the + // wrong kind of thing, a lost quote, an operator with nothing after it, + // and a clause with its body missing. + let cases: [(&str, &str); 22] = [ + // An entity that does not exist. + ("functionz", "functionz"), + ("blocks where size > 3", "blocks"), + ("", "functions"), + // A field that does not exist. + ("functions where inns > 3", "inns"), + ("strings where lenght >= 4", "lenght"), + ("imports where address = 0x1000", "address"), + // The wrong kind of value for the field. + ("functions where insns > \"many\"", "many"), + ("functions where insns > large", "large"), + ("xrefs where to = main", "main"), + ("sections where exec = 3", "exec"), + ("sections where exec ~ \"true\"", "exec"), + ("functions where insns ~ 10", "insns"), + ("functions where insns > \"100\"", "quoted"), + // A quote that was never closed. + ("strings where text ~ \"oops", "oops"), + ("calls to \"memcpy where arg3 is bounded", "memcpy"), + // An operator with nothing after it. + ("functions where insns >", "insns >"), + ("functions where insns > 3 and name ~", "name ~"), + ("functions where insns", "insns"), + // A clause with nothing in it. + ("functions where", "where"), + ("functions where insns > 3 and", "and"), + // And the dataflow clauses, which have bodies of their own. + ("calls where arg3 is bouned", "bouned"), + ("functions where arg3 is bounded", "calls"), + ]; + for (text, wanted) in cases { + let error = match accept(text) { + Err(e) => e, + Ok(_) => panic!("{text:?} was accepted"), + }; + assert!( + error.contains(wanted), + "{text:?} should have been refused with a message naming {wanted:?}, said: {error}" + ); + // A refusal that says nothing beyond the fact of failing is the + // generic parse error this test exists to forbid. + assert!( + error.len() > wanted.len() + 4, + "{text:?} was refused with {error:?}, which says nothing" + ); + } +} + +#[test] +fn the_rest_of_the_nonsense_is_caught_when_it_runs() { + // These need a program, because what they get wrong is only wrong once + // there is something to ask. + let cases: [(&str, &str); 5] = [ + ("calls to", "the end of the query"), + ("values", "reaching"), + ("values reaching arg1", "of"), + ("values reaching frobnicate of calls", "frobnicate"), + ( + "functions where reads some argument of a call to \"x\"", + "some", + ), + ]; + for (text, wanted) in cases { + let error = match accept(text) { + Err(e) => e, + Ok(_) => match program() { + Some(p) => match query::run(&p, text) { + Err(e) => e, + Ok(_) => panic!("{text:?} was accepted"), + }, + None => continue, + }, + }; + assert!( + error.contains(wanted), + "{text:?} should have been refused with a message naming {wanted:?}, said: {error}" + ); + } +} + +#[test] +fn a_wrong_kind_of_comparison_is_refused_rather_than_coerced() { + // The failure this guards against is not a crash: it is `insns > "10"` + // comparing the characters and reporting an empty answer, which reads as + // "there are none" rather than "that is not a question". + let Some(p) = program() else { + return; + }; + assert!(query::run(&p, "functions where insns > \"10\"").is_err()); + let ok = query::run(&p, "functions where insns > 10").expect("a number against a number"); + assert!( + !ok.rows.is_empty(), + "the fixture has functions of more than ten instructions" + ); +} + +#[test] +fn the_declared_kind_of_a_field_is_the_kind_its_rows_hold() { + // The kind table is keyed by field name rather than by entity, so it is + // only correct while no two entities disagree about a name. Checked here + // against the rows themselves rather than by reading the table twice. + let Some(p) = program() else { + return; + }; + for entity in [ + "functions", + "strings", + "symbols", + "imports", + "exports", + "sections", + "xrefs", + "calls", + ] { + let answer = query::run(&p, entity).unwrap_or_else(|e| panic!("{entity}: {e}")); + for row in &answer.rows { + for (name, value) in &row.values { + // Every field is comparable against a literal of its own kind + // and, when it is not text, refuses the others. That is the + // property the table exists for, so assert it through the + // parser rather than by exporting the table. + let (good, bad) = match value { + Field::Number(_) | Field::Address(_) => ("1", "\"x\""), + Field::Bool(_) => ("true", "1"), + Field::Text(_) | Field::Missing => continue, + }; + let text = format!("{entity} where {name} = {good}"); + query::parse(&text) + .and_then(|q| q.validate()) + .unwrap_or_else(|e| panic!("{text:?}: {e}")); + let text = format!("{entity} where {name} = {bad}"); + assert!( + query::parse(&text).and_then(|q| q.validate()).is_err(), + "{text:?} was accepted, so `{name}` is not declared as what it holds" + ); + } + } + } +} + +#[test] +fn a_misspelled_dataflow_word_is_not_silently_a_field() { + // `arg3 is bounded` is a question, and `bouned` is a typo in it rather + // than a field of something. Matching nothing would look like an answer. + let e = query::parse("calls where arg3 is bouned").unwrap_err(); + assert!(e.contains("bounded"), "{e}"); + assert!(e.contains("unconstrained"), "{e}"); +} + +/// A deterministic generator, so a failure is reproducible from the seed +/// alone. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + // A plain linear congruential generator: the numbers only have to be + // varied, not unpredictable. + self.0 = self + .0 + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + self.0 >> 11 + } + + fn below(&mut self, n: usize) -> usize { + if n == 0 { + 0 + } else { + (self.next() % n as u64) as usize + } + } + + fn pick<'a, T>(&mut self, from: &'a [T]) -> &'a T { + &from[self.below(from.len())] + } +} + +/// The pieces a query is made of, plus the ones that do not belong in one. +const WORDS: [&str; 36] = [ + "calls", + "values", + "functions", + "xrefs", + "where", + "limit", + "to", + "of", + "reaching", + "reads", + "any", + "every", + "argument", + "is", + "not", + "and", + "or", + "arg1", + "arg3", + "arg0", + "arg99999999999999999999", + "bounded", + "unconstrained", + "target", + "indirect", + "insns", + "exec", + "\"memcpy\"", + "\"", + "(", + ")", + "=", + "~", + ">=", + "0x401000", + "18446744073709551615", +]; + +/// The characters a mutation reaches for, which are the ones that change how +/// text lexes rather than what it says. +const POISON: [char; 14] = [ + '"', + '\'', + '(', + ')', + '=', + '~', + '<', + '>', + '!', + '\\', + '\u{1f600}', + '\u{0}', + '\n', + ' ', +]; + +/// One near-miss of an accepted query. +/// +/// Near misses are what finds parser bugs: uniform noise stops at the first +/// token, while a query with one character moved reaches the clause that +/// takes a wrong turn. +fn mutate(seed: &str, rng: &mut Rng) -> String { + let mut chars: Vec = seed.chars().collect(); + if chars.is_empty() { + return String::new(); + } + match rng.below(6) { + // Drop a character, which is how a quote or a bracket goes missing. + 0 => { + let i = rng.below(chars.len()); + chars.remove(i); + } + // Insert one that changes how the rest lexes. + 1 => { + let i = rng.below(chars.len() + 1); + chars.insert(i, *rng.pick(&POISON)); + } + // Replace one. + 2 => { + let i = rng.below(chars.len()); + chars[i] = *rng.pick(&POISON); + } + // Swap two neighbours: `is not` becomes `is nto`. + 3 if chars.len() > 1 => { + let i = rng.below(chars.len() - 1); + chars.swap(i, i + 1); + } + // Repeat a run, which is how a clause ends up with two heads. + 4 => { + let at = rng.below(chars.len()); + let len = rng.below(8).min(chars.len() - at); + let run: Vec = chars[at..at + len].to_vec(); + for (n, c) in run.into_iter().enumerate() { + chars.insert(at + n, c); + } + } + // Splice a word in from somewhere else. + _ => { + let at = rng.below(chars.len()); + let word = *rng.pick(&WORDS); + for (n, c) in word.chars().enumerate() { + chars.insert(at + n, c); + } + } + } + chars.into_iter().collect() +} + +/// How long the mutation hunt gets. Long enough to cover the corpus many +/// times over, short enough that nobody is tempted to skip the suite. +const BUDGET: Duration = Duration::from_millis(1500); + +/// What a person will wait for one answer at a prompt. +const CEILING: Duration = Duration::from_secs(2); + +/// Whatever the query is, the answer is a value or an error that says +/// something. +fn probe(p: Option<&Program>, input: &str) { + let each = Instant::now(); + match query::parse(input) { + Ok(q) => { + // Validating and running have to terminate as well, and running + // is the part that lifts functions. + if q.validate().is_ok() { + if let Some(p) = p { + let _ = query::run(p, input); + } + } + } + Err(e) => assert!(!e.trim().is_empty(), "{input:?} was refused with nothing"), + } + assert!( + each.elapsed() < CEILING, + "{input:?} took {:?}, which is longer than anything typed at a prompt may take", + each.elapsed() + ); +} + +#[test] +fn truncated_and_scrambled_queries_neither_panic_nor_hang() { + let p = program(); + let mut rng = Rng(0x5eed); + let mut inputs: Vec = Vec::new(); + + // Truncations: every prefix of every accepted form, which is what a + // half-typed query looks like. + for text in GOOD { + for end in 0..=text.len() { + if text.is_char_boundary(end) { + inputs.push(text[..end].to_string()); + } + } + } + // Suffixes as well, so the parser meets a clause with its head missing. + for text in GOOD { + for start in 0..text.len() { + if text.is_char_boundary(start) { + inputs.push(text[start..].to_string()); + } + } + } + // Words in an order nobody would type. + for _ in 0..3000 { + let len = 1 + rng.below(12); + let mut out = String::new(); + for _ in 0..len { + out.push_str(rng.pick(&WORDS)); + out.push(' '); + } + inputs.push(out); + } + // And bytes that are not words at all. + for _ in 0..1000 { + let len = rng.below(40); + let mut out = String::new(); + for _ in 0..len { + out.push(char::from_u32((rng.next() % 0x2ff) as u32 + 1).unwrap_or('?')); + } + inputs.push(out); + } + + let started = Instant::now(); + for input in &inputs { + probe(p.as_ref(), input); + } + let whole = started.elapsed(); + assert!( + whole < Duration::from_secs(120), + "{} inputs took {whole:?} in total", + inputs.len() + ); + println!("{} inputs in {whole:?}", inputs.len()); +} + +#[test] +fn mutated_queries_neither_panic_nor_hang() { + // Seeded from the accepted forms and run for a fixed budget, so the text + // it tries is a query with something wrong with it rather than noise that + // dies at the first token. Repeated mutation compounds, so late cases are + // several edits away from anything valid. + let p = program(); + let mut rng = Rng(0x0dd_ba11); + let start = Instant::now(); + let mut cases = 0u64; + let mut parsed = 0u64; + let mut case = String::new(); + while start.elapsed() < BUDGET { + // Every so often, go back to something that was once a query. + if case.is_empty() || rng.below(8) == 0 { + case = (*rng.pick(&GOOD)).to_string(); + } + case = mutate(&case, &mut rng); + probe(p.as_ref(), &case); + if query::parse(&case).is_ok() { + parsed += 1; + } + cases += 1; + } + assert!( + cases > 200, + "only {cases} mutated queries in {BUDGET:?}; something is very slow" + ); + // A corpus nothing in survives parsing is noise wearing a corpus's + // clothes, and would not reach the parts of the parser worth testing. + assert!( + parsed > 0, + "none of {cases} mutated queries parsed, so none of them were near misses" + ); + println!("{cases} mutated queries, {parsed} of them still valid, no panics"); +} + +#[test] +fn asking_about_one_callee_does_not_lift_the_whole_image() { + // A question that names a callee is answered from the cross references + // outward: only the functions that reach it are lifted, and only the calls + // to it are described. A question about indirect calls has no such handle + // and pays for the whole image. The saving is asserted as the set of + // functions the first one has to look at, which is the same on every + // machine, rather than as a stopwatch reading, which is not. + let dir: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + let Ok(data) = std::fs::read(dir.join("panicky")) else { + return; + }; + let Ok(obj) = e5r_format::load(&data, &LoadOptions::default()) else { + return; + }; + let p = analyze(obj, &Options::default()); + + let callers = e5r_api::dataflow::callers_of_name(&p, "memcpy").len(); + assert!( + callers > 0 && callers < p.functions.len(), + "{callers} of {} functions call memcpy", + p.functions.len() + ); + + let started = Instant::now(); + let narrow = query::run(&p, "calls to \"memcpy\" where arg3 is not bounded"); + let narrow_took = started.elapsed(); + let narrow = narrow.expect("a query about one callee"); + + let started = Instant::now(); + let wide = query::run(&p, "calls where target is indirect limit 5"); + let wide_took = started.elapsed(); + let wide = wide.expect("a query about every call"); + + println!( + "{} functions, {callers} call memcpy: by name {narrow_took:?} for {} rows, \ + over everything {wide_took:?} for {} matched", + p.functions.len(), + narrow.rows.len(), + wide.matched + ); + // The comparison, not the reading. A wall-clock bound is a fact about the + // machine and its load -- a shared runner took 25s and 90s here for two + // queries that are milliseconds on an idle one -- and a gate that fails on + // a busy afternoon is a gate people learn to ignore. What holds anywhere + // is that the question with a handle costs no more than the question + // without one, because both were asked of the same image on the same + // machine a moment apart. + assert!( + narrow_took <= wide_took, + "asking about one callee took {narrow_took:?}, asking about every call \ + took {wide_took:?}: the handle bought nothing" + ); +} diff --git a/crates/e5r-api/tests/ranges.rs b/crates/e5r-api/tests/ranges.rs new file mode 100644 index 0000000..b767af4 --- /dev/null +++ b/crates/e5r-api/tests/ranges.rs @@ -0,0 +1,241 @@ +//! Range analysis, checked against what the code actually produces. +//! +//! An interval is sound when it contains every value the computation can +//! produce. That cannot be proved by reading it, but it can be falsified by +//! running the code: take the case table the lifter's oracle uses, run each +//! call in the interpreter, and require the analysis to have said the value +//! that came back was possible. A range that excludes something that happened +//! is a bug with no allowance. +//! +//! The comparison is against the bits the result register held, not against +//! the number C would have made of them: the interval describes a register. +//! +//! The test also reports how often the analysis knows anything at all, because +//! an analysis that answers "any value" to everything is sound and useless. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::Addr; +use e5r_format::LoadOptions; +use e5r_ir::range::Range; +use e5r_ir::ssa::{SsaFunction, SsaKind}; + +/// Floor on the share of returned values the analysis can say anything about. +/// Only raised. +const MIN_BOUNDED: f64 = 0.50; + +fn build() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(build()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +struct Case { + name: String, + args: Vec, +} + +/// The cases whose arguments are plain numbers, with the answer the processor +/// gave. +fn cases() -> Vec { + let Some(dir) = build() else { + return Vec::new(); + }; + let table = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/portable/cases.txt"); + let _ = dir; + let Ok(text) = std::fs::read_to_string(table) else { + return Vec::new(); + }; + let mut out = Vec::new(); + for line in text + .lines() + .map(|l| l.split('#').next().unwrap_or("").trim()) + .filter(|l| !l.is_empty()) + { + let mut parts = line.split_whitespace(); + let head = parts.next().unwrap_or_default(); + let (name, kind) = head.split_once(':').unwrap_or((head, "u64")); + if kind.starts_with('f') || line.contains('.') { + continue; + } + let mut args = Vec::new(); + let mut usable = true; + for token in parts { + match number(token) { + Some(v) => args.push(v), + None => usable = false, + } + } + if usable { + out.push(Case { + name: name.to_string(), + args, + }); + } + } + out +} + +fn number(token: &str) -> Option { + if token.starts_with('&') { + return None; + } + if let Some(hex) = token.strip_prefix("0x") { + return u64::from_str_radix(hex, 16).ok(); + } + if let Some(neg) = token.strip_prefix('-') { + return Some((neg.parse::().ok()?).wrapping_neg() as u64); + } + token.parse::().ok() +} + +fn ssa_of(p: &Program, f: &e5r_analysis::Function) -> Option { + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut ssa = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut ssa); + Some(ssa) +} + +/// What the analysis says about the value a function leaves behind. +/// +/// The join over every path out: a function with a switch in it returns from +/// several places, and what it can produce is what any of them can. Taking one +/// of them would be comparing a run that went one way against a bound computed +/// for another. +fn returned( + ssa: &SsaFunction, + abi: &e5r_ir::abi::Abi, + ranges: &BTreeMap, +) -> Option { + let mut out: Option = None; + for b in ssa.blocks.values() { + if !b + .ops + .iter() + .any(|op| op.kind == SsaKind::Op(e5r_ir::op::Op::Return)) + { + continue; + } + // The last write to a result register in this block is what it leaves. + let last = b.ops.iter().rev().filter_map(|op| op.out).find(|v| { + v.location.space == e5r_ir::op::Space::Register + && abi.results.contains(&v.location.offset) + }); + let Some(range) = last.and_then(|v| ranges.get(&v)) else { + // A path whose value is unknown makes the whole answer unknown. + return Some(Range::ANY); + }; + out = Some(match out { + Some(existing) => existing.join(*range), + None => *range, + }); + } + out +} + +#[test] +fn every_range_contains_what_the_processor_produced() { + let cases = cases(); + if cases.is_empty() { + return; + } + + let mut checked = 0; + let mut bounded = 0; + let mut wrong = Vec::new(); + + for fixture in ["wide.a64.O0.o", "wide.a64.O2.o", "wide.x64.O0.o"] { + let Some(p) = open(fixture) else { continue }; + let abi = e5r_ir::abi::of(&p.object.arch); + for case in &cases { + let Some(f) = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(case.name.as_str())) + else { + continue; + }; + let Some(ssa) = ssa_of(&p, f) else { continue }; + let ranges = e5r_ir::range::ranges(&ssa); + let Some(range) = returned(&ssa, &abi, &ranges) else { + continue; + }; + // Run it, and compare against what the register actually held. + let setup = e5r_api::Setup { + arguments: case.args.clone(), + depth: 4096, + ..Default::default() + }; + let outcome = e5r_api::emulate::run(&p, f, &setup); + if outcome.stop != e5r_ir::Stop::Returned { + continue; + } + checked += 1; + if !range.is_any() { + bounded += 1; + } + let produced = outcome.result as i64; + if !range.contains(produced) && !range.is_any() { + wrong.push(format!( + "{fixture}: {}{:?} produced {produced}, the range said {}..={}", + case.name, case.args, range.low, range.high + )); + } + } + } + + assert!( + wrong.is_empty(), + "{} range(s) exclude a value the processor produced:\n {}", + wrong.len(), + wrong.join("\n ") + ); + if checked == 0 { + return; + } + let share = bounded as f64 / checked as f64; + assert!( + share >= MIN_BOUNDED, + "only {bounded} of {checked} results were bounded at all ({share:.2}), \ + floor is {MIN_BOUNDED}" + ); + println!("{checked} results checked, {bounded} bounded ({share:.2})"); +} + +#[test] +fn a_masked_value_is_bounded_by_its_mask() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + // `indexed` reads `table[i & 63] + table[(i >> 6) & 63]`, so the masks + // bound the indices whatever the argument is. + let Some(f) = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some("indexed")) + else { + return; + }; + let Some(ssa) = ssa_of(&p, f) else { return }; + let ranges = e5r_ir::range::ranges(&ssa); + let bounded = ranges + .values() + .filter(|r| !r.is_any() && r.width() <= 63) + .count(); + assert!( + bounded > 0, + "nothing in a function full of masks came out bounded" + ); +} diff --git a/crates/e5r-api/tests/shapes.rs b/crates/e5r-api/tests/shapes.rs new file mode 100644 index 0000000..6d19a89 --- /dev/null +++ b/crates/e5r-api/tests/shapes.rs @@ -0,0 +1,138 @@ +//! Structure recovery measured against the debug information. +//! +//! A function that walks an array of structures touches the same offsets the +//! declaration lists, and the machine code says so whether or not the binary +//! carries types. The oracle is DWARF: the fixture is compiled with debug +//! information, the recovery runs without looking at it, and the two are +//! compared. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::Addr; +use e5r_format::LoadOptions; +use e5r_types::ctype::Type; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// The shapes seen through each incoming pointer of one function. +fn shapes_of(p: &Program, name: &str) -> Option> { + let f = p + .functions_by_address() + .find(|f| f.display_name() == name)?; + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut ssa = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut ssa); + Some( + e5r_ir::shape::shapes(&ssa) + .into_iter() + .map(|(l, s)| (l.offset, s)) + .collect(), + ) +} + +/// The fields the debug information declares for the first parameter. +fn declared_fields(p: &Program, name: &str) -> Option> { + let d = p.object.debug.as_ref()?; + let f = d.functions.values().find(|f| f.name == name)?; + let (_, ty) = f.signature.parameters.first()?; + let Type::Pointer(inner) = d.types.get(d.types.resolve(*ty))? else { + return None; + }; + let Type::Composite(c) = d.types.get(d.types.resolve(*inner))? else { + return None; + }; + Some( + c.fields + .iter() + .map(|field| { + ( + field.offset as i64, + d.types.size_of(field.ty).unwrap_or(0) as u8, + ) + }) + .collect(), + ) +} + +#[test] +fn the_fields_of_a_structure_come_out_of_the_code() { + for fixture in ["wide.a64.O0.o", "wide.a64.O1.o", "wide.x64.O0.o"] { + let Some(p) = open(fixture) else { continue }; + let Some(shapes) = shapes_of(&p, "use_struct") else { + continue; + }; + let Some(declared) = declared_fields(&p, "use_struct") else { + continue; + }; + assert!(!declared.is_empty(), "{fixture}: no declared fields"); + + // The pointer is the first argument, wherever the convention puts it. + let abi = e5r_ir::abi::of(&p.object.arch); + let first = abi.integer_arguments[0]; + let shape = shapes + .get(&first) + .unwrap_or_else(|| panic!("{fixture}: nothing was seen through the first argument")); + + let recovered = shape.fields(); + assert_eq!( + recovered, declared, + "{fixture}: recovered {recovered:?}, the debug information says {declared:?}" + ); + // And the stride says how big the structure is. + assert_eq!( + shape.size(), + Some(16), + "{fixture}: the element size came out as {:?}", + shape.size() + ); + } +} + +#[test] +fn an_array_walk_reports_its_element_size() { + for fixture in ["wide.a64.O0.o", "wide.x64.O0.o"] { + let Some(p) = open(fixture) else { continue }; + let Some(shapes) = shapes_of(&p, "sum_array") else { + continue; + }; + let abi = e5r_ir::abi::of(&p.object.arch); + let Some(shape) = shapes.get(&abi.integer_arguments[0]) else { + continue; + }; + // `u64 *a` read one element at a time: eight bytes at offset zero. + assert_eq!(shape.fields(), vec![(0, 8)], "{fixture}"); + } +} + +#[test] +fn nothing_is_reported_for_a_function_that_takes_no_pointers() { + let Some(p) = open("wide.a64.O0.o") else { + return; + }; + let Some(shapes) = shapes_of(&p, "arith64") else { + return; + }; + let abi = e5r_ir::abi::of(&p.object.arch); + assert!( + !shapes.contains_key(&abi.integer_arguments[0]), + "an integer argument was reported as a pointer" + ); +} diff --git a/crates/e5r-api/tests/signatures.rs b/crates/e5r-api/tests/signatures.rs new file mode 100644 index 0000000..cc40e1b --- /dev/null +++ b/crates/e5r-api/tests/signatures.rs @@ -0,0 +1,119 @@ +//! Signature matching measured on a binary whose names were removed. +//! +//! The oracle is the unstripped copy: signatures are built from it, applied to +//! the stripped one, and every name the matcher produces has to be the name +//! that function actually had. A wrong name is worse than no name, so the test +//! treats one as a failure and a miss as a gap with a floor. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_format::LoadOptions; + +/// Floor on the share of named functions a stripped copy gets back. +const MIN_RECALL: f64 = 0.90; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +#[test] +fn a_stripped_binary_gets_its_names_back() { + let (Some(named), Some(stripped)) = (open("hello.a64.O2"), open("hello.a64.O2.stripped")) + else { + return; + }; + + let library = e5r_api::collect_signatures(&named, "hello.a64.O2"); + assert!(!library.is_empty(), "no signatures were collected"); + + // What each address was really called, from the unstripped copy. + // Import thunks are excluded from libraries, so they are not what recall + // is measured over either. + let truth: BTreeMap = named + .functions_by_address() + .filter(|f| !f.name.as_deref().is_some_and(|n| n.ends_with("@plt"))) + .filter_map(|f| Some((f.entry.get(), f.name.clone()?))) + .collect(); + + let found = e5r_api::identify(&stripped, &library); + let mut wrong = Vec::new(); + for identified in &found { + match truth.get(&identified.addr.get()) { + Some(real) if *real != identified.name => wrong.push(format!( + "{}: called it {:?}, it is {real:?} ({})", + identified.addr, + identified.name, + identified.resolution.as_str() + )), + // A function the unstripped copy did not name cannot be checked, + // and claiming a name for it is not evidence of anything. + _ => {} + } + } + assert!( + wrong.is_empty(), + "{} of {} matches name the wrong function:\n {}", + wrong.len(), + found.len(), + wrong.join("\n ") + ); + + let recovered = found + .iter() + .filter(|i| truth.contains_key(&i.addr.get())) + .count(); + let recall = recovered as f64 / truth.len().max(1) as f64; + assert!( + recall >= MIN_RECALL, + "recovered {recovered} of {} names ({recall:.2}), floor is {MIN_RECALL}", + truth.len() + ); + println!( + "recovered {recovered} of {} names ({recall:.2})", + truth.len() + ); +} + +#[test] +fn a_library_built_from_one_build_recognizes_another() { + // Same source at a different optimization level is a different function, + // and the matcher must not claim otherwise. + let (Some(a), Some(b)) = (open("hello.a64.O0"), open("hello.a64.O2")) else { + return; + }; + let library = e5r_api::collect_signatures(&a, "hello.a64.O0"); + let truth: BTreeMap = b + .functions_by_address() + .filter_map(|f| Some((f.entry.get(), f.name.clone()?))) + .collect(); + let found = e5r_api::identify(&b, &library); + for identified in &found { + if let Some(real) = truth.get(&identified.addr.get()) { + assert_eq!( + *real, identified.name, + "a match across optimization levels named the wrong function" + ); + } + } +} + +#[test] +fn the_library_file_survives_a_round_trip() { + let Some(p) = open("hello.a64.O2") else { + return; + }; + let library = e5r_api::collect_signatures(&p, "hello"); + let text = library.to_text(); + let back = e5r_db::signature::Library::from_text(&text); + assert_eq!(back.len(), library.len()); + assert_eq!(back.to_text(), text); +} diff --git a/crates/e5r-api/tests/vtables.rs b/crates/e5r-api/tests/vtables.rs new file mode 100644 index 0000000..2100078 --- /dev/null +++ b/crates/e5r-api/tests/vtables.rs @@ -0,0 +1,198 @@ +//! Virtual table recovery, measured against the symbols that name them. +//! +//! A C++ compiler emits a `_ZTV` symbol for every class with virtual +//! functions, so the fixture's own symbol table says which tables exist and +//! what they are called. The scan is then run over the same binary with those +//! symbols ignored, and has to find the same tables in the same places: that +//! is the case that matters, because a stripped binary is where a scan is the +//! only thing there is. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::Addr; +use e5r_format::LoadOptions; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// The same program with every vtable symbol taken away. +fn without_symbols(mut p: Program) -> Program { + p.object.symbols.retain(|s| !s.name.starts_with("_ZTV")); + p +} + +#[test] +fn every_named_table_is_found_and_named() { + for fixture in [ + "shapes.a64.O0.cpp.o", + "shapes.a64.O2.cpp.o", + "shapes.a64.O0.cpp", + "shapes.a64.O2.cpp", + ] { + let Some(p) = open(fixture) else { continue }; + let expected: BTreeMap = p + .object + .symbols + .iter() + .filter(|s| s.name.starts_with("_ZTV") && s.addr != Addr::ZERO) + .map(|s| (s.addr.get(), s.name.clone())) + .collect(); + assert!(!expected.is_empty(), "{fixture}: no vtable symbols at all"); + + let found = e5r_api::vtables(&p); + for (addr, symbol) in &expected { + let table = found + .iter() + .find(|t| t.addr.get() == *addr) + .unwrap_or_else(|| panic!("{fixture}: no table at {addr:#x} for {symbol}")); + let class = table + .class + .as_deref() + .unwrap_or_else(|| panic!("{fixture}: the table at {addr:#x} has no class")); + // `_ZTV4Cube` is the table for `Cube`. + assert!( + symbol.contains(class), + "{fixture}: {symbol} came out as {class:?}" + ); + assert!( + !table.methods.is_empty(), + "{fixture}: {class} has no methods" + ); + } + } +} + +#[test] +fn the_scan_finds_the_same_tables_without_the_symbols() { + // The linked binaries: in an object file a pure virtual slot has no value + // yet, so a scan with nothing to go on cannot know the table continues + // past it. Once linked, every slot points at something. + for fixture in ["shapes.a64.O0.cpp", "shapes.a64.O2.cpp"] { + let Some(p) = open(fixture) else { continue }; + // Only the tables whose first slot holds a real function. An abstract + // class whose destructor was never emitted has an empty slot there, + // and a scan with nothing to go on cannot tell that from data: the + // symbol is the only thing that says a table starts here. + let named: BTreeMap = e5r_api::vtables(&p) + .into_iter() + .filter(|t| t.class.is_some() && t.methods.first() != Some(&Addr::ZERO)) + .map(|t| (t.addr.get(), t.methods.len())) + .collect(); + assert!(!named.is_empty(), "{fixture}: nothing to compare against"); + + let scanned: BTreeMap = e5r_api::vtables(&without_symbols(p)) + .into_iter() + .map(|t| (t.addr.get(), t.methods.len())) + .collect(); + + for (addr, methods) in &named { + let found = scanned + .get(addr) + .unwrap_or_else(|| panic!("{fixture}: the scan missed the table at {addr:#x}")); + assert_eq!( + found, methods, + "{fixture}: the table at {addr:#x} came out with {found} methods, not {methods}" + ); + } + } +} + +#[test] +fn every_slot_points_at_something_executable() { + for fixture in [ + "shapes.a64.O0.cpp.o", + "shapes.a64.O2.cpp.o", + "shapes.a64.O0.cpp", + "shapes.a64.O2.cpp", + ] { + let Some(p) = open(fixture) else { continue }; + for table in e5r_api::vtables(&p) { + for (slot, method) in table.methods.iter().enumerate() { + // A slot can be empty: a pure virtual function in an object + // file has no address until it is linked. + if *method == Addr::ZERO { + continue; + } + let executable = p + .object + .sections + .iter() + .any(|s| s.exec && s.range.contains(*method)); + assert!( + executable, + "{fixture}: slot {slot} of the table at {} points at {method}, \ + which is not code", + table.addr + ); + } + } + } +} + +#[test] +fn a_binary_with_no_classes_has_no_tables() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let found = e5r_api::vtables(&p); + assert!( + found.is_empty(), + "found {} tables in a C program: {:?}", + found.len(), + found.iter().map(|t| t.addr).collect::>() + ); +} + +/// Virtual dispatch, run in the interpreter and compared against the processor. +/// +/// The fixture writes its own answer when it runs, so the expected value came +/// out of hardware. Getting it right means the indirect calls through the +/// vtables went where the machine sent them. +#[test] +fn virtual_dispatch_interprets_to_the_same_answer() { + for fixture in ["shapes.a64.O0.cpp", "shapes.a64.O2.cpp"] { + let Some(dir) = corpus() else { return }; + let Ok(recorded) = std::fs::read(dir.join(format!("{fixture}.out"))) else { + continue; + }; + let Some(expected) = recorded + .get(..8) + .map(|b| u64::from_le_bytes(b.try_into().unwrap())) + else { + continue; + }; + let Some(p) = open(fixture) else { continue }; + let Some(f) = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some("run_shapes")) + else { + continue; + }; + let setup = e5r_api::Setup { + depth: 256, + ..Default::default() + }; + let run = e5r_api::emulate::run(&p, f, &setup); + assert_eq!( + run.stop, + e5r_ir::Stop::Returned, + "{fixture}: the run stopped with {:?}", + run.stop + ); + assert_eq!( + run.result, expected, + "{fixture}: interpreted {:#x}, the processor produced {expected:#x}", + run.result + ); + } +} diff --git a/crates/e5r-arch/Cargo.toml b/crates/e5r-arch/Cargo.toml new file mode 100644 index 0000000..60e43af --- /dev/null +++ b/crates/e5r-arch/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "e5r-arch" +description = "e5r — instruction decoders." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +e5r-core = { workspace = true } diff --git a/crates/e5r-arch/src/aarch64/mod.rs b/crates/e5r-arch/src/aarch64/mod.rs new file mode 100644 index 0000000..07f9aec --- /dev/null +++ b/crates/e5r-arch/src/aarch64/mod.rs @@ -0,0 +1,2172 @@ +//! AArch64 A64 decoder. +//! +//! Fixed 32-bit instructions, little-endian, so decoding is a pure function of +//! four bytes and the address. The top-level split follows the architecture +//! reference manual's decode tree on bits 28:25, and each group is a function. +//! +//! Aliases are resolved here rather than left to the formatter: `sbfm` with a +//! full-width extraction *is* `asr`, and analysis that has to recognize both +//! spellings is analysis with a bug waiting in it. + +// Encoding literals are grouped the way the architecture reference manual draws +// them, which is what makes them checkable against the manual. +#![allow(clippy::unusual_byte_groupings)] + +pub mod simd; +pub mod sysreg; +pub mod text; + +pub use text::{Style, format}; + +use e5r_core::Addr; + +use crate::insn::{AddrMode, Cond, Extend, Flow, Insn, Mem, Operand, Reg, RegClass, Shift, Width}; + +/// Bits `hi..=lo` of `w`. +#[inline] +pub(crate) const fn bits(w: u32, hi: u32, lo: u32) -> u32 { + (w >> lo) & ((1u32 << (hi - lo + 1)) - 1) +} + +/// Bit `n` of `w`. +#[inline] +pub(crate) const fn bit(w: u32, n: u32) -> u32 { + (w >> n) & 1 +} + +/// Sign-extend the low `n` bits of `v`. +#[inline] +const fn sext(v: u32, n: u32) -> i64 { + let shift = 64 - n; + ((v as u64) << shift) as i64 >> shift +} + +/// Register 31 is the zero register in most encodings. +#[inline] +fn r(num: u32, sf: bool) -> Reg { + let width = if sf { Width::W64 } else { Width::W32 }; + if num == 31 { + Reg { + class: RegClass::Zr, + num: 31, + width, + } + } else { + Reg::gpr(num as u8, width) + } +} + +/// Register 31 is the stack pointer in the rest. +#[inline] +fn rsp(num: u32, sf: bool) -> Reg { + let width = if sf { Width::W64 } else { Width::W32 }; + if num == 31 { + Reg { + class: RegClass::Sp, + num: 31, + width, + } + } else { + Reg::gpr(num as u8, width) + } +} + +/// Vector register at a given width. +#[inline] +fn v(num: u32, width: Width) -> Reg { + Reg::vec(num as u8, width) +} + +/// A general purpose register, for the SIMD module. +pub(crate) fn gpr_for(num: u32, wide: bool) -> Reg { + r(num, wide) +} + +/// A stack-pointer-capable register, for the SIMD module. +pub(crate) fn rsp_for(num: u32) -> Reg { + rsp(num, true) +} + +const COND_NAMES: [&str; 16] = [ + "eq", "ne", "cs", "cc", "mi", "pl", "vs", "vc", "hi", "ls", "ge", "lt", "gt", "le", "al", "nv", +]; + +/// The mnemonic for `b.`, as one static string so [`Insn`] stays `Copy`. +const B_COND: [&str; 16] = [ + "b.eq", "b.ne", "b.cs", "b.cc", "b.mi", "b.pl", "b.vs", "b.vc", "b.hi", "b.ls", "b.ge", "b.lt", + "b.gt", "b.le", "b.al", "b.nv", +]; + +/// Barrier option names, indexed by `CRm`. The unnamed slots print as numbers. +const BARRIERS: [&str; 16] = [ + "#0x0", "oshld", "oshst", "osh", "#0x4", "nshld", "nshst", "nsh", "#0x8", "ishld", "ishst", + "ish", "#0xc", "ld", "st", "sy", +]; + +/// Prefetch operation name for a 5-bit `Rt`. +/// +/// The field is `type:target:policy`: pld/pli/pst, l1/l2/l3/slc, keep/strm. +/// The eight encodings with no type print as bare numbers, as objdump does. +fn prefetch_name(rt: u32) -> &'static str { + const NAMES: [&str; 32] = [ + "pldl1keep", + "pldl1strm", + "pldl2keep", + "pldl2strm", + "pldl3keep", + "pldl3strm", + "pldslckeep", + "pldslcstrm", + "plil1keep", + "plil1strm", + "plil2keep", + "plil2strm", + "plil3keep", + "plil3strm", + "plislckeep", + "plislcstrm", + "pstl1keep", + "pstl1strm", + "pstl2keep", + "pstl2strm", + "pstl3keep", + "pstl3strm", + "pstslckeep", + "pstslcstrm", + "#0x18", + "#0x19", + "#0x1a", + "#0x1b", + "#0x1c", + "#0x1d", + "#0x1e", + "#0x1f", + ]; + NAMES[(rt & 31) as usize] +} + +/// PSTATE field name for an `op1:op2` pair. +fn pstate_name(op1: u32, op2: u32) -> &'static str { + match (op1, op2) { + (0, 3) => "uao", + (0, 4) => "pan", + (0, 5) => "spsel", + (3, 1) => "tco", + (3, 2) => "ssbs", + (3, 4) => "dit", + (3, 6) => "daifset", + (3, 7) => "daifclr", + _ => "pstate", + } +} + +/// The DC, IC, AT and TLBI operations, by `op1:CRn:CRm:op2`. +fn sys_alias(op1: u32, crn: u32, crm: u32, op2: u32) -> Option<(&'static str, &'static str)> { + Some(match (op1, crn, crm, op2) { + (0, 7, 1, 0) => ("ic", "ialluis"), + (0, 7, 5, 0) => ("ic", "iallu"), + (3, 7, 5, 1) => ("ic", "ivau"), + (0, 7, 6, 1) => ("dc", "ivac"), + (0, 7, 6, 2) => ("dc", "isw"), + (0, 7, 10, 2) => ("dc", "csw"), + (0, 7, 14, 2) => ("dc", "cisw"), + (3, 7, 4, 1) => ("dc", "zva"), + (3, 7, 4, 3) => ("dc", "gva"), + (3, 7, 4, 4) => ("dc", "gzva"), + (3, 7, 10, 1) => ("dc", "cvac"), + (3, 7, 11, 1) => ("dc", "cvau"), + (3, 7, 12, 1) => ("dc", "cvap"), + (3, 7, 13, 1) => ("dc", "cvadp"), + (3, 7, 14, 1) => ("dc", "civac"), + (0, 7, 8, 0) => ("at", "s1e1r"), + (0, 7, 8, 1) => ("at", "s1e1w"), + (0, 7, 8, 2) => ("at", "s1e0r"), + (0, 7, 8, 3) => ("at", "s1e0w"), + (0, 8, 3, 0) => ("tlbi", "vmalle1is"), + (0, 8, 3, 1) => ("tlbi", "vae1is"), + (0, 8, 3, 3) => ("tlbi", "vaae1is"), + (0, 8, 7, 0) => ("tlbi", "vmalle1"), + (0, 8, 7, 1) => ("tlbi", "vae1"), + (0, 8, 7, 3) => ("tlbi", "vaae1"), + _ => return None, + }) +} + +/// Render a system register encoding, given bits 20:5 of the instruction. +pub fn sysreg_name(enc: u32) -> String { + // Tagged values from the generic SYS form print as a bare `cN`. + if enc & 0xffff_0000 != 0 { + return format!("C{}", enc & 0xffff); + } + if let Ok(e) = u16::try_from(enc) + && let Some(n) = sysreg::lookup(e) + { + return n.to_string(); + } + // binutils prints op0 straight from bits 20:19, which is 2 or 3 for a + // normal MRS and 0 or 1 for the encodings it does not consider one. + let op0 = (enc >> 14) & 3; + let op1 = (enc >> 11) & 7; + let crn = (enc >> 7) & 15; + let crm = (enc >> 3) & 15; + let op2 = enc & 7; + format!("s{op0}_{op1}_c{crn}_c{crm}_{op2}") +} + +/// Name of a condition code. +pub fn cond_name(c: Cond) -> &'static str { + COND_NAMES[(c.0 & 15) as usize] +} + +/// Decode one instruction at `addr`. +/// +/// Returns `None` when fewer than four bytes are available or the encoding is +/// not allocated. An unallocated encoding is not an error: it is data, or a +/// newer extension, and the caller decides which. +pub fn decode(bytes: &[u8], addr: Addr) -> Option { + if bytes.len() < 4 { + return None; + } + let w = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + decode_word(w, addr) +} + +/// Decode from an already-assembled word. +pub fn decode_word(w: u32, addr: Addr) -> Option { + let op0 = bits(w, 28, 25); + match op0 { + 0b1000 | 0b1001 => dp_immediate(w, addr), + 0b1010 | 0b1011 => branches(w, addr), + 0b0100 | 0b0110 | 0b1100 | 0b1110 => loads_stores(w, addr), + 0b0101 | 0b1101 => dp_register(w, addr), + 0b0111 | 0b1111 => dp_simd(w, addr), + // 0b0000 is reserved; UDF #imm16 lives at the bottom of it. + 0b0000 if bits(w, 31, 16) == 0 => { + let mut i = ins(addr, "udf", Flow::Trap); + i.push(Operand::Count(bits(w, 15, 0) as i64)); + Some(i) + } + _ => None, + } +} + +fn ins(addr: Addr, mnemonic: &'static str, flow: Flow) -> Insn { + Insn::new(addr, 4, mnemonic, flow) +} + +// ---------------------------------------------------------------- immediates + +fn dp_immediate(w: u32, addr: Addr) -> Option { + match bits(w, 25, 23) { + 0b000 | 0b001 => pc_rel(w, addr), + 0b010 => add_sub_imm(w, addr), + 0b011 => add_sub_imm_tags(w, addr), + 0b100 => logical_imm(w, addr), + 0b101 => move_wide(w, addr), + 0b110 => bitfield(w, addr), + 0b111 => extract(w, addr), + _ => None, + } +} + +/// ADR and ADRP: the address-forming pair that makes a string reference. +fn pc_rel(w: u32, addr: Addr) -> Option { + let imm = (bits(w, 23, 5) << 2) | bits(w, 30, 29); + let page = bit(w, 31) == 1; + let (mnem, target) = if page { + // ADRP shifts by 12 and drops the low 12 bits of the pc. + let off = sext(imm, 21) << 12; + ("adrp", Addr(addr.get() & !0xfff).wrapping_offset(off)) + } else { + ("adr", addr.wrapping_offset(sext(imm, 21))) + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(bits(w, 4, 0), true))); + i.push(Operand::Addr(target)); + Some(i) +} + +fn add_sub_imm(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let sub = bit(w, 30) == 1; + let setflags = bit(w, 29) == 1; + let sh = bit(w, 22) == 1; + let imm12 = bits(w, 21, 10); + let imm = imm12 as i64; + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + + // Aliases objdump prefers. + if setflags && rd == 31 { + let mut i = ins(addr, if sub { "cmp" } else { "cmn" }, Flow::Next); + i.push(Operand::Reg(rsp(rn, sf))); + i.push(Operand::Imm(imm)); + if sh { + i.push(Operand::ShiftOp(Shift::Lsl, 12)); + } + return Some(i); + } + if !sub && !setflags && !sh && imm == 0 && (rd == 31 || rn == 31) { + let mut i = ins(addr, "mov", Flow::Next); + i.push(Operand::Reg(rsp(rd, sf))); + i.push(Operand::Reg(rsp(rn, sf))); + return Some(i); + } + + let mnem = match (sub, setflags) { + (false, false) => "add", + (false, true) => "adds", + (true, false) => "sub", + (true, true) => "subs", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(if setflags { r(rd, sf) } else { rsp(rd, sf) })); + i.push(Operand::Reg(rsp(rn, sf))); + i.push(Operand::Imm(imm)); + if sh { + i.push(Operand::ShiftOp(Shift::Lsl, 12)); + } + Some(i) +} + +/// ADDG and SUBG, the memory-tagging forms. +fn add_sub_imm_tags(w: u32, addr: Addr) -> Option { + if bit(w, 31) == 0 || bit(w, 29) == 1 { + return None; + } + // Bit 22 set makes this the CSSC min/max immediate group instead. + if bit(w, 22) == 1 { + return min_max_imm(w, addr); + } + let mut i = ins( + addr, + if bit(w, 30) == 1 { "subg" } else { "addg" }, + Flow::Next, + ); + i.push(Operand::Reg(rsp(bits(w, 4, 0), true))); + i.push(Operand::Reg(rsp(bits(w, 9, 5), true))); + i.push(Operand::Imm((bits(w, 21, 16) as i64) << 4)); + i.push(Operand::Imm(bits(w, 13, 10) as i64)); + Some(i) +} + +/// SMAX, UMAX, SMIN and UMIN against an 8-bit immediate (FEAT_CSSC). +fn min_max_imm(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let imm8 = bits(w, 17, 10); + let (mnem, signed) = match bits(w, 21, 18) { + 0b0000 => ("smax", true), + 0b0001 => ("umax", false), + 0b0010 => ("smin", true), + 0b0011 => ("umin", false), + _ => return None, + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(bits(w, 4, 0), sf))); + i.push(Operand::Reg(r(bits(w, 9, 5), sf))); + i.push(Operand::Count(if signed { + sext(imm8, 8) + } else { + imm8 as i64 + })); + Some(i) +} + +/// The logical immediate encoding: a run of ones rotated within a power-of-two +/// element, packed into `N:immr:imms`. Returns `None` for the reserved patterns. +fn decode_bit_masks(n: u32, imms: u32, immr: u32, sf: bool) -> Option { + let width = if sf { 64 } else { 32 }; + // The element size is found from the highest clear bit of `imms` under `n`. + let combined = (n << 6) | (!imms & 0x3f); + let len = 31 - (combined | 1).leading_zeros(); + if len == 0 || (1u32 << len) > width { + return None; + } + let esize = 1u32 << len; + let levels = esize - 1; + // S = esize - 1 is reserved. + if imms & levels == levels { + return None; + } + let s = imms & levels; + let r = immr & levels; + let welem: u64 = if s + 1 >= 64 { + u64::MAX + } else { + (1u64 << (s + 1)) - 1 + }; + // Rotate right within the element, then replicate to the full width. + let elem = if r == 0 { + welem + } else { + let e = esize as u64; + let masked = welem & if e >= 64 { u64::MAX } else { (1u64 << e) - 1 }; + ((masked >> r) | (masked << (e - r as u64))) + & if e >= 64 { u64::MAX } else { (1u64 << e) - 1 } + }; + let mut out = 0u64; + let mut pos = 0; + while pos < width { + out |= elem << pos; + pos += esize; + } + Some(if sf { out } else { out & 0xffff_ffff }) +} + +/// Whether a `MOVZ` or `MOVN` would spell this bitmask immediate, in which +/// case `ORR Rd, ZR, #imm` keeps its own name rather than taking the MOV alias. +/// +/// The ARM ARM defines the test as written here; it is the reason `orr x1, +/// xzr, #0x10` disassembles as itself while a value no move-wide can reach +/// disassembles as `mov`. +fn move_wide_preferred(sf: bool, n: u32, imms: u32, immr: u32) -> bool { + let width = if sf { 64 } else { 32 }; + // The immediate's element size has to be the whole register. + if sf && n != 1 { + return false; + } + if !sf && (n != 0 || imms & 0x20 != 0) { + return false; + } + // The immediate is a run of `imms + 1` ones rotated right by `immr`. A + // MOVZ can spell it when that run fits inside one aligned halfword; a MOVN + // when the complementary run of zeros does. Both are the same question + // asked of a cyclic run: where it starts within its halfword, plus how + // long it is, must not pass sixteen. + if imms < 16 { + // The ones. `(16 - immr % 16) % 16` is where they begin. + return (16 - immr % 16) % 16 <= 15 - imms; + } + // The zeros, which begin `imms + 1` past the ones and are as many as the + // register has left. Written as the mirror of the branch above rather than + // as the ARM ARM's closed form, which is a subtraction that underflows in + // `u32` for every `imms` below `width` -- the whole of this branch. + let zeros = width - imms - 1; + if zeros <= 16 { + let start = (16 - (zeros + immr) % 16) % 16; + return start + zeros <= 16; + } + false +} + +fn logical_imm(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let opc = bits(w, 30, 29); + let n = bit(w, 22); + if !sf && n == 1 { + return None; + } + let imm = decode_bit_masks(n, bits(w, 15, 10), bits(w, 21, 16), sf)?; + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + + // ANDS with a discarded destination is TST; ORR from zr is MOV. + if opc == 0b11 && rd == 31 { + let mut i = ins(addr, "tst", Flow::Next); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::UImm(imm)); + return Some(i); + } + if opc == 0b01 && rn == 31 && !move_wide_preferred(sf, n, bits(w, 15, 10), bits(w, 21, 16)) { + let mut i = ins(addr, "mov", Flow::Next); + i.push(Operand::Reg(rsp(rd, sf))); + i.push(Operand::UImm(imm)); + return Some(i); + } + + let mnem = match opc { + 0b00 => "and", + 0b01 => "orr", + 0b10 => "eor", + _ => "ands", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(if opc == 0b11 { + r(rd, sf) + } else { + rsp(rd, sf) + })); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::UImm(imm)); + Some(i) +} + +fn move_wide(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let opc = bits(w, 30, 29); + let hw = bits(w, 22, 21); + if !sf && hw > 1 { + return None; + } + let imm16 = bits(w, 20, 5) as u64; + let shift = hw * 16; + let rd = bits(w, 4, 0); + + match opc { + 0b00 => { + // MOVN with a value that fits is spelled MOV by objdump. + let val = !(imm16 << shift); + let val = if sf { val } else { val & 0xffff_ffff }; + if !(imm16 == 0 && hw != 0) { + let mut i = ins(addr, "mov", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::UImm(val)); + return Some(i); + } + let mut i = ins(addr, "movn", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Imm(imm16 as i64)); + if shift != 0 { + i.push(Operand::ShiftOp(Shift::Lsl, shift as u8)); + } + Some(i) + } + 0b10 => { + if !(imm16 == 0 && hw != 0) { + let mut i = ins(addr, "mov", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::UImm(imm16 << shift)); + return Some(i); + } + let mut i = ins(addr, "movz", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Imm(imm16 as i64)); + // Only a zero immediate reaches here, so the shift is the whole + // difference between this and any other encoding of the same + // value, and dropping it printed four distinct instructions the + // same way. + if shift != 0 { + i.push(Operand::ShiftOp(Shift::Lsl, shift as u8)); + } + Some(i) + } + 0b11 => { + let mut i = ins(addr, "movk", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Imm(imm16 as i64)); + if shift != 0 { + i.push(Operand::ShiftOp(Shift::Lsl, shift as u8)); + } + Some(i) + } + _ => None, + } +} + +fn bitfield(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let opc = bits(w, 30, 29); + let n = bit(w, 22); + if n != bit(w, 31) { + return None; + } + let immr = bits(w, 21, 16); + let imms = bits(w, 15, 10); + // The 32-bit forms hold five bits in each field. A sixth one set is an + // undefined encoding, which llvm and binutils both refuse -- and which + // this decoder turned into a shift amount by subtracting `immr` from a + // width smaller than it. + if !sf && (immr & 0x20 != 0 || imms & 0x20 != 0) { + return None; + } + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let width = if sf { 64 } else { 32 }; + + let signed = opc == 0b00; + let unsigned = opc == 0b10; + + // The alias set the manual defines, in the order objdump prefers them. + // LSL is UBFM with imms = immr - 1. The signed encoding of that shape is + // SBFIZ, not a shift, and falls through to the insert forms below. + if unsigned && imms + 1 == immr { + let mut i = ins(addr, "lsl", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Count((width - immr) as i64)); + return Some(i); + } + if imms == width - 1 && (signed || unsigned) { + let mut i = ins(addr, if signed { "asr" } else { "lsr" }, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Count(immr as i64)); + return Some(i); + } + if immr == 0 && (signed || unsigned) { + let ext = match (signed, imms) { + (true, 7) => Some("sxtb"), + (true, 15) => Some("sxth"), + (true, 31) if sf => Some("sxtw"), + // There is no 64-bit UXTB or UXTH: zero extending into an X + // register is already what writing a W register does. + (false, 7) if !sf => Some("uxtb"), + (false, 15) if !sf => Some("uxth"), + _ => None, + }; + if let Some(m) = ext { + let mut i = ins(addr, m, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, false))); + return Some(i); + } + } + if imms < immr { + // Insert forms. + let mnem = match opc { + 0b00 => "sbfiz", + 0b01 => "bfi", + _ => "ubfiz", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Count((width - immr) as i64)); + i.push(Operand::Count((imms + 1) as i64)); + return Some(i); + } + + let mnem = match opc { + 0b00 => "sbfx", + 0b01 => "bfxil", + _ => "ubfx", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Count(immr as i64)); + i.push(Operand::Count((imms - immr + 1) as i64)); + Some(i) +} + +fn extract(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + if bits(w, 30, 29) != 0 || bit(w, 22) != bit(w, 31) { + return None; + } + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + let imms = bits(w, 15, 10); + if rn == rm { + let mut i = ins(addr, "ror", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Count(imms as i64)); + return Some(i); + } + let mut i = ins(addr, "extr", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Reg(r(rm, sf))); + i.push(Operand::Count(imms as i64)); + Some(i) +} + +// ------------------------------------------------------------------ branches + +fn branches(w: u32, addr: Addr) -> Option { + let op0 = bits(w, 31, 29); + + // Conditional branch immediate. + if op0 == 0b010 && bit(w, 25) == 0 { + let cond = bits(w, 3, 0); + if bit(w, 4) != 0 { + return None; + } + let target = addr.wrapping_offset(sext(bits(w, 23, 5), 19) << 2); + let mut i = ins(addr, B_COND[cond as usize], Flow::CondBranch(target)); + i.push(Operand::Addr(target)); + return Some(i); + } + + // Exception generation. The whole 0xd4 page is this group; an extra + // condition on op1 here silently dropped every svc and brk. + if bits(w, 31, 24) == 0b1101_0100 { + return exception(w, addr); + } + // System. + if bits(w, 31, 22) == 0b1101_0101_00 { + return system(w, addr); + } + // Unconditional branch register. + if bits(w, 31, 25) == 0b1101_011 { + return branch_register(w, addr); + } + // Unconditional branch immediate. + if bits(w, 30, 26) == 0b00101 { + let link = bit(w, 31) == 1; + let target = addr.wrapping_offset(sext(bits(w, 25, 0), 26) << 2); + let flow = if link { + Flow::Call(target) + } else { + Flow::Branch(target) + }; + let mut i = ins(addr, if link { "bl" } else { "b" }, flow); + i.push(Operand::Addr(target)); + return Some(i); + } + // Compare and branch. + if bits(w, 30, 25) == 0b011010 { + let sf = bit(w, 31) == 1; + let neg = bit(w, 24) == 1; + let target = addr.wrapping_offset(sext(bits(w, 23, 5), 19) << 2); + let mut i = ins( + addr, + if neg { "cbnz" } else { "cbz" }, + Flow::CondBranch(target), + ); + i.push(Operand::Reg(r(bits(w, 4, 0), sf))); + i.push(Operand::Addr(target)); + return Some(i); + } + // Test and branch. + if bits(w, 30, 25) == 0b011011 { + let neg = bit(w, 24) == 1; + let b = (bit(w, 31) << 5) | bits(w, 23, 19); + let target = addr.wrapping_offset(sext(bits(w, 18, 5), 14) << 2); + let mut i = ins( + addr, + if neg { "tbnz" } else { "tbz" }, + Flow::CondBranch(target), + ); + i.push(Operand::Reg(r(bits(w, 4, 0), b >= 32))); + i.push(Operand::Count(b as i64)); + i.push(Operand::Addr(target)); + return Some(i); + } + None +} + +fn exception(w: u32, addr: Addr) -> Option { + let opc = bits(w, 23, 21); + let ll = bits(w, 1, 0); + let imm16 = bits(w, 20, 5) as i64; + let (mnem, flow) = match (opc, ll) { + (0b000, 0b01) => ("svc", Flow::Syscall), + (0b000, 0b10) => ("hvc", Flow::Syscall), + (0b000, 0b11) => ("smc", Flow::Syscall), + (0b001, 0b00) => ("brk", Flow::Trap), + (0b010, 0b00) => ("hlt", Flow::Trap), + (0b101, 0b01) => ("dcps1", Flow::Trap), + (0b101, 0b10) => ("dcps2", Flow::Trap), + (0b101, 0b11) => ("dcps3", Flow::Trap), + _ => return None, + }; + let mut i = ins(addr, mnem, flow); + i.push(Operand::Imm(imm16)); + Some(i) +} + +fn system(w: u32, addr: Addr) -> Option { + // Hint space: NOP and the pointer-authentication hints. + if bits(w, 31, 12) == 0b1101_0101_0000_0011_0010 && bits(w, 4, 0) == 0b11111 { + let crm = bits(w, 11, 8); + let op2 = bits(w, 7, 5); + let hint = (crm << 3) | op2; + let mnem = match hint { + 0 => "nop", + 1 => "yield", + 2 => "wfe", + 3 => "wfi", + 4 => "sev", + 5 => "sevl", + 7 => "xpaclri", + 8 => "pacia1716", + 10 => "pacib1716", + 12 => "autia1716", + 14 => "autib1716", + 24 => "paciaz", + 25 => "paciasp", + 26 => "pacibz", + 27 => "pacibsp", + 28 => "autiaz", + 29 => "autiasp", + 30 => "autibz", + 31 => "autibsp", + 32 | 34 | 36 | 38 => "bti", + _ => "hint", + }; + let mut i = ins(addr, mnem, Flow::Next); + if mnem == "hint" { + i.push(Operand::Imm(hint as i64)); + } else if mnem == "bti" { + i.push(Operand::Name(match op2 { + 2 => "c", + 4 => "j", + 6 => "jc", + _ => "", + })); + } + return Some(i); + } + // Barriers. + if bits(w, 31, 12) == 0b1101_0101_0000_0011_0011 { + let crm = bits(w, 11, 8); + let mnem = match bits(w, 7, 5) { + 0b010 => "clrex", + 0b100 => "dsb", + 0b101 => "dmb", + 0b110 => "isb", + 0b111 => "sb", + _ => return None, + }; + let mut i = ins(addr, mnem, Flow::Next); + if mnem != "sb" && mnem != "clrex" { + i.push(Operand::Name(BARRIERS[crm as usize])); + } + return Some(i); + } + // MSR immediate to a pstate field. + if bits(w, 31, 19) == 0b1101_0101_0000_0 && bits(w, 4, 0) == 0b11111 { + let op1 = bits(w, 18, 16); + let op2 = bits(w, 7, 5); + let crm = bits(w, 11, 8); + // SMSTART and SMSTOP are PSTATE writes with their own spelling. + if op1 == 0b011 && op2 == 0b011 { + let start = crm & 1 == 1; + let mut i = ins(addr, if start { "smstart" } else { "smstop" }, Flow::Next); + match crm >> 1 { + 0b001 => i.push(Operand::Name("sm")), + 0b010 => i.push(Operand::Name("za")), + _ => &mut i, + }; + return Some(i); + } + let mut i = ins(addr, "msr", Flow::Next); + i.push(Operand::Name(pstate_name(op1, op2))); + i.push(Operand::Imm(crm as i64)); + return Some(i); + } + // SYS and SYSL: cache, TLB and address translation maintenance, which + // assemblers spell as DC, IC, AT and TLBI. + if bits(w, 31, 22) == 0b1101_0101_00 && bits(w, 20, 19) == 0b01 { + let op1 = bits(w, 18, 16); + let crn = bits(w, 15, 12); + let crm = bits(w, 11, 8); + let op2 = bits(w, 7, 5); + let rt = bits(w, 4, 0); + if let Some((mnem, arg)) = sys_alias(op1, crn, crm, op2) { + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Name(arg)); + i.push(Operand::Reg(r(rt, true))); + return Some(i); + } + let mut i = ins( + addr, + if bit(w, 21) == 1 { "sysl" } else { "sys" }, + Flow::Next, + ); + i.push(Operand::Count(op1 as i64)); + i.push(Operand::Sys(0x1_0000 | crn)); + i.push(Operand::Sys(0x2_0000 | crm)); + i.push(Operand::Count(op2 as i64)); + i.push(Operand::Reg(r(rt, true))); + return Some(i); + } + + // MRS and MSR register. Bit 21 is L, and it alone gives the direction. + if bits(w, 31, 22) == 0b1101_0101_00 { + let read = bit(w, 21) == 1; + let sysreg = bits(w, 20, 5); + let rt = bits(w, 4, 0); + let mut i = ins(addr, if read { "mrs" } else { "msr" }, Flow::Next); + if read { + i.push(Operand::Reg(r(rt, true))); + i.push(Operand::Sys(sysreg)); + } else { + i.push(Operand::Sys(sysreg)); + i.push(Operand::Reg(r(rt, true))); + } + return Some(i); + } + None +} + +fn branch_register(w: u32, addr: Addr) -> Option { + let opc = bits(w, 24, 21); + let op2 = bits(w, 20, 16); + let op3 = bits(w, 15, 10); + let rn = bits(w, 9, 5); + let op4 = bits(w, 4, 0); + if op2 != 0b11111 { + return None; + } + + // Pointer-authenticated forms carry a modifier in op3. + let authed = op3 & 0b111110 == 0b000010; + match (opc, op3, op4) { + (0b0000, 0, 0) => { + let mut i = ins(addr, "br", Flow::IndirectBranch); + i.push(Operand::Reg(r(rn, true))); + Some(i) + } + (0b0001, 0, 0) => { + let mut i = ins(addr, "blr", Flow::IndirectCall); + i.push(Operand::Reg(r(rn, true))); + Some(i) + } + (0b0010, 0, 0) => { + let mut i = ins(addr, "ret", Flow::Return); + // objdump prints the register only when it is not x30. + if rn != 30 { + i.push(Operand::Reg(r(rn, true))); + } + Some(i) + } + (0b0100, 0, 0) if rn == 31 => Some(ins(addr, "eret", Flow::Return)), + (0b0101, 0, 0) if rn == 31 => Some(ins(addr, "drps", Flow::Trap)), + (0b0000, _, _) if authed => { + let mut i = ins( + addr, + if bit(w, 10) == 1 { "braa" } else { "braaz" }, + Flow::IndirectBranch, + ); + i.push(Operand::Reg(r(rn, true))); + Some(i) + } + (0b0001, _, _) if authed => { + let mut i = ins( + addr, + if bit(w, 10) == 1 { "blraa" } else { "blraaz" }, + Flow::IndirectCall, + ); + i.push(Operand::Reg(r(rn, true))); + Some(i) + } + (0b0010, _, _) if authed => Some(ins(addr, "retaa", Flow::Return)), + _ => None, + } +} + +// ----------------------------------------------------------- loads and stores + +fn loads_stores(w: u32, addr: Addr) -> Option { + // Load register (literal): a pc-relative load, and the only load whose + // target address analysis can resolve without dataflow. + if bits(w, 29, 27) == 0b011 && bit(w, 25) == 0 && bits(w, 24, 24) == 0 { + let opc = bits(w, 31, 30); + let simd = bit(w, 26) == 1; + let target = addr.wrapping_offset(sext(bits(w, 23, 5), 19) << 2); + let rt = bits(w, 4, 0); + let (mnem, reg) = match (opc, simd) { + (0b00, false) => ("ldr", r(rt, false)), + (0b01, false) => ("ldr", r(rt, true)), + (0b10, false) => ("ldrsw", r(rt, true)), + (0b11, false) => ("prfm", r(rt, true)), + (0b00, true) => ("ldr", v(rt, Width::W32)), + (0b01, true) => ("ldr", v(rt, Width::W64)), + (0b10, true) => ("ldr", v(rt, Width::W128)), + _ => return None, + }; + let mut i = ins(addr, mnem, Flow::Next); + if mnem == "prfm" { + i.push(Operand::Name(prefetch_name(rt))); + } else { + i.push(Operand::Reg(reg)); + } + i.push(Operand::Addr(target)); + return Some(i); + } + + // Load/store pair, in its three addressing modes. + if bits(w, 29, 27) == 0b101 { + return load_store_pair(w, addr); + } + // Load/store register, all forms. + if bits(w, 29, 27) == 0b111 { + return load_store_reg(w, addr); + } + // Load/store exclusive. + if bits(w, 29, 24) == 0b001000 { + return load_store_exclusive(w, addr); + } + None +} + +/// Element size and mnemonic base for a pair instruction. +fn pair_form(w: u32) -> Option<(Reg, Reg, u64, &'static str)> { + let opc = bits(w, 31, 30); + let simd = bit(w, 26) == 1; + let load = bit(w, 22) == 1; + let rt = bits(w, 4, 0); + let rt2 = bits(w, 14, 10); + let (a, b, size) = match (opc, simd) { + (0b00, false) => (r(rt, false), r(rt2, false), 4), + // opc 01 without SIMD is LDPSW when loading and STGP when storing. + // LDPSW writes 64-bit registers but transfers 4 bytes; STGP scales by + // the 16-byte tag granule. + (0b01, false) => (r(rt, true), r(rt2, true), if load { 4 } else { 16 }), + (0b10, false) => (r(rt, true), r(rt2, true), 8), + (0b00, true) => (v(rt, Width::W32), v(rt2, Width::W32), 4), + (0b01, true) => (v(rt, Width::W64), v(rt2, Width::W64), 8), + (0b10, true) => (v(rt, Width::W128), v(rt2, Width::W128), 16), + _ => return None, + }; + let mnem = if opc == 0b01 && !simd { + if load { "ldpsw" } else { "stgp" } + } else if load { + "ldp" + } else { + "stp" + }; + Some((a, b, size, mnem)) +} + +fn load_store_pair(w: u32, addr: Addr) -> Option { + let (rt, rt2, size, mnem) = pair_form(w)?; + let mode = match bits(w, 24, 23) { + 0b00 => AddrMode::Offset, // no-allocate, printed the same + 0b01 => AddrMode::PostIndex, + 0b10 => AddrMode::Offset, + 0b11 => AddrMode::PreIndex, + _ => return None, + }; + let mnem = if bits(w, 24, 23) == 0b00 { + if mnem == "ldp" { "ldnp" } else { "stnp" } + } else { + mnem + }; + let imm7 = sext(bits(w, 21, 15), 7) * size as i64; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(rt)); + i.push(Operand::Reg(rt2)); + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(bits(w, 9, 5), true)), + index: None, + disp: imm7, + mode, + size: size * 2, + })); + Some(i) +} + +/// Mnemonic and transfer size for a scalar load or store. +fn reg_form(size: u32, opc: u32, simd: bool, rt: u32) -> Option<(&'static str, Reg, u64)> { + if simd { + let width = match (size, bit_of(opc, 1)) { + (0b00, 0) => Width::W8, + (0b01, 0) => Width::W16, + (0b10, 0) => Width::W32, + (0b11, 0) => Width::W64, + (0b00, 1) => Width::W128, + _ => return None, + }; + let load = opc & 1 == 1; + return Some(( + if load { "ldr" } else { "str" }, + v(rt, width), + width.bytes(), + )); + } + Some(match (size, opc) { + (0b00, 0b00) => ("strb", r(rt, false), 1), + (0b00, 0b01) => ("ldrb", r(rt, false), 1), + (0b00, 0b10) => ("ldrsb", r(rt, true), 1), + (0b00, 0b11) => ("ldrsb", r(rt, false), 1), + (0b01, 0b00) => ("strh", r(rt, false), 2), + (0b01, 0b01) => ("ldrh", r(rt, false), 2), + (0b01, 0b10) => ("ldrsh", r(rt, true), 2), + (0b01, 0b11) => ("ldrsh", r(rt, false), 2), + (0b10, 0b00) => ("str", r(rt, false), 4), + (0b10, 0b01) => ("ldr", r(rt, false), 4), + (0b10, 0b10) => ("ldrsw", r(rt, true), 4), + (0b11, 0b00) => ("str", r(rt, true), 8), + (0b11, 0b01) => ("ldr", r(rt, true), 8), + (0b11, 0b10) => ("prfm", r(rt, true), 8), + _ => return None, + }) +} + +#[inline] +const fn bit_of(v: u32, n: u32) -> u32 { + (v >> n) & 1 +} + +fn load_store_reg(w: u32, addr: Addr) -> Option { + let size = bits(w, 31, 30); + let simd = bit(w, 26) == 1; + let opc = bits(w, 23, 22); + let rt = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let (mnem, reg, xfer) = reg_form(size, opc, simd, rt)?; + + // Unsigned immediate offset, the common form. + if bit(w, 24) == 1 { + let imm = (bits(w, 21, 10) as i64) * xfer as i64; + let mut i = ins(addr, mnem, Flow::Next); + if mnem == "prfm" { + i.push(Operand::Name(prefetch_name(rt))); + } else { + i.push(Operand::Reg(reg)); + } + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: None, + disp: imm, + mode: AddrMode::Offset, + size: xfer, + })); + return Some(i); + } + + // Atomic memory operations share the register-offset slot, distinguished + // by bit 21. Without this they decode as LDUR/STUR of the wrong register. + if bit(w, 21) == 1 && bits(w, 11, 10) == 0 && !simd { + return atomic_memory(w, addr); + } + // LDRAA and LDRAB: authenticated loads with a scaled 10-bit offset. + if size == 0b11 && !simd && bit(w, 21) == 1 && bit(w, 10) == 1 { + let offset = sext((bit(w, 22) << 9) | bits(w, 20, 12), 10) * 8; + let writeback = bit(w, 11) == 1; + let mut i = ins( + addr, + if bit(w, 23) == 1 { "ldrab" } else { "ldraa" }, + Flow::Next, + ); + i.push(Operand::Reg(r(rt, true))); + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: None, + disp: offset, + mode: if writeback { + AddrMode::PreIndex + } else { + AddrMode::Offset + }, + size: 8, + })); + return Some(i); + } + + let imm9 = sext(bits(w, 20, 12), 9); + match bits(w, 11, 10) { + // Unscaled immediate: LDUR and friends. + 0b00 => { + let unscaled: &'static str = match mnem { + "ldr" => "ldur", + "str" => "stur", + "ldrb" => "ldurb", + "strb" => "sturb", + "ldrh" => "ldurh", + "strh" => "sturh", + "ldrsb" => "ldursb", + "ldrsh" => "ldursh", + "ldrsw" => "ldursw", + "prfm" => "prfum", + other => other, + }; + let mut i = ins(addr, unscaled, Flow::Next); + if unscaled == "prfum" { + i.push(Operand::Name(prefetch_name(rt))); + } else { + i.push(Operand::Reg(reg)); + } + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: None, + disp: imm9, + mode: AddrMode::Offset, + size: xfer, + })); + Some(i) + } + 0b01 | 0b11 => { + let mode = if bits(w, 11, 10) == 0b01 { + AddrMode::PostIndex + } else { + AddrMode::PreIndex + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(reg)); + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: None, + disp: imm9, + mode, + size: xfer, + })); + Some(i) + } + // Unprivileged access: the register-offset slot with bit 21 clear. + 0b10 if bit(w, 21) == 0 => { + let unpriv: &'static str = match mnem { + "ldr" => "ldtr", + "str" => "sttr", + "ldrb" => "ldtrb", + "strb" => "sttrb", + "ldrh" => "ldtrh", + "strh" => "sttrh", + "ldrsb" => "ldtrsb", + "ldrsh" => "ldtrsh", + "ldrsw" => "ldtrsw", + other => other, + }; + let mut i = ins(addr, unpriv, Flow::Next); + i.push(Operand::Reg(reg)); + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: None, + disp: imm9, + mode: AddrMode::Offset, + size: xfer, + })); + Some(i) + } + // Register offset. + 0b10 => { + let option = bits(w, 15, 13); + let s = bit(w, 12); + let rm = bits(w, 20, 16); + let extend = match option { + 0b010 => Extend::Uxtw, + 0b011 => Extend::Lsl, + 0b110 => Extend::Sxtw, + 0b111 => Extend::Sxtx, + _ => return None, + }; + // With S set objdump prints the shift even when it is zero, + // which for a byte access it always is. + let amount = if s == 1 { + xfer.trailing_zeros() as u8 + } else { + 0 + }; + let explicit_zero_shift = s == 1 && amount == 0 && extend == Extend::Lsl; + let index_wide = matches!(extend, Extend::Lsl | Extend::Sxtx); + let mut i = ins(addr, mnem, Flow::Next); + if mnem == "prfm" { + i.push(Operand::Name(prefetch_name(rt))); + } else { + i.push(Operand::Reg(reg)); + } + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: Some(( + r(rm, index_wide), + if explicit_zero_shift { + Extend::LslZero + } else { + extend + }, + amount, + )), + disp: 0, + mode: AddrMode::Offset, + size: xfer, + })); + Some(i) + } + _ => None, + } +} + +/// Atomic compare-and-swap, which shares the exclusive group's encoding space. +fn compare_and_swap(w: u32, addr: Addr) -> Option { + let size = bits(w, 31, 30); + if bits(w, 14, 10) != 0b11111 { + return None; + } + let sf = size == 0b11; + let acquire = bit(w, 22) == 1; + let release = bit(w, 15) == 1; + let mnem: &'static str = match (acquire, release, size) { + (false, false, 0b00) => "casb", + (false, true, 0b00) => "caslb", + (true, false, 0b00) => "casab", + (true, true, 0b00) => "casalb", + (false, false, 0b01) => "cash", + (false, true, 0b01) => "caslh", + (true, false, 0b01) => "casah", + (true, true, 0b01) => "casalh", + (false, false, _) => "cas", + (false, true, _) => "casl", + (true, false, _) => "casa", + (true, true, _) => "casal", + }; + let narrow = size < 0b10; + let width = sf && !narrow; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(bits(w, 20, 16), width))); + i.push(Operand::Reg(r(bits(w, 4, 0), width))); + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(bits(w, 9, 5), true)), + index: None, + disp: 0, + mode: AddrMode::Offset, + size: 1u64 << size, + })); + Some(i) +} + +/// LDADD and the rest of the atomic read-modify-write family, plus SWP. +fn atomic_memory(w: u32, addr: Addr) -> Option { + let size = bits(w, 31, 30); + let a = bit(w, 23) == 1; + let rl = bit(w, 22) == 1; + let o3 = bit(w, 15) == 1; + let opc = bits(w, 14, 12); + let rs = bits(w, 20, 16); + let rt = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let sf = size == 0b11; + let narrow = match size { + 0b00 => Some("b"), + 0b01 => Some("h"), + _ => None, + }; + + // LDAPR is the one o3 form that takes no source register. + if o3 && opc == 0b100 { + if rs != 31 { + return None; + } + let mnem: &'static str = match narrow { + Some("b") => "ldaprb", + Some("h") => "ldaprh", + _ => "ldapr", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rt, sf))); + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: None, + disp: 0, + mode: AddrMode::Offset, + size: 1u64 << size, + })); + return Some(i); + } + + let base: &'static str = if o3 { + if opc != 0 { + return None; + } + "swp" + } else { + match opc { + 0b000 => "ldadd", + 0b001 => "ldclr", + 0b010 => "ldeor", + 0b011 => "ldset", + 0b100 => "ldsmax", + 0b101 => "ldsmin", + 0b110 => "ldumax", + _ => "ldumin", + } + }; + // Discarding the loaded value has an ST alias, which is how a compiler + // spells an atomic increment whose old value nobody wants. + let store_alias = !o3 && rt == 31 && !a; + let mnem = atomic_mnemonic(base, store_alias, a, rl, narrow)?; + + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rs, sf))); + if !store_alias { + i.push(Operand::Reg(r(rt, sf))); + } + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: None, + disp: 0, + mode: AddrMode::Offset, + size: 1u64 << size, + })); + Some(i) +} + +/// Assemble an atomic mnemonic from its parts. Every combination is a distinct +/// static string, so [`Insn`] stays `Copy` and allocation-free. +fn atomic_mnemonic( + base: &'static str, + store: bool, + acquire: bool, + release: bool, + narrow: Option<&'static str>, +) -> Option<&'static str> { + macro_rules! table { + ($($b:literal),* $(,)?) => { + match (base, store, acquire, release, narrow) { + $( + ($b, false, false, false, None) => Some(concat!($b)), + ($b, false, false, true, None) => Some(concat!($b, "l")), + ($b, false, true, false, None) => Some(concat!($b, "a")), + ($b, false, true, true, None) => Some(concat!($b, "al")), + ($b, false, false, false, Some("b")) => Some(concat!($b, "b")), + ($b, false, false, true, Some("b")) => Some(concat!($b, "lb")), + ($b, false, true, false, Some("b")) => Some(concat!($b, "ab")), + ($b, false, true, true, Some("b")) => Some(concat!($b, "alb")), + ($b, false, false, false, Some("h")) => Some(concat!($b, "h")), + ($b, false, false, true, Some("h")) => Some(concat!($b, "lh")), + ($b, false, true, false, Some("h")) => Some(concat!($b, "ah")), + ($b, false, true, true, Some("h")) => Some(concat!($b, "alh")), + )* + _ => None, + } + }; + } + if store { + // st[l][b|h]: the alias drops the leading `ld`. + let short = base.strip_prefix("ld")?; + return match (short, release, narrow) { + ("add", false, None) => Some("stadd"), + ("add", true, None) => Some("staddl"), + ("add", false, Some("b")) => Some("staddb"), + ("add", true, Some("b")) => Some("staddlb"), + ("add", false, Some("h")) => Some("staddh"), + ("add", true, Some("h")) => Some("staddlh"), + ("clr", false, None) => Some("stclr"), + ("clr", true, None) => Some("stclrl"), + ("eor", false, None) => Some("steor"), + ("eor", true, None) => Some("steorl"), + ("set", false, None) => Some("stset"), + ("set", true, None) => Some("stsetl"), + ("smax", false, None) => Some("stsmax"), + ("smax", true, None) => Some("stsmaxl"), + ("smin", false, None) => Some("stsmin"), + ("smin", true, None) => Some("stsminl"), + ("umax", false, None) => Some("stumax"), + ("umax", true, None) => Some("stumaxl"), + ("umin", false, None) => Some("stumin"), + ("umin", true, None) => Some("stuminl"), + _ => None, + }; + } + table!( + "ldadd", "ldclr", "ldeor", "ldset", "ldsmax", "ldsmin", "ldumax", "ldumin", "swp" + ) +} + +fn load_store_exclusive(w: u32, addr: Addr) -> Option { + // o2 and o1 together mean compare-and-swap rather than exclusive access. + if bit(w, 23) == 1 && bit(w, 21) == 1 { + return compare_and_swap(w, addr); + } + let size = bits(w, 31, 30); + let load = bit(w, 22) == 1; + let pair = bit(w, 21) == 1; + let o0 = bit(w, 15); + let rt = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rs = bits(w, 20, 16); + let rt2 = bits(w, 14, 10); + let sf = size == 0b11; + let xfer = 1u64 << size; + + let suffix = match size { + 0b00 => "b", + 0b01 => "h", + _ => "", + }; + // Ordered accesses with no status register: LDAR/STLR, or the limited + // ordering LDLAR/STLLR when o0 is clear. + if !pair && bit(w, 23) == 1 { + let mnem: &'static str = match (load, o0, suffix) { + (true, 1, "b") => "ldarb", + (true, 1, "h") => "ldarh", + (true, 1, _) => "ldar", + (false, 1, "b") => "stlrb", + (false, 1, "h") => "stlrh", + (false, 1, _) => "stlr", + (true, _, "b") => "ldlarb", + (true, _, "h") => "ldlarh", + (true, _, _) => "ldlar", + (false, _, "b") => "stllrb", + (false, _, "h") => "stllrh", + (false, _, _) => "stllr", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rt, sf))); + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: None, + disp: 0, + mode: AddrMode::Offset, + size: xfer, + })); + return Some(i); + } + + let mnem: &'static str = match (load, pair, o0, suffix) { + (true, false, 0, "b") => "ldxrb", + (true, false, 0, "h") => "ldxrh", + (true, false, 0, _) => "ldxr", + (true, false, 1, "b") => "ldaxrb", + (true, false, 1, "h") => "ldaxrh", + (true, false, 1, _) => "ldaxr", + (false, false, 0, "b") => "stxrb", + (false, false, 0, "h") => "stxrh", + (false, false, 0, _) => "stxr", + (false, false, 1, "b") => "stlxrb", + (false, false, 1, "h") => "stlxrh", + (false, false, 1, _) => "stlxr", + (true, true, 0, _) => "ldxp", + (true, true, 1, _) => "ldaxp", + (false, true, 0, _) => "stxp", + (false, true, 1, _) => "stlxp", + _ => return None, + }; + let mut i = ins(addr, mnem, Flow::Next); + if !load { + i.push(Operand::Reg(r(rs, false))); + } + i.push(Operand::Reg(r(rt, sf))); + if pair { + i.push(Operand::Reg(r(rt2, sf))); + } + i.push(Operand::Mem(Mem { + seg: None, + base: Some(rsp(rn, true)), + index: None, + disp: 0, + mode: AddrMode::Offset, + size: if pair { xfer * 2 } else { xfer }, + })); + Some(i) +} + +// ------------------------------------------------------- register arithmetic + +fn dp_register(w: u32, addr: Addr) -> Option { + let op0 = bit(w, 30); + let op1 = bit(w, 28); + let op2 = bits(w, 24, 21); + + if op1 == 1 { + return match op2 { + 0b0000 if bits(w, 15, 10) == 0 => add_sub_carry(w, addr), + 0b0010 if bit(w, 11) == 0 => cond_compare(w, addr, false), + 0b0010 if bit(w, 11) == 1 => cond_compare(w, addr, true), + 0b0100 => cond_select(w, addr), + 0b0110 => { + if op0 == 1 { + dp_1source(w, addr) + } else { + dp_2source(w, addr) + } + } + 0b1000..=0b1111 => dp_3source(w, addr), + _ => None, + }; + } + // Logical and arithmetic, shifted or extended register. + if bits(w, 28, 24) == 0b01010 { + return logical_shifted(w, addr); + } + if bits(w, 28, 24) == 0b01011 { + if bit(w, 21) == 1 { + return add_sub_extended(w, addr); + } + return add_sub_shifted(w, addr); + } + None +} + +fn shift_kind(v: u32) -> Option { + Some(match v { + 0b00 => Shift::Lsl, + 0b01 => Shift::Lsr, + 0b10 => Shift::Asr, + 0b11 => Shift::Ror, + _ => return None, + }) +} + +fn logical_shifted(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let opc = bits(w, 30, 29); + let n = bit(w, 21); + let shift = shift_kind(bits(w, 23, 22))?; + let amount = bits(w, 15, 10); + if !sf && amount >= 32 { + return None; + } + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + + // ORR from zr with no shift is MOV; ANDS to zr is TST; ORN from zr is MVN. + if opc == 0b01 && n == 0 && rn == 31 && amount == 0 && shift == Shift::Lsl { + let mut i = ins(addr, "mov", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rm, sf))); + return Some(i); + } + if opc == 0b01 && n == 1 && rn == 31 { + let mut i = ins(addr, "mvn", Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + push_shifted(&mut i, r(rm, sf), shift, amount); + return Some(i); + } + if opc == 0b11 && n == 0 && rd == 31 { + let mut i = ins(addr, "tst", Flow::Next); + i.push(Operand::Reg(r(rn, sf))); + push_shifted(&mut i, r(rm, sf), shift, amount); + return Some(i); + } + + let mnem = match (opc, n) { + (0b00, 0) => "and", + (0b00, 1) => "bic", + (0b01, 0) => "orr", + (0b01, 1) => "orn", + (0b10, 0) => "eor", + (0b10, 1) => "eon", + (0b11, 0) => "ands", + _ => "bics", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + push_shifted(&mut i, r(rm, sf), shift, amount); + Some(i) +} + +fn push_shifted(i: &mut Insn, reg: Reg, shift: Shift, amount: u32) { + if amount == 0 && shift == Shift::Lsl { + i.push(Operand::Reg(reg)); + } else { + i.push(Operand::Shifted(reg, shift, amount as u8)); + } +} + +fn add_sub_shifted(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let sub = bit(w, 30) == 1; + let setflags = bit(w, 29) == 1; + let shift = shift_kind(bits(w, 23, 22))?; + if shift == Shift::Ror { + return None; + } + let amount = bits(w, 15, 10); + if !sf && amount >= 32 { + return None; + } + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + + if setflags && rd == 31 { + let mut i = ins(addr, if sub { "cmp" } else { "cmn" }, Flow::Next); + i.push(Operand::Reg(r(rn, sf))); + push_shifted(&mut i, r(rm, sf), shift, amount); + return Some(i); + } + if sub && rn == 31 { + let mut i = ins(addr, if setflags { "negs" } else { "neg" }, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + push_shifted(&mut i, r(rm, sf), shift, amount); + return Some(i); + } + + let mnem = match (sub, setflags) { + (false, false) => "add", + (false, true) => "adds", + (true, false) => "sub", + (true, true) => "subs", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + push_shifted(&mut i, r(rm, sf), shift, amount); + Some(i) +} + +fn add_sub_extended(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let sub = bit(w, 30) == 1; + let setflags = bit(w, 29) == 1; + if bits(w, 23, 22) != 0 { + return None; + } + let option = bits(w, 15, 13); + let amount = bits(w, 12, 10); + if amount > 4 { + return None; + } + let extend = match option { + 0 => Extend::Uxtb, + 1 => Extend::Uxth, + 2 => Extend::Uxtw, + 3 => Extend::Uxtx, + 4 => Extend::Sxtb, + 5 => Extend::Sxth, + 6 => Extend::Sxtw, + _ => Extend::Sxtx, + }; + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + // The operand register is 64-bit only for the x-extending options. + // objdump keeps the operand at the instruction's own width; only the + // 64-bit variant names an X register for the x-extending options. + let rm_wide = sf && matches!(extend, Extend::Uxtx | Extend::Sxtx); + // UXTX (64-bit) and UXTW (32-bit) print as LSL, but only when the + // instruction actually involves SP; otherwise the extend is spelled out. + let sp_involved = rd == 31 || rn == 31; + let prints_lsl = + sp_involved && ((sf && extend == Extend::Uxtx) || (!sf && extend == Extend::Uxtw)); + let shown = if prints_lsl { Extend::Lsl } else { extend }; + + if setflags && rd == 31 { + let mut i = ins(addr, if sub { "cmp" } else { "cmn" }, Flow::Next); + i.push(Operand::Reg(rsp(rn, sf))); + push_extended(&mut i, r(rm, rm_wide), shown, amount, prints_lsl); + return Some(i); + } + + let mnem = match (sub, setflags) { + (false, false) => "add", + (false, true) => "adds", + (true, false) => "sub", + (true, true) => "subs", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(if setflags { r(rd, sf) } else { rsp(rd, sf) })); + i.push(Operand::Reg(rsp(rn, sf))); + push_extended(&mut i, r(rm, rm_wide), shown, amount, prints_lsl); + Some(i) +} + +fn push_extended(i: &mut Insn, reg: Reg, extend: Extend, amount: u32, prints_lsl: bool) { + if amount == 0 && prints_lsl { + i.push(Operand::Reg(reg)); + } else { + i.push(Operand::Extended(reg, extend, amount as u8)); + } +} + +fn add_sub_carry(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let sub = bit(w, 30) == 1; + let setflags = bit(w, 29) == 1; + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + if sub && rn == 31 { + let mut i = ins(addr, if setflags { "ngcs" } else { "ngc" }, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rm, sf))); + return Some(i); + } + let mnem = match (sub, setflags) { + (false, false) => "adc", + (false, true) => "adcs", + (true, false) => "sbc", + (true, true) => "sbcs", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Reg(r(rm, sf))); + Some(i) +} + +fn cond_compare(w: u32, addr: Addr, imm_form: bool) -> Option { + let sf = bit(w, 31) == 1; + let sub = bit(w, 30) == 1; + if bit(w, 29) != 1 || bit(w, 4) != 0 || bit(w, 10) != 0 { + return None; + } + let mut i = ins(addr, if sub { "ccmp" } else { "ccmn" }, Flow::Next); + i.push(Operand::Reg(r(bits(w, 9, 5), sf))); + if imm_form { + i.push(Operand::Imm(bits(w, 20, 16) as i64)); + } else { + i.push(Operand::Reg(r(bits(w, 20, 16), sf))); + } + i.push(Operand::Imm(bits(w, 3, 0) as i64)); + i.push(Operand::Cond(Cond(bits(w, 15, 12) as u8))); + Some(i) +} + +fn cond_select(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + let op = bit(w, 30); + let o2 = bit(w, 10); + if bit(w, 29) != 0 { + return None; + } + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + let cond = bits(w, 15, 12); + + // CSINC/CSINV/CSNEG with both sources zr and an invertible condition are + // the cset family, which is how a compiler materializes a boolean. + let invertible = cond != 0b1110 && cond != 0b1111; + if rn == 31 && rm == 31 && invertible { + let mnem = match (op, o2) { + (0, 1) => Some("cset"), + (1, 0) => Some("csetm"), + _ => None, + }; + if let Some(m) = mnem { + let mut i = ins(addr, m, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Cond(Cond((cond ^ 1) as u8))); + return Some(i); + } + } + if rn == rm && rn != 31 && invertible { + let mnem = match (op, o2) { + (0, 1) => Some("cinc"), + (1, 0) => Some("cinv"), + (1, 1) => Some("cneg"), + _ => None, + }; + if let Some(m) = mnem { + let mut i = ins(addr, m, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Cond(Cond((cond ^ 1) as u8))); + return Some(i); + } + } + + let mnem = match (op, o2) { + (0, 0) => "csel", + (0, 1) => "csinc", + (1, 0) => "csinv", + _ => "csneg", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Reg(r(rm, sf))); + i.push(Operand::Cond(Cond(cond as u8))); + Some(i) +} + +fn dp_1source(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + if bit(w, 29) != 0 { + return None; + } + let opcode2 = bits(w, 20, 16); + let opcode = bits(w, 15, 10); + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + if opcode2 != 0 { + return None; + } + let mnem = match opcode { + 0b000000 => "rbit", + 0b000001 => "rev16", + 0b000010 => { + if sf { + "rev32" + } else { + "rev" + } + } + 0b000011 if sf => "rev", + 0b000100 => "clz", + 0b000101 => "cls", + _ => return None, + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + Some(i) +} + +fn dp_2source(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + if bit(w, 29) != 0 { + return None; + } + let opcode = bits(w, 15, 10); + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + let mnem = match opcode { + 0b000010 => "udiv", + 0b000011 => "sdiv", + 0b001000 => "lslv", + 0b001001 => "lsrv", + 0b001010 => "asrv", + 0b001011 => "rorv", + 0b010000 => "crc32b", + 0b010001 => "crc32h", + 0b010010 => "crc32w", + 0b010011 => "crc32x", + 0b010100 => "crc32cb", + 0b010101 => "crc32ch", + 0b010110 => "crc32cw", + 0b010111 => "crc32cx", + _ => return None, + }; + // objdump prints the variable shifts without the trailing v. + let shown: &'static str = match mnem { + "lslv" => "lsl", + "lsrv" => "lsr", + "asrv" => "asr", + "rorv" => "ror", + other => other, + }; + let mut i = ins(addr, shown, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, sf))); + i.push(Operand::Reg(r(rm, sf))); + Some(i) +} + +fn dp_3source(w: u32, addr: Addr) -> Option { + let sf = bit(w, 31) == 1; + if bits(w, 30, 29) != 0 { + return None; + } + let op31 = bits(w, 23, 21); + let o0 = bit(w, 15); + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + let ra = bits(w, 14, 10); + + let (mnem, wide_operands) = match (op31, o0, sf) { + (0b000, 0, _) => ("madd", sf), + (0b000, 1, _) => ("msub", sf), + (0b001, 0, true) => ("smaddl", false), + (0b001, 1, true) => ("smsubl", false), + (0b010, 0, true) => ("smulh", true), + (0b101, 0, true) => ("umaddl", false), + (0b101, 1, true) => ("umsubl", false), + (0b110, 0, true) => ("umulh", true), + _ => return None, + }; + + // With a zero accumulator these are the multiply aliases. + if ra == 31 { + let alias: Option<&'static str> = match mnem { + "madd" => Some("mul"), + "msub" => Some("mneg"), + "smaddl" => Some("smull"), + "smsubl" => Some("smnegl"), + "umaddl" => Some("umull"), + "umsubl" => Some("umnegl"), + _ => None, + }; + if let Some(a) = alias { + let mut i = ins(addr, a, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, wide_operands))); + i.push(Operand::Reg(r(rm, wide_operands))); + return Some(i); + } + } + + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(r(rn, wide_operands))); + i.push(Operand::Reg(r(rm, wide_operands))); + if mnem != "smulh" && mnem != "umulh" { + i.push(Operand::Reg(r(ra, sf))); + } + Some(i) +} + +// -------------------------------------------------------------- FP and SIMD + +/// Expand an 8-bit FP immediate to a double, per `VFPExpandImm`. +/// +/// The encoding is `sign : NOT(b6) : Replicate(b6, 8) : b5 b4 : frac`, which +/// covers the small set of constants a compiler materializes inline. Every +/// width expands to the same real value, so one double is enough. +pub(crate) fn vfp_expand_imm(imm8: u8) -> u64 { + let sign = (imm8 >> 7) as u64 & 1; + let b6 = (imm8 >> 6) as u64 & 1; + let b54 = (imm8 >> 4) as u64 & 3; + let frac = imm8 as u64 & 0xf; + // 11-bit exponent: one inverted bit, eight copies, then two literal bits. + let exp = ((1 - b6) << 10) | (if b6 == 1 { 0xffu64 } else { 0 } << 2) | b54; + (sign << 63) | (exp << 52) | (frac << 48) +} + +/// A deliberately shallow slice of the FP and SIMD space. +/// +/// Enough to keep a listing readable and to keep the sweep aligned: scalar +/// moves, the FP arithmetic a compiler emits for `double`, and the compares. +/// Anything else returns `None` and prints as an unknown word, which is honest. +fn dp_simd(w: u32, addr: Addr) -> Option { + // Floating point data processing, three source: the fused multiply-adds. + if bits(w, 31, 24) == 0b0001_1111 { + let width = match bits(w, 23, 22) { + 0b00 => Width::W32, + 0b01 => Width::W64, + 0b11 => Width::W16, + _ => return None, + }; + let mnem = match (bit(w, 21), bit(w, 15)) { + (0, 0) => "fmadd", + (0, 1) => "fmsub", + (1, 0) => "fnmadd", + _ => "fnmsub", + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(v(bits(w, 4, 0), width))); + i.push(Operand::Reg(v(bits(w, 9, 5), width))); + i.push(Operand::Reg(v(bits(w, 20, 16), width))); + i.push(Operand::Reg(v(bits(w, 14, 10), width))); + return Some(i); + } + // Floating point data processing, one and two source. + if bits(w, 31, 24) == 0b0001_1110 && bit(w, 21) == 1 { + let ftype = bits(w, 23, 22); + let width = match ftype { + 0b00 => Width::W32, + 0b01 => Width::W64, + 0b11 => Width::W16, + _ => return None, + }; + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + + // Two source. + if bits(w, 11, 10) == 0b10 { + let mnem = match bits(w, 15, 12) { + 0b0000 => "fmul", + 0b0001 => "fdiv", + 0b0010 => "fadd", + 0b0011 => "fsub", + 0b0100 => "fmax", + 0b0101 => "fmin", + 0b0110 => "fmaxnm", + 0b0111 => "fminnm", + 0b1000 => "fnmul", + _ => return None, + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(v(rd, width))); + i.push(Operand::Reg(v(rn, width))); + i.push(Operand::Reg(v(rm, width))); + return Some(i); + } + // Floating point immediate, which must be tried before the one-source + // group because both have bit 12 clear. + if bits(w, 12, 10) == 0b100 && bits(w, 9, 5) == 0 { + let mut i = ins(addr, "fmov", Flow::Next); + i.push(Operand::Reg(v(rd, width))); + i.push(Operand::FpImm(vfp_expand_imm(bits(w, 20, 13) as u8))); + return Some(i); + } + // Precision conversion, whose destination width differs from its + // source width and so cannot share the one-source path. + if bits(w, 14, 10) == 0b10000 && bits(w, 20, 17) == 0b0001 { + let to = match bits(w, 16, 15) { + 0b00 => Width::W32, + 0b01 => Width::W64, + 0b11 => Width::W16, + _ => return None, + }; + if to == width { + return None; + } + let mut i = ins(addr, "fcvt", Flow::Next); + i.push(Operand::Reg(v(rd, to))); + i.push(Operand::Reg(v(rn, width))); + return Some(i); + } + // One source. + if bits(w, 14, 10) == 0b10000 { + let mnem = match bits(w, 20, 15) { + 0b000000 => "fmov", + 0b000001 => "fabs", + 0b000010 => "fneg", + 0b000011 => "fsqrt", + 0b001000 => "frintn", + 0b001001 => "frintp", + 0b001010 => "frintm", + 0b001011 => "frintz", + 0b001100 => "frinta", + 0b001110 => "frintx", + 0b001111 => "frinti", + _ => return None, + }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(v(rd, width))); + i.push(Operand::Reg(v(rn, width))); + return Some(i); + } + // Compare. + if bits(w, 13, 10) == 0b1000 { + let with_zero = bit(w, 3) == 1; + let mnem = if bit(w, 4) == 1 { "fcmpe" } else { "fcmp" }; + let mut i = ins(addr, mnem, Flow::Next); + i.push(Operand::Reg(v(rn, width))); + if with_zero { + i.push(Operand::Name("#0.0")); + } else { + i.push(Operand::Reg(v(rm, width))); + } + return Some(i); + } + // Conditional select. + if bits(w, 11, 10) == 0b11 { + let mut i = ins(addr, "fcsel", Flow::Next); + i.push(Operand::Reg(v(rd, width))); + i.push(Operand::Reg(v(rn, width))); + i.push(Operand::Reg(v(rm, width))); + i.push(Operand::Cond(Cond(bits(w, 15, 12) as u8))); + return Some(i); + } + } + + // Advanced SIMD, which is most of this space. + if let Some(i) = simd::decode(w, addr) { + return Some(i); + } + + // Conversion between floating point and integer. + if bits(w, 30, 24) == 0b0011110 && bit(w, 21) == 1 && bits(w, 11, 10) == 0 { + let sf = bit(w, 31) == 1; + let ftype = bits(w, 23, 22); + let fwidth = match ftype { + 0b00 => Width::W32, + 0b01 => Width::W64, + 0b11 => Width::W16, + _ => return None, + }; + let rmode = bits(w, 20, 19); + let opcode = bits(w, 18, 16); + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let (mnem, to_int) = match (rmode, opcode) { + (0b00, 0b010) => ("scvtf", false), + (0b00, 0b011) => ("ucvtf", false), + (0b00, 0b000) => ("fcvtns", true), + (0b00, 0b001) => ("fcvtnu", true), + (0b01, 0b000) => ("fcvtps", true), + (0b01, 0b001) => ("fcvtpu", true), + (0b10, 0b000) => ("fcvtms", true), + (0b10, 0b001) => ("fcvtmu", true), + (0b11, 0b000) => ("fcvtzs", true), + (0b11, 0b001) => ("fcvtzu", true), + (0b00, 0b110) => ("fmov", true), + (0b00, 0b111) => ("fmov", false), + _ => return None, + }; + let mut i = ins(addr, mnem, Flow::Next); + if to_int { + i.push(Operand::Reg(r(rd, sf))); + i.push(Operand::Reg(v(rn, fwidth))); + } else { + i.push(Operand::Reg(v(rd, fwidth))); + i.push(Operand::Reg(r(rn, sf))); + } + return Some(i); + } + None +} diff --git a/crates/e5r-arch/src/aarch64/simd.rs b/crates/e5r-arch/src/aarch64/simd.rs new file mode 100644 index 0000000..387099a --- /dev/null +++ b/crates/e5r-arch/src/aarch64/simd.rs @@ -0,0 +1,784 @@ +//! Advanced SIMD. +//! +//! The groups a compiler and a hand-written library actually emit: the +//! three-operand arithmetic, the immediate forms, the shifts, the lane moves, +//! the table lookups, the structure loads and stores, and the AES instructions. +//! Scalable Vector Extension encodings are not here; they decode as nothing, +//! which the coverage number records honestly. + +use e5r_core::Addr; + +use crate::insn::{AddrMode, Flow, Insn, Lanes, Mem, Operand, Reg, Width}; + +use super::{bit, bits}; + +/// The lane arrangement for a `size` field and a `Q` bit. +fn lanes(size: u32, q: u32) -> Option { + Some(match (size, q) { + (0b00, 0) => Lanes::B8, + (0b00, 1) => Lanes::B16, + (0b01, 0) => Lanes::H4, + (0b01, 1) => Lanes::H8, + (0b10, 0) => Lanes::S2, + (0b10, 1) => Lanes::S4, + (0b11, 0) => Lanes::D1, + (0b11, 1) => Lanes::D2, + _ => return None, + }) +} + +/// The wider arrangement a lengthening instruction writes. +fn wide_lanes(size: u32) -> Option { + Some(match size { + 0b00 => Lanes::H8, + 0b01 => Lanes::S4, + 0b10 => Lanes::D2, + _ => return None, + }) +} + +fn v(num: u32, l: Lanes) -> Operand { + Operand::Vector(num as u8, l) +} + +fn ins(addr: Addr, m: &'static str) -> Insn { + Insn::new(addr, 4, m, Flow::Next) +} + +/// Decode an Advanced SIMD instruction, or `None`. +pub fn decode(w: u32, addr: Addr) -> Option { + // Crypto AES: one source, one destination, byte lanes only. + if w & 0xFF3E_0C00 == 0x4E28_0800 { + let mnem = match bits(w, 16, 12) { + 0b00100 => "aese", + 0b00101 => "aesd", + 0b00110 => "aesmc", + 0b00111 => "aesimc", + _ => return None, + }; + let mut i = ins(addr, mnem); + i.push(v(bits(w, 4, 0), Lanes::B16)); + i.push(v(bits(w, 9, 5), Lanes::B16)); + return Some(i); + } + + // Table lookup, and the permutes that share its shape. + if w & 0xBF20_8C00 == 0x0E00_0000 { + return table_lookup(w, addr); + } + if w & 0xBF20_8C00 == 0x0E00_0800 { + return permute(w, addr); + } + // Extract: a byte-granular concatenation of two registers. + if w & 0xBF20_8400 == 0x2E00_0000 { + let q = bit(w, 30); + let l = if q == 1 { Lanes::B16 } else { Lanes::B8 }; + let mut i = ins(addr, "ext"); + i.push(v(bits(w, 4, 0), l)); + i.push(v(bits(w, 9, 5), l)); + i.push(v(bits(w, 20, 16), l)); + i.push(Operand::Count(bits(w, 14, 11) as i64)); + return Some(i); + } + // Copy: dup, ins, umov, smov. + if w & 0x9FE0_8400 == 0x0E00_0400 { + return copy(w, addr); + } + // Modified immediate, which is also where the vector fmov lives. + if w & 0x9FF8_0400 == 0x0F00_0400 { + return modified_immediate(w, addr); + } + // Shift by immediate, distinguished from the above by a non-zero immh. + if w & 0x9F80_0400 == 0x0F00_0400 && bits(w, 22, 19) != 0 { + return shift_immediate(w, addr); + } + // Two-register miscellaneous. + if w & 0x9F3E_0C00 == 0x0E20_0800 { + return two_reg_misc(w, addr); + } + // Scalar two-register miscellaneous: the conversions that read and write + // a vector register rather than a general one. + if w & 0xDF3E_0C00 == 0x5E20_0800 { + let u = bit(w, 29); + let opcode = bits(w, 16, 12); + let mnem = match (opcode, u) { + (0b11101, 0) => "scvtf", + (0b11101, 1) => "ucvtf", + (0b11011, 0) => "fcvtzs", + (0b11011, 1) => "fcvtzu", + (0b11010, 0) => "fcvtns", + (0b11010, 1) => "fcvtnu", + (0b11100, 0) => "fcvtas", + (0b11100, 1) => "fcvtau", + _ => return None, + }; + let width = if bit(w, 22) == 1 { + Width::W64 + } else { + Width::W32 + }; + let mut i = ins(addr, mnem); + i.push(Operand::Reg(Reg::vec(bits(w, 4, 0) as u8, width))); + i.push(Operand::Reg(Reg::vec(bits(w, 9, 5) as u8, width))); + return Some(i); + } + // Scalar pairwise: one register folded to a scalar. + if w & 0xDF3E_0C00 == 0x5E30_0800 { + return scalar_pairwise(w, addr); + } + // Across lanes. + if w & 0x9F3E_0C00 == 0x0E30_0800 { + return across_lanes(w, addr); + } + // Three different: the lengthening and widening forms. + if w & 0x9F20_0C00 == 0x0E20_0000 { + return three_different(w, addr); + } + // Three same. + if w & 0x9F20_0400 == 0x0E20_0400 { + return three_same(w, addr); + } + // Load and store of multiple structures. + if w & 0xBFBF_0000 == 0x0C00_0000 || w & 0xBFA0_0000 == 0x0C80_0000 { + return load_store_structures(w, addr); + } + None +} + +fn table_lookup(w: u32, addr: Addr) -> Option { + if bits(w, 23, 22) != 0 { + return None; + } + let q = bit(w, 30); + let l = if q == 1 { Lanes::B16 } else { Lanes::B8 }; + let len = bits(w, 14, 13) + 1; + let mut i = ins(addr, if bit(w, 12) == 1 { "tbx" } else { "tbl" }); + i.push(v(bits(w, 4, 0), l)); + i.push(Operand::VectorList( + bits(w, 9, 5) as u8, + len as u8, + Lanes::B16, + )); + i.push(v(bits(w, 20, 16), l)); + Some(i) +} + +fn permute(w: u32, addr: Addr) -> Option { + let l = lanes(bits(w, 23, 22), bit(w, 30))?; + let mnem = match bits(w, 14, 12) { + 0b001 => "uzp1", + 0b010 => "trn1", + 0b011 => "zip1", + 0b101 => "uzp2", + 0b110 => "trn2", + 0b111 => "zip2", + _ => return None, + }; + let mut i = ins(addr, mnem); + i.push(v(bits(w, 4, 0), l)); + i.push(v(bits(w, 9, 5), l)); + i.push(v(bits(w, 20, 16), l)); + Some(i) +} + +/// The element width and index an `imm5` field selects. +fn elem(imm5: u32) -> Option<(Width, u8)> { + if imm5 & 1 != 0 { + Some((Width::W8, (imm5 >> 1) as u8)) + } else if imm5 & 2 != 0 { + Some((Width::W16, (imm5 >> 2) as u8)) + } else if imm5 & 4 != 0 { + Some((Width::W32, (imm5 >> 3) as u8)) + } else if imm5 & 8 != 0 { + Some((Width::W64, (imm5 >> 4) as u8)) + } else { + None + } +} + +fn copy(w: u32, addr: Addr) -> Option { + let q = bit(w, 30); + let op = bit(w, 29); + let imm5 = bits(w, 20, 16); + let imm4 = bits(w, 14, 11); + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let (width, index) = elem(imm5)?; + let size = match width { + Width::W8 => 0, + Width::W16 => 1, + Width::W32 => 2, + _ => 3, + }; + + // op set means an insert from one lane to another. + if op == 1 { + if q == 0 { + return None; + } + let src = elem(imm5).map(|(wd, _)| wd)?; + let shift = match src { + Width::W8 => 0, + Width::W16 => 1, + Width::W32 => 2, + _ => 3, + }; + let mut i = ins(addr, "mov"); + i.push(Operand::VectorLane(rd as u8, width, index)); + i.push(Operand::VectorLane(rn as u8, width, (imm4 >> shift) as u8)); + return Some(i); + } + + match imm4 { + // dup from a vector element. + 0b0000 => { + let l = lanes(size, q)?; + let mut i = ins(addr, "dup"); + i.push(v(rd, l)); + i.push(Operand::VectorLane(rn as u8, width, index)); + Some(i) + } + // dup from a general register. + 0b0001 => { + let l = lanes(size, q)?; + let mut i = ins(addr, "dup"); + i.push(v(rd, l)); + i.push(Operand::Reg(super::gpr_for(rn, size == 3))); + Some(i) + } + // smov and umov. + 0b0101 | 0b0111 => { + let signed = imm4 == 0b0101; + let wide = q == 1; + // umov of a full lane is spelled mov. + let mnem = if signed { + "smov" + } else if (wide && size == 3) || (!wide && size == 2) { + "mov" + } else { + "umov" + }; + let mut i = ins(addr, mnem); + i.push(Operand::Reg(super::gpr_for(rd, wide))); + i.push(Operand::VectorLane(rn as u8, width, index)); + Some(i) + } + // ins from a general register. + 0b0011 => { + let mut i = ins(addr, "mov"); + i.push(Operand::VectorLane(rd as u8, width, index)); + i.push(Operand::Reg(super::gpr_for(rn, size == 3))); + Some(i) + } + _ => None, + } +} + +/// Expand the `abcdefgh` immediate a `movi` carries, per `AdvSIMDExpandImm`. +fn advsimd_expand(cmode: u32, op: u32, imm8: u64) -> u64 { + let replicate32 = |v: u64| v | (v << 32); + match cmode >> 1 { + 0b000 => replicate32(imm8), + 0b001 => replicate32(imm8 << 8), + 0b010 => replicate32(imm8 << 16), + 0b011 => replicate32(imm8 << 24), + 0b100 => { + let h = imm8 | (imm8 << 16); + h | (h << 32) + } + 0b101 => { + let h = (imm8 << 8) | (imm8 << 24); + h | (h << 32) + } + 0b110 => { + let v = if cmode & 1 == 0 { + (imm8 << 8) | 0xff + } else { + (imm8 << 16) | 0xffff + }; + replicate32(v) + } + // cmode 1110 with op set is the 64-bit form: each bit of the + // immediate becomes a whole byte. + _ if cmode & 1 == 0 && op == 1 => { + let mut out = 0u64; + for i in 0..8 { + if imm8 >> i & 1 != 0 { + out |= 0xffu64 << (i * 8); + } + } + out + } + _ => replicate32(imm8), + } +} + +fn modified_immediate(w: u32, addr: Addr) -> Option { + let q = bit(w, 30); + let op = bit(w, 29); + let cmode = bits(w, 15, 12); + let o2 = bit(w, 11); + let imm8 = ((bits(w, 18, 16) << 5) | bits(w, 9, 5)) as u64; + let rd = bits(w, 4, 0); + + // The floating point forms. + if o2 == 0 && cmode == 0b1111 { + let l = if op == 1 { + if q == 0 { + return None; + } + Lanes::D2 + } else { + lanes(0b10, q)? + }; + let mut i = ins(addr, "fmov"); + i.push(v(rd, l)); + i.push(Operand::FpImm(super::vfp_expand_imm(imm8 as u8))); + return Some(i); + } + if o2 != 0 { + return None; + } + + // cmode and op together name the instruction, the lane width, and the + // shift kind. This is the manual's table rather than a pattern. + let high = cmode >> 1; + let odd = cmode & 1 == 1; + let (mnem, size, shift, msl) = match (high, odd, op) { + (0b000..=0b011, false, 0) => ("movi", 0b10, (high as u8) * 8, false), + (0b000..=0b011, true, 0) => ("orr", 0b10, (high as u8) * 8, false), + (0b000..=0b011, false, 1) => ("mvni", 0b10, (high as u8) * 8, false), + (0b000..=0b011, true, 1) => ("bic", 0b10, (high as u8) * 8, false), + (0b100 | 0b101, false, 0) => ("movi", 0b01, ((high as u8) - 4) * 8, false), + (0b100 | 0b101, true, 0) => ("orr", 0b01, ((high as u8) - 4) * 8, false), + (0b100 | 0b101, false, 1) => ("mvni", 0b01, ((high as u8) - 4) * 8, false), + (0b100 | 0b101, true, 1) => ("bic", 0b01, ((high as u8) - 4) * 8, false), + // Shifting ones: the shift is spelled `msl`. + (0b110, _, 0) => ("movi", 0b10, if odd { 16 } else { 8 }, true), + (0b110, _, 1) => ("mvni", 0b10, if odd { 16 } else { 8 }, true), + // Byte lanes, and the 64-bit pattern form. + (0b111, false, 0) => ("movi", 0b00, 0, false), + (0b111, false, 1) => { + // A 64-bit pattern per lane, printed on a scalar when Q is clear. + let value = advsimd_expand(cmode, op, imm8); + let mut i = ins(addr, "movi"); + if q == 1 { + i.push(v(rd, Lanes::D2)); + } else { + i.push(Operand::Reg(Reg::vec(rd as u8, Width::W64))); + } + i.push(Operand::UImm(value)); + return Some(i); + } + _ => return None, + }; + + let mut i = ins(addr, mnem); + i.push(v(rd, lanes(size, q)?)); + i.push(Operand::UImm(imm8)); + if shift != 0 { + i.push(Operand::ShiftOp( + if msl { + crate::insn::Shift::Msl + } else { + crate::insn::Shift::Lsl + }, + shift, + )); + } + Some(i) +} + +fn shift_immediate(w: u32, addr: Addr) -> Option { + let q = bit(w, 30); + let u = bit(w, 29); + let immh = bits(w, 22, 19); + let immb = bits(w, 18, 16); + let opcode = bits(w, 15, 11); + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + + // The element size is the position of the highest set bit of immh. + let size = match immh { + 0b0001 => 0, + 0b0010 | 0b0011 => 1, + 0b0100..=0b0111 => 2, + 0b1000..=0b1111 => 3, + _ => return None, + }; + if size == 3 && q == 0 { + return None; + } + let l = lanes(size, q)?; + let esize = 8u32 << size; + let imm = (immh << 3) | immb; + let right = esize * 2 - imm; + let left = imm - esize; + + let (mnem, amount, narrow) = match (opcode, u) { + (0b00000, 0) => ("sshr", right, false), + (0b00000, 1) => ("ushr", right, false), + (0b00010, 0) => ("ssra", right, false), + (0b00010, 1) => ("usra", right, false), + (0b00100, 0) => ("srshr", right, false), + (0b00100, 1) => ("urshr", right, false), + (0b00110, 0) => ("srsra", right, false), + (0b00110, 1) => ("ursra", right, false), + (0b01000, 1) => ("sri", right, false), + (0b01010, 0) => ("shl", left, false), + (0b01010, 1) => ("sli", left, false), + (0b10000, 0) => ("shrn", right, true), + (0b10000, 1) => ("sqshrun", right, true), + (0b10001, 0) => ("rshrn", right, true), + (0b10100, 0) => ("sshll", left, true), + (0b10100, 1) => ("ushll", left, true), + _ => return None, + }; + + // A lengthening shift of zero is spelled as an extend. + let mnem: &'static str = match (mnem, amount, q) { + ("sshll", 0, 0) => "sxtl", + ("sshll", 0, 1) => "sxtl2", + ("ushll", 0, 0) => "uxtl", + ("ushll", 0, 1) => "uxtl2", + (other, _, _) => other, + }; + let extend = matches!(mnem, "sxtl" | "sxtl2" | "uxtl" | "uxtl2"); + + let mut i = ins(addr, mnem); + if narrow { + // A narrowing shift writes half-width lanes and reads full ones; a + // lengthening one does the reverse. + let wide = wide_lanes(size)?; + if mnem.ends_with("shll") || extend { + i.push(v(rd, wide)); + i.push(v(rn, lanes(size, q)?)); + } else { + i.push(v(rd, lanes(size, q)?)); + i.push(v(rn, wide)); + } + } else { + i.push(v(rd, l)); + i.push(v(rn, l)); + } + if !extend { + i.push(Operand::Count(amount as i64)); + } + Some(i) +} + +fn two_reg_misc(w: u32, addr: Addr) -> Option { + let q = bit(w, 30); + let u = bit(w, 29); + let size = bits(w, 23, 22); + let opcode = bits(w, 16, 12); + let l = lanes(size, q)?; + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + + let (mnem, zero) = match (opcode, u) { + (0b00000, 0) => ("rev64", false), + (0b00000, 1) => ("rev32", false), + (0b00001, 0) => ("rev16", false), + (0b00100, 0) => ("cls", false), + (0b00100, 1) => ("clz", false), + (0b00101, 0) => ("cnt", false), + // objdump spells the bitwise NOT `mvn`, as the manual's alias does. + (0b00101, 1) => ("mvn", false), + (0b00111, 0) => ("sqabs", false), + (0b00111, 1) => ("sqneg", false), + (0b01000, 0) => ("cmgt", true), + (0b01000, 1) => ("cmge", true), + (0b01001, 0) => ("cmeq", true), + (0b01001, 1) => ("cmle", true), + (0b01010, 0) => ("cmlt", true), + (0b01011, 0) => ("abs", false), + (0b01011, 1) => ("neg", false), + (0b10010, 0) => ("xtn", false), + (0b10010, 1) => ("sqxtun", false), + (0b10100, 0) => ("sqxtn", false), + (0b10100, 1) => ("uqxtn", false), + _ => return None, + }; + + // A narrowing move writing the upper half of its destination is spelled + // with a trailing `2`. + let narrowing = matches!(mnem, "xtn" | "sqxtn" | "uqxtn" | "sqxtun"); + let mnem = if narrowing && q == 1 { + match mnem { + "xtn" => "xtn2", + "sqxtn" => "sqxtn2", + "uqxtn" => "uqxtn2", + _ => "sqxtun2", + } + } else { + mnem + }; + let mut i = ins(addr, mnem); + i.push(v(rd, l)); + // The narrowing moves read the next arrangement up. + if narrowing { + i.push(v(rn, wide_lanes(size)?)); + } else { + i.push(v(rn, l)); + } + if zero { + i.push(Operand::Count(0)); + } + Some(i) +} + +/// The scalar pairwise forms: two lanes of one register folded into a scalar. +fn scalar_pairwise(w: u32, addr: Addr) -> Option { + let u = bit(w, 29); + let size = bits(w, 23, 22); + let opcode = bits(w, 16, 12); + let (mnem, width, lanes) = match (opcode, u) { + (0b11011, 0) if size == 0b11 => ("addp", Width::W64, Lanes::D2), + (0b01101, 1) => { + let half = size & 1 == 0; + ( + "faddp", + if half { Width::W32 } else { Width::W64 }, + if half { Lanes::S2 } else { Lanes::D2 }, + ) + } + (0b01100, 1) => { + let half = size & 1 == 0; + ( + "fmaxnmp", + if half { Width::W32 } else { Width::W64 }, + if half { Lanes::S2 } else { Lanes::D2 }, + ) + } + (0b01111, 1) => { + let half = size & 1 == 0; + ( + "fmaxp", + if half { Width::W32 } else { Width::W64 }, + if half { Lanes::S2 } else { Lanes::D2 }, + ) + } + _ => return None, + }; + let mut i = ins(addr, mnem); + i.push(Operand::Reg(Reg::vec(bits(w, 4, 0) as u8, width))); + i.push(v(bits(w, 9, 5), lanes)); + Some(i) +} + +fn across_lanes(w: u32, addr: Addr) -> Option { + let q = bit(w, 30); + let u = bit(w, 29); + let size = bits(w, 23, 22); + let opcode = bits(w, 16, 12); + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let mnem = match (opcode, u) { + (0b00011, 0) => "saddlv", + (0b00011, 1) => "uaddlv", + (0b01010, 0) => "smaxv", + (0b01010, 1) => "umaxv", + (0b11010, 0) => "sminv", + (0b11010, 1) => "uminv", + (0b11011, 0) => "addv", + _ => return None, + }; + // The destination is a scalar of the lane width, except for the long adds, + // which accumulate into twice it. + let long = opcode == 0b00011; + let width = match (size, long) { + (0b00, false) => Width::W8, + (0b01, false) | (0b00, true) => Width::W16, + (_, false) | (0b01, true) => Width::W32, + _ => Width::W64, + }; + let mut i = ins(addr, mnem); + i.push(Operand::Reg(Reg::vec(rd as u8, width))); + i.push(v(rn, lanes(size, q)?)); + Some(i) +} + +fn three_different(w: u32, addr: Addr) -> Option { + let q = bit(w, 30); + let u = bit(w, 29); + let size = bits(w, 23, 22); + let opcode = bits(w, 15, 12); + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + + let mnem: &'static str = match (opcode, u, q) { + (0b0000, 0, 0) => "saddl", + (0b0000, 0, 1) => "saddl2", + (0b0000, 1, 0) => "uaddl", + (0b0000, 1, 1) => "uaddl2", + (0b0010, 0, 0) => "ssubl", + (0b0010, 0, 1) => "ssubl2", + (0b0010, 1, 0) => "usubl", + (0b0010, 1, 1) => "usubl2", + (0b1000, 0, 0) => "smlal", + (0b1000, 0, 1) => "smlal2", + (0b1000, 1, 0) => "umlal", + (0b1000, 1, 1) => "umlal2", + (0b1010, 0, 0) => "smlsl", + (0b1010, 0, 1) => "smlsl2", + (0b1010, 1, 0) => "umlsl", + (0b1010, 1, 1) => "umlsl2", + (0b1100, 0, 0) => "smull", + (0b1100, 0, 1) => "smull2", + (0b1100, 1, 0) => "umull", + (0b1100, 1, 1) => "umull2", + (0b1110, 0, 0) => "pmull", + (0b1110, 0, 1) => "pmull2", + _ => return None, + }; + let narrow = lanes(size, q)?; + let wide = wide_lanes(size)?; + let mut i = ins(addr, mnem); + i.push(v(rd, wide)); + i.push(v(rn, narrow)); + i.push(v(rm, narrow)); + Some(i) +} + +fn three_same(w: u32, addr: Addr) -> Option { + let q = bit(w, 30); + let u = bit(w, 29); + let size = bits(w, 23, 22); + let opcode = bits(w, 15, 11); + let rd = bits(w, 4, 0); + let rn = bits(w, 9, 5); + let rm = bits(w, 20, 16); + + // The logical group takes its arrangement from Q alone. + if opcode == 0b00011 { + let l = if q == 1 { Lanes::B16 } else { Lanes::B8 }; + let mnem = match (u, size) { + (0, 0b00) => "and", + (0, 0b01) => "bic", + (0, 0b10) => "orr", + (0, 0b11) => "orn", + (1, 0b00) => "eor", + (1, 0b01) => "bsl", + (1, 0b10) => "bit", + _ => "bif", + }; + // `orr` with the same source twice is how a vector move is spelled. + if mnem == "orr" && rn == rm { + let mut i = ins(addr, "mov"); + i.push(v(rd, l)); + i.push(v(rn, l)); + return Some(i); + } + let mut i = ins(addr, mnem); + i.push(v(rd, l)); + i.push(v(rn, l)); + i.push(v(rm, l)); + return Some(i); + } + + let mnem: &'static str = match (opcode, u) { + (0b00000, 0) => "shadd", + (0b00000, 1) => "uhadd", + (0b00001, 0) => "sqadd", + (0b00001, 1) => "uqadd", + (0b00010, 0) => "srhadd", + (0b00010, 1) => "urhadd", + (0b00100, 0) => "shsub", + (0b00100, 1) => "uhsub", + (0b00101, 0) => "sqsub", + (0b00101, 1) => "uqsub", + (0b00110, 0) => "cmgt", + (0b00110, 1) => "cmhi", + (0b00111, 0) => "cmge", + (0b00111, 1) => "cmhs", + (0b01000, 0) => "sshl", + (0b01000, 1) => "ushl", + (0b01001, 0) => "sqshl", + (0b01001, 1) => "uqshl", + (0b01010, 0) => "srshl", + (0b01010, 1) => "urshl", + (0b01011, 0) => "sqrshl", + (0b01011, 1) => "uqrshl", + (0b01100, 0) => "smax", + (0b01100, 1) => "umax", + (0b01101, 0) => "smin", + (0b01101, 1) => "umin", + (0b01110, 0) => "sabd", + (0b01110, 1) => "uabd", + (0b01111, 0) => "saba", + (0b01111, 1) => "uaba", + (0b10000, 0) => "add", + (0b10000, 1) => "sub", + (0b10001, 0) => "cmtst", + (0b10001, 1) => "cmeq", + (0b10010, 0) => "mla", + (0b10010, 1) => "mls", + (0b10011, 0) => "mul", + (0b10011, 1) => "pmul", + (0b10100, 0) => "smaxp", + (0b10100, 1) => "umaxp", + (0b10101, 0) => "sminp", + (0b10101, 1) => "uminp", + (0b10111, 0) => "addp", + _ => return None, + }; + let l = lanes(size, q)?; + let mut i = ins(addr, mnem); + i.push(v(rd, l)); + i.push(v(rn, l)); + i.push(v(rm, l)); + Some(i) +} + +/// LD1 through LD4 and ST1 through ST4, in their multiple-structure forms. +fn load_store_structures(w: u32, addr: Addr) -> Option { + let q = bit(w, 30); + let load = bit(w, 22) == 1; + let post = bit(w, 23) == 1; + let opcode = bits(w, 15, 12); + let size = bits(w, 11, 10); + let rn = bits(w, 9, 5); + let rt = bits(w, 4, 0); + let rm = bits(w, 20, 16); + + let (base, count) = match opcode { + 0b0000 => ("4", 4u32), + 0b0010 => ("1", 4), + 0b0100 => ("3", 3), + 0b0110 => ("1", 3), + 0b0111 => ("1", 1), + 0b1000 => ("2", 2), + 0b1010 => ("1", 2), + _ => return None, + }; + let mnem: &'static str = match (load, base) { + (true, "1") => "ld1", + (true, "2") => "ld2", + (true, "3") => "ld3", + (true, "4") => "ld4", + (false, "1") => "st1", + (false, "2") => "st2", + (false, "3") => "st3", + _ => "st4", + }; + let l = lanes(size, q)?; + let bytes = if q == 1 { 16 } else { 8 } * count as u64; + + let mut i = ins(addr, mnem); + i.push(Operand::VectorList(rt as u8, count as u8, l)); + i.push(Operand::Mem(Mem { + seg: None, + base: Some(super::rsp_for(rn)), + index: (post && rm != 31).then(|| (super::gpr_for(rm, true), crate::insn::Extend::Lsl, 0)), + disp: if post && rm == 31 { bytes as i64 } else { 0 }, + mode: if post { + AddrMode::PostIndex + } else { + AddrMode::Offset + }, + size: bytes, + })); + Some(i) +} diff --git a/crates/e5r-arch/src/aarch64/sysreg.rs b/crates/e5r-arch/src/aarch64/sysreg.rs new file mode 100644 index 0000000..da178f3 --- /dev/null +++ b/crates/e5r-arch/src/aarch64/sysreg.rs @@ -0,0 +1,1136 @@ +//! System register names, keyed by the 16-bit `op0:op1:CRn:CRm:op2` field. +//! +//! Sorted by encoding so the lookup is a binary search. The table was taken +//! from what the assembler accepts and the disassembler prints, over every one +//! of the 32768 encodings an `MRS` can name, and `sysreg_names_match_binutils` +//! checks it against that same authority whenever the tools are installed. + +/// Every encoding binutils gives a name, sorted by encoding. +pub const SYSREGS: &[(u16, &str)] = &[ + (0x8002, "osdtrrx_el1"), + (0x8004, "dbgbvr0_el1"), + (0x8005, "dbgbcr0_el1"), + (0x8006, "dbgwvr0_el1"), + (0x8007, "dbgwcr0_el1"), + (0x800c, "dbgbvr1_el1"), + (0x800d, "dbgbcr1_el1"), + (0x800e, "dbgwvr1_el1"), + (0x800f, "dbgwcr1_el1"), + (0x8010, "mdccint_el1"), + (0x8012, "mdscr_el1"), + (0x8014, "dbgbvr2_el1"), + (0x8015, "dbgbcr2_el1"), + (0x8016, "dbgwvr2_el1"), + (0x8017, "dbgwcr2_el1"), + (0x801a, "osdtrtx_el1"), + (0x801c, "dbgbvr3_el1"), + (0x801d, "dbgbcr3_el1"), + (0x801e, "dbgwvr3_el1"), + (0x801f, "dbgwcr3_el1"), + (0x8022, "mdselr_el1"), + (0x8024, "dbgbvr4_el1"), + (0x8025, "dbgbcr4_el1"), + (0x8026, "dbgwvr4_el1"), + (0x8027, "dbgwcr4_el1"), + (0x802c, "dbgbvr5_el1"), + (0x802d, "dbgbcr5_el1"), + (0x802e, "dbgwvr5_el1"), + (0x802f, "dbgwcr5_el1"), + (0x8032, "oseccr_el1"), + (0x8034, "dbgbvr6_el1"), + (0x8035, "dbgbcr6_el1"), + (0x8036, "dbgwvr6_el1"), + (0x8037, "dbgwcr6_el1"), + (0x803c, "dbgbvr7_el1"), + (0x803d, "dbgbcr7_el1"), + (0x803e, "dbgwvr7_el1"), + (0x803f, "dbgwcr7_el1"), + (0x8044, "dbgbvr8_el1"), + (0x8045, "dbgbcr8_el1"), + (0x8046, "dbgwvr8_el1"), + (0x8047, "dbgwcr8_el1"), + (0x804c, "dbgbvr9_el1"), + (0x804d, "dbgbcr9_el1"), + (0x804e, "dbgwvr9_el1"), + (0x804f, "dbgwcr9_el1"), + (0x8054, "dbgbvr10_el1"), + (0x8055, "dbgbcr10_el1"), + (0x8056, "dbgwvr10_el1"), + (0x8057, "dbgwcr10_el1"), + (0x805c, "dbgbvr11_el1"), + (0x805d, "dbgbcr11_el1"), + (0x805e, "dbgwvr11_el1"), + (0x805f, "dbgwcr11_el1"), + (0x8064, "dbgbvr12_el1"), + (0x8065, "dbgbcr12_el1"), + (0x8066, "dbgwvr12_el1"), + (0x8067, "dbgwcr12_el1"), + (0x806c, "dbgbvr13_el1"), + (0x806d, "dbgbcr13_el1"), + (0x806e, "dbgwvr13_el1"), + (0x806f, "dbgwcr13_el1"), + (0x8074, "dbgbvr14_el1"), + (0x8075, "dbgbcr14_el1"), + (0x8076, "dbgwvr14_el1"), + (0x8077, "dbgwcr14_el1"), + (0x807c, "dbgbvr15_el1"), + (0x807d, "dbgbcr15_el1"), + (0x807e, "dbgwvr15_el1"), + (0x807f, "dbgwcr15_el1"), + (0x8080, "mdrar_el1"), + (0x8084, "oslar_el1"), + (0x808c, "oslsr_el1"), + (0x809c, "osdlr_el1"), + (0x80a4, "dbgprcr_el1"), + (0x83c6, "dbgclaimset_el1"), + (0x83ce, "dbgclaimclr_el1"), + (0x83f6, "dbgauthstatus_el1"), + (0x8400, "csrcr_el1"), + (0x8401, "csrptr_el1"), + (0x8403, "csrptridx_el1"), + (0x8740, "pmevcntsvr0_el1"), + (0x8741, "pmevcntsvr1_el1"), + (0x8742, "pmevcntsvr2_el1"), + (0x8743, "pmevcntsvr3_el1"), + (0x8744, "pmevcntsvr4_el1"), + (0x8745, "pmevcntsvr5_el1"), + (0x8746, "pmevcntsvr6_el1"), + (0x8747, "pmevcntsvr7_el1"), + (0x8748, "pmevcntsvr8_el1"), + (0x8749, "pmevcntsvr9_el1"), + (0x874a, "pmevcntsvr10_el1"), + (0x874b, "pmevcntsvr11_el1"), + (0x874c, "pmevcntsvr12_el1"), + (0x874d, "pmevcntsvr13_el1"), + (0x874e, "pmevcntsvr14_el1"), + (0x874f, "pmevcntsvr15_el1"), + (0x8750, "pmevcntsvr16_el1"), + (0x8751, "pmevcntsvr17_el1"), + (0x8752, "pmevcntsvr18_el1"), + (0x8753, "pmevcntsvr19_el1"), + (0x8754, "pmevcntsvr20_el1"), + (0x8755, "pmevcntsvr21_el1"), + (0x8756, "pmevcntsvr22_el1"), + (0x8757, "pmevcntsvr23_el1"), + (0x8758, "pmevcntsvr24_el1"), + (0x8759, "pmevcntsvr25_el1"), + (0x875a, "pmevcntsvr26_el1"), + (0x875b, "pmevcntsvr27_el1"), + (0x875c, "pmevcntsvr28_el1"), + (0x875d, "pmevcntsvr29_el1"), + (0x875e, "pmevcntsvr30_el1"), + (0x875f, "pmccntsvr_el1"), + (0x8760, "pmicntsvr_el1"), + (0x8801, "trctraceidr"), + (0x8802, "trcvictlr"), + (0x8804, "trcseqevr0"), + (0x8805, "trccntrldvr0"), + (0x8806, "trcidr8"), + (0x8807, "trcimspec0"), + (0x8808, "trcprgctlr"), + (0x8809, "trcqctlr"), + (0x880a, "trcviiectlr"), + (0x880c, "trcseqevr1"), + (0x880d, "trccntrldvr1"), + (0x880e, "trcidr9"), + (0x880f, "trcimspec1"), + (0x8810, "trcprocselr"), + (0x8811, "trciteedcr"), + (0x8812, "trcvissctlr"), + (0x8814, "trcseqevr2"), + (0x8815, "trccntrldvr2"), + (0x8816, "trcidr10"), + (0x8817, "trcimspec2"), + (0x8818, "trcstatr"), + (0x881a, "trcvipcssctlr"), + (0x881d, "trccntrldvr3"), + (0x881e, "trcidr11"), + (0x881f, "trcimspec3"), + (0x8820, "trcconfigr"), + (0x8825, "trccntctlr0"), + (0x8826, "trcidr12"), + (0x8827, "trcimspec4"), + (0x882d, "trccntctlr1"), + (0x882e, "trcidr13"), + (0x882f, "trcimspec5"), + (0x8830, "trcauxctlr"), + (0x8834, "trcseqrstevr"), + (0x8835, "trccntctlr2"), + (0x8837, "trcimspec6"), + (0x883c, "trcseqstr"), + (0x883d, "trccntctlr3"), + (0x883f, "trcimspec7"), + (0x8840, "trceventctl0r"), + (0x8842, "trcvdctlr"), + (0x8844, "trcextinselr0"), + (0x8845, "trccntvr0"), + (0x8847, "trcidr0"), + (0x8848, "trceventctl1r"), + (0x884a, "trcvdsacctlr"), + (0x884c, "trcextinselr1"), + (0x884d, "trccntvr1"), + (0x884f, "trcidr1"), + (0x8850, "trcrsr"), + (0x8852, "trcvdarcctlr"), + (0x8854, "trcextinselr2"), + (0x8855, "trccntvr2"), + (0x8857, "trcidr2"), + (0x8858, "trcstallctlr"), + (0x885c, "trcextinselr3"), + (0x885d, "trccntvr3"), + (0x885f, "trcidr3"), + (0x8860, "trctsctlr"), + (0x8867, "trcidr4"), + (0x8868, "trcsyncpr"), + (0x886f, "trcidr5"), + (0x8870, "trcccctlr"), + (0x8877, "trcidr6"), + (0x8878, "trcbbctlr"), + (0x887f, "trcidr7"), + (0x8881, "trcrsctlr16"), + (0x8882, "trcssccr0"), + (0x8883, "trcsspcicr0"), + (0x8884, "trcoslar"), + (0x8889, "trcrsctlr17"), + (0x888a, "trcssccr1"), + (0x888b, "trcsspcicr1"), + (0x888c, "trcoslsr"), + (0x8890, "trcrsctlr2"), + (0x8891, "trcrsctlr18"), + (0x8892, "trcssccr2"), + (0x8893, "trcsspcicr2"), + (0x8898, "trcrsctlr3"), + (0x8899, "trcrsctlr19"), + (0x889a, "trcssccr3"), + (0x889b, "trcsspcicr3"), + (0x88a0, "trcrsctlr4"), + (0x88a1, "trcrsctlr20"), + (0x88a2, "trcssccr4"), + (0x88a3, "trcsspcicr4"), + (0x88a4, "trcpdcr"), + (0x88a8, "trcrsctlr5"), + (0x88a9, "trcrsctlr21"), + (0x88aa, "trcssccr5"), + (0x88ab, "trcsspcicr5"), + (0x88ac, "trcpdsr"), + (0x88b0, "trcrsctlr6"), + (0x88b1, "trcrsctlr22"), + (0x88b2, "trcssccr6"), + (0x88b3, "trcsspcicr6"), + (0x88b8, "trcrsctlr7"), + (0x88b9, "trcrsctlr23"), + (0x88ba, "trcssccr7"), + (0x88bb, "trcsspcicr7"), + (0x88c0, "trcrsctlr8"), + (0x88c1, "trcrsctlr24"), + (0x88c2, "trcsscsr0"), + (0x88c8, "trcrsctlr9"), + (0x88c9, "trcrsctlr25"), + (0x88ca, "trcsscsr1"), + (0x88d0, "trcrsctlr10"), + (0x88d1, "trcrsctlr26"), + (0x88d2, "trcsscsr2"), + (0x88d8, "trcrsctlr11"), + (0x88d9, "trcrsctlr27"), + (0x88da, "trcsscsr3"), + (0x88e0, "trcrsctlr12"), + (0x88e1, "trcrsctlr28"), + (0x88e2, "trcsscsr4"), + (0x88e8, "trcrsctlr13"), + (0x88e9, "trcrsctlr29"), + (0x88ea, "trcsscsr5"), + (0x88f0, "trcrsctlr14"), + (0x88f1, "trcrsctlr30"), + (0x88f2, "trcsscsr6"), + (0x88f8, "trcrsctlr15"), + (0x88f9, "trcrsctlr31"), + (0x88fa, "trcsscsr7"), + (0x8900, "trcacvr0"), + (0x8901, "trcacvr8"), + (0x8902, "trcacatr0"), + (0x8903, "trcacatr8"), + (0x8904, "trcdvcvr0"), + (0x8905, "trcdvcvr4"), + (0x8906, "trcdvcmr0"), + (0x8907, "trcdvcmr4"), + (0x8910, "trcacvr1"), + (0x8911, "trcacvr9"), + (0x8912, "trcacatr1"), + (0x8913, "trcacatr9"), + (0x8920, "trcacvr2"), + (0x8921, "trcacvr10"), + (0x8922, "trcacatr2"), + (0x8923, "trcacatr10"), + (0x8924, "trcdvcvr1"), + (0x8925, "trcdvcvr5"), + (0x8926, "trcdvcmr1"), + (0x8927, "trcdvcmr5"), + (0x8930, "trcacvr3"), + (0x8931, "trcacvr11"), + (0x8932, "trcacatr3"), + (0x8933, "trcacatr11"), + (0x8940, "trcacvr4"), + (0x8941, "trcacvr12"), + (0x8942, "trcacatr4"), + (0x8943, "trcacatr12"), + (0x8944, "trcdvcvr2"), + (0x8945, "trcdvcvr6"), + (0x8946, "trcdvcmr2"), + (0x8947, "trcdvcmr6"), + (0x8950, "trcacvr5"), + (0x8951, "trcacvr13"), + (0x8952, "trcacatr5"), + (0x8953, "trcacatr13"), + (0x8960, "trcacvr6"), + (0x8961, "trcacvr14"), + (0x8962, "trcacatr6"), + (0x8963, "trcacatr14"), + (0x8964, "trcdvcvr3"), + (0x8965, "trcdvcvr7"), + (0x8966, "trcdvcmr3"), + (0x8967, "trcdvcmr7"), + (0x8970, "trcacvr7"), + (0x8971, "trcacvr15"), + (0x8972, "trcacatr7"), + (0x8973, "trcacatr15"), + (0x8980, "trccidcvr0"), + (0x8981, "trcvmidcvr0"), + (0x8982, "trccidcctlr0"), + (0x898a, "trccidcctlr1"), + (0x8990, "trccidcvr1"), + (0x8991, "trcvmidcvr1"), + (0x8992, "trcvmidcctlr0"), + (0x899a, "trcvmidcctlr1"), + (0x89a0, "trccidcvr2"), + (0x89a1, "trcvmidcvr2"), + (0x89b0, "trccidcvr3"), + (0x89b1, "trcvmidcvr3"), + (0x89c0, "trccidcvr4"), + (0x89c1, "trcvmidcvr4"), + (0x89d0, "trccidcvr5"), + (0x89d1, "trcvmidcvr5"), + (0x89e0, "trccidcvr6"), + (0x89e1, "trcvmidcvr6"), + (0x89f0, "trccidcvr7"), + (0x89f1, "trcvmidcvr7"), + (0x8b84, "trcitctrl"), + (0x8b97, "trcdevid"), + (0x8b9f, "trcdevtype"), + (0x8ba7, "trcpidr4"), + (0x8baf, "trcpidr5"), + (0x8bb7, "trcpidr6"), + (0x8bbf, "trcpidr7"), + (0x8bc6, "trcclaimset"), + (0x8bc7, "trcpidr0"), + (0x8bce, "trcclaimclr"), + (0x8bcf, "trcpidr1"), + (0x8bd6, "trcdevaff0"), + (0x8bd7, "trcpidr2"), + (0x8bde, "trcdevaff1"), + (0x8bdf, "trcpidr3"), + (0x8be6, "trclar"), + (0x8be7, "trccidr0"), + (0x8bee, "trclsr"), + (0x8bef, "trccidr1"), + (0x8bf6, "trcauthstatus"), + (0x8bf7, "trccidr2"), + (0x8bfe, "trcdevarch"), + (0x8bff, "trccidr3"), + (0x8c00, "brbinf0_el1"), + (0x8c01, "brbsrc0_el1"), + (0x8c02, "brbtgt0_el1"), + (0x8c04, "brbinf16_el1"), + (0x8c05, "brbsrc16_el1"), + (0x8c06, "brbtgt16_el1"), + (0x8c08, "brbinf1_el1"), + (0x8c09, "brbsrc1_el1"), + (0x8c0a, "brbtgt1_el1"), + (0x8c0c, "brbinf17_el1"), + (0x8c0d, "brbsrc17_el1"), + (0x8c0e, "brbtgt17_el1"), + (0x8c10, "brbinf2_el1"), + (0x8c11, "brbsrc2_el1"), + (0x8c12, "brbtgt2_el1"), + (0x8c14, "brbinf18_el1"), + (0x8c15, "brbsrc18_el1"), + (0x8c16, "brbtgt18_el1"), + (0x8c18, "brbinf3_el1"), + (0x8c19, "brbsrc3_el1"), + (0x8c1a, "brbtgt3_el1"), + (0x8c1c, "brbinf19_el1"), + (0x8c1d, "brbsrc19_el1"), + (0x8c1e, "brbtgt19_el1"), + (0x8c20, "brbinf4_el1"), + (0x8c21, "brbsrc4_el1"), + (0x8c22, "brbtgt4_el1"), + (0x8c24, "brbinf20_el1"), + (0x8c25, "brbsrc20_el1"), + (0x8c26, "brbtgt20_el1"), + (0x8c28, "brbinf5_el1"), + (0x8c29, "brbsrc5_el1"), + (0x8c2a, "brbtgt5_el1"), + (0x8c2c, "brbinf21_el1"), + (0x8c2d, "brbsrc21_el1"), + (0x8c2e, "brbtgt21_el1"), + (0x8c30, "brbinf6_el1"), + (0x8c31, "brbsrc6_el1"), + (0x8c32, "brbtgt6_el1"), + (0x8c34, "brbinf22_el1"), + (0x8c35, "brbsrc22_el1"), + (0x8c36, "brbtgt22_el1"), + (0x8c38, "brbinf7_el1"), + (0x8c39, "brbsrc7_el1"), + (0x8c3a, "brbtgt7_el1"), + (0x8c3c, "brbinf23_el1"), + (0x8c3d, "brbsrc23_el1"), + (0x8c3e, "brbtgt23_el1"), + (0x8c40, "brbinf8_el1"), + (0x8c41, "brbsrc8_el1"), + (0x8c42, "brbtgt8_el1"), + (0x8c44, "brbinf24_el1"), + (0x8c45, "brbsrc24_el1"), + (0x8c46, "brbtgt24_el1"), + (0x8c48, "brbinf9_el1"), + (0x8c49, "brbsrc9_el1"), + (0x8c4a, "brbtgt9_el1"), + (0x8c4c, "brbinf25_el1"), + (0x8c4d, "brbsrc25_el1"), + (0x8c4e, "brbtgt25_el1"), + (0x8c50, "brbinf10_el1"), + (0x8c51, "brbsrc10_el1"), + (0x8c52, "brbtgt10_el1"), + (0x8c54, "brbinf26_el1"), + (0x8c55, "brbsrc26_el1"), + (0x8c56, "brbtgt26_el1"), + (0x8c58, "brbinf11_el1"), + (0x8c59, "brbsrc11_el1"), + (0x8c5a, "brbtgt11_el1"), + (0x8c5c, "brbinf27_el1"), + (0x8c5d, "brbsrc27_el1"), + (0x8c5e, "brbtgt27_el1"), + (0x8c60, "brbinf12_el1"), + (0x8c61, "brbsrc12_el1"), + (0x8c62, "brbtgt12_el1"), + (0x8c64, "brbinf28_el1"), + (0x8c65, "brbsrc28_el1"), + (0x8c66, "brbtgt28_el1"), + (0x8c68, "brbinf13_el1"), + (0x8c69, "brbsrc13_el1"), + (0x8c6a, "brbtgt13_el1"), + (0x8c6c, "brbinf29_el1"), + (0x8c6d, "brbsrc29_el1"), + (0x8c6e, "brbtgt29_el1"), + (0x8c70, "brbinf14_el1"), + (0x8c71, "brbsrc14_el1"), + (0x8c72, "brbtgt14_el1"), + (0x8c74, "brbinf30_el1"), + (0x8c75, "brbsrc30_el1"), + (0x8c76, "brbtgt30_el1"), + (0x8c78, "brbinf15_el1"), + (0x8c79, "brbsrc15_el1"), + (0x8c7a, "brbtgt15_el1"), + (0x8c7c, "brbinf31_el1"), + (0x8c7d, "brbsrc31_el1"), + (0x8c7e, "brbtgt31_el1"), + (0x8c80, "brbcr_el1"), + (0x8c81, "brbfcr_el1"), + (0x8c82, "brbts_el1"), + (0x8c88, "brbinfinj_el1"), + (0x8c89, "brbsrcinj_el1"), + (0x8c8a, "brbtgtinj_el1"), + (0x8c90, "brbidr0_el1"), + (0x9000, "teecr32_el1"), + (0x9080, "teehbr32_el1"), + (0x9808, "mdccsr_el0"), + (0x9820, "dbgdtr_el0"), + (0x9828, "dbgdtrrx_el0"), + (0x9c00, "csrcr_el0"), + (0x9c01, "csrptr_el0"), + (0x9c02, "csridr_el0"), + (0x9c03, "csrptridx_el0"), + (0xa038, "dbgvcr32_el2"), + (0xa400, "csrcr_el2"), + (0xa401, "csrptr_el2"), + (0xa403, "csrptridx_el2"), + (0xa480, "brbcr_el2"), + (0xac00, "csrcr_el12"), + (0xac01, "csrptr_el12"), + (0xac80, "brbcr_el12"), + (0xc000, "midr_el1"), + (0xc004, "mpuir_el1"), + (0xc005, "mpidr_el1"), + (0xc006, "revidr_el1"), + (0xc008, "id_pfr0_el1"), + (0xc009, "id_pfr1_el1"), + (0xc00a, "id_dfr0_el1"), + (0xc00b, "id_afr0_el1"), + (0xc00c, "id_mmfr0_el1"), + (0xc00d, "id_mmfr1_el1"), + (0xc00e, "id_mmfr2_el1"), + (0xc00f, "id_mmfr3_el1"), + (0xc010, "id_isar0_el1"), + (0xc011, "id_isar1_el1"), + (0xc012, "id_isar2_el1"), + (0xc013, "id_isar3_el1"), + (0xc014, "id_isar4_el1"), + (0xc015, "id_isar5_el1"), + (0xc016, "id_mmfr4_el1"), + (0xc017, "id_isar6_el1"), + (0xc018, "mvfr0_el1"), + (0xc019, "mvfr1_el1"), + (0xc01a, "mvfr2_el1"), + (0xc01c, "id_pfr2_el1"), + (0xc01d, "id_dfr1_el1"), + (0xc01e, "id_mmfr5_el1"), + (0xc020, "id_aa64pfr0_el1"), + (0xc021, "id_aa64pfr1_el1"), + (0xc024, "id_aa64zfr0_el1"), + (0xc025, "id_aa64smfr0_el1"), + (0xc028, "id_aa64dfr0_el1"), + (0xc029, "id_aa64dfr1_el1"), + (0xc02c, "id_aa64afr0_el1"), + (0xc02d, "id_aa64afr1_el1"), + (0xc030, "id_aa64isar0_el1"), + (0xc031, "id_aa64isar1_el1"), + (0xc032, "id_aa64isar2_el1"), + (0xc038, "id_aa64mmfr0_el1"), + (0xc039, "id_aa64mmfr1_el1"), + (0xc03a, "id_aa64mmfr2_el1"), + (0xc03b, "id_aa64mmfr3_el1"), + (0xc03c, "id_aa64mmfr4_el1"), + (0xc080, "sctlr_el1"), + (0xc081, "actlr_el1"), + (0xc082, "cpacr_el1"), + (0xc083, "sctlr2_el1"), + (0xc085, "rgsr_el1"), + (0xc086, "gcr_el1"), + (0xc090, "zcr_el1"), + (0xc091, "trfcr_el1"), + (0xc093, "trcitecr_el1"), + (0xc094, "smpri_el1"), + (0xc096, "smcr_el1"), + (0xc100, "ttbr0_el1"), + (0xc101, "ttbr1_el1"), + (0xc102, "tcr_el1"), + (0xc103, "tcr2_el1"), + (0xc108, "apiakeylo_el1"), + (0xc109, "apiakeyhi_el1"), + (0xc10a, "apibkeylo_el1"), + (0xc10b, "apibkeyhi_el1"), + (0xc110, "apdakeylo_el1"), + (0xc111, "apdakeyhi_el1"), + (0xc112, "apdbkeylo_el1"), + (0xc113, "apdbkeyhi_el1"), + (0xc118, "apgakeylo_el1"), + (0xc119, "apgakeyhi_el1"), + (0xc128, "gcscr_el1"), + (0xc129, "gcspr_el1"), + (0xc12a, "gcscre0_el1"), + (0xc200, "spsr_el1"), + (0xc201, "elr_el1"), + (0xc208, "sp_el0"), + (0xc210, "spsel"), + (0xc212, "currentel"), + (0xc213, "pan"), + (0xc214, "uao"), + (0xc218, "allint"), + (0xc230, "icc_pmr_el1"), + (0xc288, "afsr0_el1"), + (0xc289, "afsr1_el1"), + (0xc290, "esr_el1"), + (0xc298, "erridr_el1"), + (0xc299, "errselr_el1"), + (0xc29a, "erxgsr_el1"), + (0xc2a0, "erxfr_el1"), + (0xc2a1, "erxctlr_el1"), + (0xc2a2, "erxstatus_el1"), + (0xc2a3, "erxaddr_el1"), + (0xc2a4, "erxpfgf_el1"), + (0xc2a5, "erxpfgctl_el1"), + (0xc2a6, "erxpfgcdn_el1"), + (0xc2a8, "erxmisc0_el1"), + (0xc2a9, "erxmisc1_el1"), + (0xc2aa, "erxmisc2_el1"), + (0xc2ab, "erxmisc3_el1"), + (0xc2b0, "tfsr_el1"), + (0xc2b1, "tfsre0_el1"), + (0xc300, "far_el1"), + (0xc305, "pfar_el1"), + (0xc309, "prenr_el1"), + (0xc311, "prselr_el1"), + (0xc340, "prbar_el1"), + (0xc341, "prlar_el1"), + (0xc344, "prbar1_el1"), + (0xc345, "prlar1_el1"), + (0xc348, "prbar2_el1"), + (0xc349, "prlar2_el1"), + (0xc34c, "prbar3_el1"), + (0xc34d, "prlar3_el1"), + (0xc350, "prbar4_el1"), + (0xc351, "prlar4_el1"), + (0xc354, "prbar5_el1"), + (0xc355, "prlar5_el1"), + (0xc358, "prbar6_el1"), + (0xc359, "prlar6_el1"), + (0xc35c, "prbar7_el1"), + (0xc35d, "prlar7_el1"), + (0xc360, "prbar8_el1"), + (0xc361, "prlar8_el1"), + (0xc364, "prbar9_el1"), + (0xc365, "prlar9_el1"), + (0xc368, "prbar10_el1"), + (0xc369, "prlar10_el1"), + (0xc36c, "prbar11_el1"), + (0xc36d, "prlar11_el1"), + (0xc370, "prbar12_el1"), + (0xc371, "prlar12_el1"), + (0xc374, "prbar13_el1"), + (0xc375, "prlar13_el1"), + (0xc378, "prbar14_el1"), + (0xc379, "prlar14_el1"), + (0xc37c, "prbar15_el1"), + (0xc37d, "prlar15_el1"), + (0xc3a0, "par_el1"), + (0xc4c8, "pmscr_el1"), + (0xc4c9, "pmsnevfr_el1"), + (0xc4ca, "pmsicr_el1"), + (0xc4cb, "pmsirr_el1"), + (0xc4cc, "pmsfcr_el1"), + (0xc4cd, "pmsevfr_el1"), + (0xc4ce, "pmslatfr_el1"), + (0xc4cf, "pmsidr_el1"), + (0xc4d0, "pmblimitr_el1"), + (0xc4d1, "pmbptr_el1"), + (0xc4d3, "pmbsr_el1"), + (0xc4d7, "pmbidr_el1"), + (0xc4d8, "trblimitr_el1"), + (0xc4d9, "trbptr_el1"), + (0xc4da, "trbbaser_el1"), + (0xc4db, "trbsr_el1"), + (0xc4dc, "trbmar_el1"), + (0xc4de, "trbtrg_el1"), + (0xc4df, "trbidr_el1"), + (0xc4eb, "pmsscr_el1"), + (0xc4f1, "pmintenset_el1"), + (0xc4f2, "pmintenclr_el1"), + (0xc4f4, "pmuacr_el1"), + (0xc4f5, "pmecr_el1"), + (0xc4f6, "pmmir_el1"), + (0xc4f7, "pmiar_el1"), + (0xc510, "mair_el1"), + (0xc511, "mair2_el1"), + (0xc512, "pire0_el1"), + (0xc513, "pir_el1"), + (0xc514, "por_el1"), + (0xc515, "s2por_el1"), + (0xc518, "amair_el1"), + (0xc519, "amair2_el1"), + (0xc520, "lorsa_el1"), + (0xc521, "lorea_el1"), + (0xc522, "lorn_el1"), + (0xc523, "lorc_el1"), + (0xc524, "mpamidr_el1"), + (0xc527, "lorid_el1"), + (0xc528, "mpam1_el1"), + (0xc529, "mpam0_el1"), + (0xc52b, "mpamsm_el1"), + (0xc600, "vbar_el1"), + (0xc601, "rvbar_el1"), + (0xc602, "rmr_el1"), + (0xc608, "isr_el1"), + (0xc609, "disr_el1"), + (0xc640, "icc_iar0_el1"), + (0xc641, "icc_eoir0_el1"), + (0xc642, "icc_hppir0_el1"), + (0xc643, "icc_bpr0_el1"), + (0xc644, "icc_ap0r0_el1"), + (0xc645, "icc_ap0r1_el1"), + (0xc646, "icc_ap0r2_el1"), + (0xc647, "icc_ap0r3_el1"), + (0xc648, "icc_ap1r0_el1"), + (0xc649, "icc_ap1r1_el1"), + (0xc64a, "icc_ap1r2_el1"), + (0xc64b, "icc_ap1r3_el1"), + (0xc64d, "icc_nmiar1_el1"), + (0xc659, "icc_dir_el1"), + (0xc65b, "icc_rpr_el1"), + (0xc65d, "icc_sgi1r_el1"), + (0xc65e, "icc_asgi1r_el1"), + (0xc65f, "icc_sgi0r_el1"), + (0xc660, "icc_iar1_el1"), + (0xc661, "icc_eoir1_el1"), + (0xc662, "icc_hppir1_el1"), + (0xc663, "icc_bpr1_el1"), + (0xc664, "icc_ctlr_el1"), + (0xc665, "icc_sre_el1"), + (0xc666, "icc_igrpen0_el1"), + (0xc667, "icc_igrpen1_el1"), + (0xc681, "contextidr_el1"), + (0xc683, "rcwsmask_el1"), + (0xc684, "tpidr_el1"), + (0xc685, "accdata_el1"), + (0xc686, "rcwmask_el1"), + (0xc687, "scxtnum_el1"), + (0xc708, "cntkctl_el1"), + (0xc800, "ccsidr_el1"), + (0xc801, "clidr_el1"), + (0xc802, "ccsidr2_el1"), + (0xc804, "gmid_el1"), + (0xc806, "smidr_el1"), + (0xc807, "aidr_el1"), + (0xd000, "csselr_el1"), + (0xd801, "ctr_el0"), + (0xd807, "dczid_el0"), + (0xd920, "rndr"), + (0xd921, "rndrrs"), + (0xd929, "gcspr_el0"), + (0xda10, "nzcv"), + (0xda11, "daif"), + (0xda12, "svcr"), + (0xda15, "dit"), + (0xda16, "ssbs"), + (0xda17, "tco"), + (0xda20, "fpcr"), + (0xda21, "fpsr"), + (0xda28, "dspsr_el0"), + (0xda29, "dlr_el0"), + (0xdca0, "pmicntr_el0"), + (0xdcb0, "pmicfiltr_el0"), + (0xdce0, "pmcr_el0"), + (0xdce1, "pmcntenset_el0"), + (0xdce2, "pmcntenclr_el0"), + (0xdce3, "pmovsclr_el0"), + (0xdce4, "pmswinc_el0"), + (0xdce5, "pmselr_el0"), + (0xdce6, "pmceid0_el0"), + (0xdce7, "pmceid1_el0"), + (0xdce8, "pmccntr_el0"), + (0xdce9, "pmxevtyper_el0"), + (0xdcea, "pmxevcntr_el0"), + (0xdcec, "pmzr_el0"), + (0xdcf0, "pmuserenr_el0"), + (0xdcf3, "pmovsset_el0"), + (0xdd14, "por_el0"), + (0xde82, "tpidr_el0"), + (0xde83, "tpidrro_el0"), + (0xde85, "tpidr2_el0"), + (0xde87, "scxtnum_el0"), + (0xde90, "amcr_el0"), + (0xde91, "amcfgr_el0"), + (0xde92, "amcgcr_el0"), + (0xde93, "amuserenr_el0"), + (0xde94, "amcntenclr0_el0"), + (0xde95, "amcntenset0_el0"), + (0xde96, "amcg1idr_el0"), + (0xde98, "amcntenclr1_el0"), + (0xde99, "amcntenset1_el0"), + (0xdea0, "amevcntr00_el0"), + (0xdea1, "amevcntr01_el0"), + (0xdea2, "amevcntr02_el0"), + (0xdea3, "amevcntr03_el0"), + (0xdeb0, "amevtyper00_el0"), + (0xdeb1, "amevtyper01_el0"), + (0xdeb2, "amevtyper02_el0"), + (0xdeb3, "amevtyper03_el0"), + (0xdee0, "amevcntr10_el0"), + (0xdee1, "amevcntr11_el0"), + (0xdee2, "amevcntr12_el0"), + (0xdee3, "amevcntr13_el0"), + (0xdee4, "amevcntr14_el0"), + (0xdee5, "amevcntr15_el0"), + (0xdee6, "amevcntr16_el0"), + (0xdee7, "amevcntr17_el0"), + (0xdee8, "amevcntr18_el0"), + (0xdee9, "amevcntr19_el0"), + (0xdeea, "amevcntr110_el0"), + (0xdeeb, "amevcntr111_el0"), + (0xdeec, "amevcntr112_el0"), + (0xdeed, "amevcntr113_el0"), + (0xdeee, "amevcntr114_el0"), + (0xdeef, "amevcntr115_el0"), + (0xdef0, "amevtyper10_el0"), + (0xdef1, "amevtyper11_el0"), + (0xdef2, "amevtyper12_el0"), + (0xdef3, "amevtyper13_el0"), + (0xdef4, "amevtyper14_el0"), + (0xdef5, "amevtyper15_el0"), + (0xdef6, "amevtyper16_el0"), + (0xdef7, "amevtyper17_el0"), + (0xdef8, "amevtyper18_el0"), + (0xdef9, "amevtyper19_el0"), + (0xdefa, "amevtyper110_el0"), + (0xdefb, "amevtyper111_el0"), + (0xdefc, "amevtyper112_el0"), + (0xdefd, "amevtyper113_el0"), + (0xdefe, "amevtyper114_el0"), + (0xdeff, "amevtyper115_el0"), + (0xdf00, "cntfrq_el0"), + (0xdf01, "cntpct_el0"), + (0xdf02, "cntvct_el0"), + (0xdf05, "cntpctss_el0"), + (0xdf06, "cntvctss_el0"), + (0xdf10, "cntp_tval_el0"), + (0xdf11, "cntp_ctl_el0"), + (0xdf12, "cntp_cval_el0"), + (0xdf18, "cntv_tval_el0"), + (0xdf19, "cntv_ctl_el0"), + (0xdf1a, "cntv_cval_el0"), + (0xdf40, "pmevcntr0_el0"), + (0xdf41, "pmevcntr1_el0"), + (0xdf42, "pmevcntr2_el0"), + (0xdf43, "pmevcntr3_el0"), + (0xdf44, "pmevcntr4_el0"), + (0xdf45, "pmevcntr5_el0"), + (0xdf46, "pmevcntr6_el0"), + (0xdf47, "pmevcntr7_el0"), + (0xdf48, "pmevcntr8_el0"), + (0xdf49, "pmevcntr9_el0"), + (0xdf4a, "pmevcntr10_el0"), + (0xdf4b, "pmevcntr11_el0"), + (0xdf4c, "pmevcntr12_el0"), + (0xdf4d, "pmevcntr13_el0"), + (0xdf4e, "pmevcntr14_el0"), + (0xdf4f, "pmevcntr15_el0"), + (0xdf50, "pmevcntr16_el0"), + (0xdf51, "pmevcntr17_el0"), + (0xdf52, "pmevcntr18_el0"), + (0xdf53, "pmevcntr19_el0"), + (0xdf54, "pmevcntr20_el0"), + (0xdf55, "pmevcntr21_el0"), + (0xdf56, "pmevcntr22_el0"), + (0xdf57, "pmevcntr23_el0"), + (0xdf58, "pmevcntr24_el0"), + (0xdf59, "pmevcntr25_el0"), + (0xdf5a, "pmevcntr26_el0"), + (0xdf5b, "pmevcntr27_el0"), + (0xdf5c, "pmevcntr28_el0"), + (0xdf5d, "pmevcntr29_el0"), + (0xdf5e, "pmevcntr30_el0"), + (0xdf60, "pmevtyper0_el0"), + (0xdf61, "pmevtyper1_el0"), + (0xdf62, "pmevtyper2_el0"), + (0xdf63, "pmevtyper3_el0"), + (0xdf64, "pmevtyper4_el0"), + (0xdf65, "pmevtyper5_el0"), + (0xdf66, "pmevtyper6_el0"), + (0xdf67, "pmevtyper7_el0"), + (0xdf68, "pmevtyper8_el0"), + (0xdf69, "pmevtyper9_el0"), + (0xdf6a, "pmevtyper10_el0"), + (0xdf6b, "pmevtyper11_el0"), + (0xdf6c, "pmevtyper12_el0"), + (0xdf6d, "pmevtyper13_el0"), + (0xdf6e, "pmevtyper14_el0"), + (0xdf6f, "pmevtyper15_el0"), + (0xdf70, "pmevtyper16_el0"), + (0xdf71, "pmevtyper17_el0"), + (0xdf72, "pmevtyper18_el0"), + (0xdf73, "pmevtyper19_el0"), + (0xdf74, "pmevtyper20_el0"), + (0xdf75, "pmevtyper21_el0"), + (0xdf76, "pmevtyper22_el0"), + (0xdf77, "pmevtyper23_el0"), + (0xdf78, "pmevtyper24_el0"), + (0xdf79, "pmevtyper25_el0"), + (0xdf7a, "pmevtyper26_el0"), + (0xdf7b, "pmevtyper27_el0"), + (0xdf7c, "pmevtyper28_el0"), + (0xdf7d, "pmevtyper29_el0"), + (0xdf7e, "pmevtyper30_el0"), + (0xdf7f, "pmccfiltr_el0"), + (0xe000, "vpidr_el2"), + (0xe004, "mpuir_el2"), + (0xe005, "vmpidr_el2"), + (0xe080, "sctlr_el2"), + (0xe081, "actlr_el2"), + (0xe083, "sctlr2_el2"), + (0xe088, "hcr_el2"), + (0xe089, "mdcr_el2"), + (0xe08a, "cptr_el2"), + (0xe08b, "hstr_el2"), + (0xe08c, "hfgrtr_el2"), + (0xe08d, "hfgwtr_el2"), + (0xe08e, "hfgitr_el2"), + (0xe08f, "hacr_el2"), + (0xe090, "zcr_el2"), + (0xe091, "trfcr_el2"), + (0xe092, "hcrx_el2"), + (0xe093, "trcitecr_el2"), + (0xe095, "smprimap_el2"), + (0xe096, "smcr_el2"), + (0xe099, "sder32_el2"), + (0xe100, "ttbr0_el2"), + (0xe101, "ttbr1_el2"), + (0xe102, "tcr_el2"), + (0xe103, "tcr2_el2"), + (0xe108, "vttbr_el2"), + (0xe10a, "vtcr_el2"), + (0xe110, "vncr_el2"), + (0xe128, "gcscr_el2"), + (0xe129, "gcspr_el2"), + (0xe130, "vsttbr_el2"), + (0xe132, "vstcr_el2"), + (0xe180, "dacr32_el2"), + (0xe188, "hdfgrtr2_el2"), + (0xe189, "hdfgwtr2_el2"), + (0xe18a, "hfgrtr2_el2"), + (0xe18b, "hfgwtr2_el2"), + (0xe18c, "hdfgrtr_el2"), + (0xe18d, "hdfgwtr_el2"), + (0xe18e, "hafgrtr_el2"), + (0xe200, "spsr_el2"), + (0xe201, "elr_el2"), + (0xe208, "sp_el1"), + (0xe218, "spsr_irq"), + (0xe219, "spsr_abt"), + (0xe21a, "spsr_und"), + (0xe21b, "spsr_fiq"), + (0xe281, "ifsr32_el2"), + (0xe288, "afsr0_el2"), + (0xe289, "afsr1_el2"), + (0xe290, "esr_el2"), + (0xe293, "vsesr_el2"), + (0xe298, "fpexc32_el2"), + (0xe2b0, "tfsr_el2"), + (0xe300, "far_el2"), + (0xe304, "hpfar_el2"), + (0xe305, "pfar_el2"), + (0xe309, "prenr_el2"), + (0xe311, "prselr_el2"), + (0xe340, "prbar_el2"), + (0xe341, "prlar_el2"), + (0xe344, "prbar1_el2"), + (0xe345, "prlar1_el2"), + (0xe348, "prbar2_el2"), + (0xe349, "prlar2_el2"), + (0xe34c, "prbar3_el2"), + (0xe34d, "prlar3_el2"), + (0xe350, "prbar4_el2"), + (0xe351, "prlar4_el2"), + (0xe354, "prbar5_el2"), + (0xe355, "prlar5_el2"), + (0xe358, "prbar6_el2"), + (0xe359, "prlar6_el2"), + (0xe35c, "prbar7_el2"), + (0xe35d, "prlar7_el2"), + (0xe360, "prbar8_el2"), + (0xe361, "prlar8_el2"), + (0xe364, "prbar9_el2"), + (0xe365, "prlar9_el2"), + (0xe368, "prbar10_el2"), + (0xe369, "prlar10_el2"), + (0xe36c, "prbar11_el2"), + (0xe36d, "prlar11_el2"), + (0xe370, "prbar12_el2"), + (0xe371, "prlar12_el2"), + (0xe374, "prbar13_el2"), + (0xe375, "prlar13_el2"), + (0xe378, "prbar14_el2"), + (0xe379, "prlar14_el2"), + (0xe37c, "prbar15_el2"), + (0xe37d, "prlar15_el2"), + (0xe4c8, "pmscr_el2"), + (0xe4d4, "pmsdsfr_el1"), + (0xe509, "mair2_el2"), + (0xe510, "mair_el2"), + (0xe512, "pire0_el2"), + (0xe513, "pir_el2"), + (0xe514, "por_el2"), + (0xe515, "s2pir_el2"), + (0xe518, "amair_el2"), + (0xe519, "amair2_el2"), + (0xe520, "mpamhcr_el2"), + (0xe521, "mpamvpmv_el2"), + (0xe528, "mpam2_el2"), + (0xe530, "mpamvpm0_el2"), + (0xe531, "mpamvpm1_el2"), + (0xe532, "mpamvpm2_el2"), + (0xe533, "mpamvpm3_el2"), + (0xe534, "mpamvpm4_el2"), + (0xe535, "mpamvpm5_el2"), + (0xe536, "mpamvpm6_el2"), + (0xe537, "mpamvpm7_el2"), + (0xe540, "mecid_p0_el2"), + (0xe541, "mecid_a0_el2"), + (0xe542, "mecid_p1_el2"), + (0xe543, "mecid_a1_el2"), + (0xe547, "mecidr_el2"), + (0xe548, "vmecid_p_el2"), + (0xe549, "vmecid_a_el2"), + (0xe600, "vbar_el2"), + (0xe601, "rvbar_el2"), + (0xe602, "rmr_el2"), + (0xe609, "vdisr_el2"), + (0xe640, "ich_ap0r0_el2"), + (0xe641, "ich_ap0r1_el2"), + (0xe642, "ich_ap0r2_el2"), + (0xe643, "ich_ap0r3_el2"), + (0xe648, "ich_ap1r0_el2"), + (0xe649, "ich_ap1r1_el2"), + (0xe64a, "ich_ap1r2_el2"), + (0xe64b, "ich_ap1r3_el2"), + (0xe64d, "icc_sre_el2"), + (0xe658, "ich_hcr_el2"), + (0xe659, "ich_vtr_el2"), + (0xe65a, "ich_misr_el2"), + (0xe65b, "ich_eisr_el2"), + (0xe65d, "ich_elrsr_el2"), + (0xe65f, "ich_vmcr_el2"), + (0xe660, "ich_lr0_el2"), + (0xe661, "ich_lr1_el2"), + (0xe662, "ich_lr2_el2"), + (0xe663, "ich_lr3_el2"), + (0xe664, "ich_lr4_el2"), + (0xe665, "ich_lr5_el2"), + (0xe666, "ich_lr6_el2"), + (0xe667, "ich_lr7_el2"), + (0xe668, "ich_lr8_el2"), + (0xe669, "ich_lr9_el2"), + (0xe66a, "ich_lr10_el2"), + (0xe66b, "ich_lr11_el2"), + (0xe66c, "ich_lr12_el2"), + (0xe66d, "ich_lr13_el2"), + (0xe66e, "ich_lr14_el2"), + (0xe66f, "ich_lr15_el2"), + (0xe681, "contextidr_el2"), + (0xe682, "tpidr_el2"), + (0xe687, "scxtnum_el2"), + (0xe6c0, "amevcntvoff00_el2"), + (0xe6c1, "amevcntvoff01_el2"), + (0xe6c2, "amevcntvoff02_el2"), + (0xe6c3, "amevcntvoff03_el2"), + (0xe6c4, "amevcntvoff04_el2"), + (0xe6c5, "amevcntvoff05_el2"), + (0xe6c6, "amevcntvoff06_el2"), + (0xe6c7, "amevcntvoff07_el2"), + (0xe6c8, "amevcntvoff08_el2"), + (0xe6c9, "amevcntvoff09_el2"), + (0xe6ca, "amevcntvoff010_el2"), + (0xe6cb, "amevcntvoff011_el2"), + (0xe6cc, "amevcntvoff012_el2"), + (0xe6cd, "amevcntvoff013_el2"), + (0xe6ce, "amevcntvoff014_el2"), + (0xe6cf, "amevcntvoff015_el2"), + (0xe6d0, "amevcntvoff10_el2"), + (0xe6d1, "amevcntvoff11_el2"), + (0xe6d2, "amevcntvoff12_el2"), + (0xe6d3, "amevcntvoff13_el2"), + (0xe6d4, "amevcntvoff14_el2"), + (0xe6d5, "amevcntvoff15_el2"), + (0xe6d6, "amevcntvoff16_el2"), + (0xe6d7, "amevcntvoff17_el2"), + (0xe6d8, "amevcntvoff18_el2"), + (0xe6d9, "amevcntvoff19_el2"), + (0xe6da, "amevcntvoff110_el2"), + (0xe6db, "amevcntvoff111_el2"), + (0xe6dc, "amevcntvoff112_el2"), + (0xe6dd, "amevcntvoff113_el2"), + (0xe6de, "amevcntvoff114_el2"), + (0xe6df, "amevcntvoff115_el2"), + (0xe703, "cntvoff_el2"), + (0xe706, "cntpoff_el2"), + (0xe708, "cnthctl_el2"), + (0xe710, "cnthp_tval_el2"), + (0xe711, "cnthp_ctl_el2"), + (0xe712, "cnthp_cval_el2"), + (0xe718, "cnthv_tval_el2"), + (0xe719, "cnthv_ctl_el2"), + (0xe71a, "cnthv_cval_el2"), + (0xe720, "cnthvs_tval_el2"), + (0xe721, "cnthvs_ctl_el2"), + (0xe722, "cnthvs_cval_el2"), + (0xe728, "cnthps_tval_el2"), + (0xe729, "cnthps_ctl_el2"), + (0xe72a, "cnthps_cval_el2"), + (0xe880, "sctlr_el12"), + (0xe882, "cpacr_el12"), + (0xe883, "sctlr2_el12"), + (0xe890, "zcr_el12"), + (0xe891, "trfcr_el12"), + (0xe893, "trcitecr_el12"), + (0xe896, "smcr_el12"), + (0xe900, "ttbr0_el12"), + (0xe901, "ttbr1_el12"), + (0xe902, "tcr_el12"), + (0xe903, "tcr2_el12"), + (0xe928, "gcscr_el12"), + (0xe929, "gcspr_el12"), + (0xea00, "spsr_el12"), + (0xea01, "elr_el12"), + (0xea88, "afsr0_el12"), + (0xea89, "afsr1_el12"), + (0xea90, "esr_el12"), + (0xeab0, "tfsr_el12"), + (0xeb00, "far_el12"), + (0xeb05, "pfar_el12"), + (0xecc8, "pmscr_el12"), + (0xed10, "mair_el12"), + (0xed11, "mair2_el12"), + (0xed12, "pire0_el12"), + (0xed13, "pir_el12"), + (0xed14, "por_el12"), + (0xed18, "amair_el12"), + (0xed19, "amair2_el12"), + (0xed28, "mpam1_el12"), + (0xee00, "vbar_el12"), + (0xee81, "contextidr_el12"), + (0xee87, "scxtnum_el12"), + (0xef08, "cntkctl_el12"), + (0xef10, "cntp_tval_el02"), + (0xef11, "cntp_ctl_el02"), + (0xef12, "cntp_cval_el02"), + (0xef18, "cntv_tval_el02"), + (0xef19, "cntv_ctl_el02"), + (0xef1a, "cntv_cval_el02"), + (0xf080, "sctlr_el3"), + (0xf081, "actlr_el3"), + (0xf083, "sctlr2_el3"), + (0xf088, "scr_el3"), + (0xf089, "sder32_el3"), + (0xf08a, "cptr_el3"), + (0xf090, "zcr_el3"), + (0xf096, "smcr_el3"), + (0xf099, "mdcr_el3"), + (0xf100, "ttbr0_el3"), + (0xf102, "tcr_el3"), + (0xf10c, "gptbr_el3"), + (0xf10e, "gpccr_el3"), + (0xf128, "gcscr_el3"), + (0xf129, "gcspr_el3"), + (0xf200, "spsr_el3"), + (0xf201, "elr_el3"), + (0xf208, "sp_el2"), + (0xf288, "afsr0_el3"), + (0xf289, "afsr1_el3"), + (0xf290, "esr_el3"), + (0xf2b0, "tfsr_el3"), + (0xf300, "far_el3"), + (0xf305, "mfar_el3"), + (0xf509, "mair2_el3"), + (0xf510, "mair_el3"), + (0xf513, "pir_el3"), + (0xf514, "por_el3"), + (0xf518, "amair_el3"), + (0xf519, "amair2_el3"), + (0xf528, "mpam3_el3"), + (0xf551, "mecid_rl_a_el3"), + (0xf600, "vbar_el3"), + (0xf601, "rvbar_el3"), + (0xf602, "rmr_el3"), + (0xf664, "icc_ctlr_el3"), + (0xf665, "icc_sre_el3"), + (0xf667, "icc_igrpen1_el3"), + (0xf682, "tpidr_el3"), + (0xf687, "scxtnum_el3"), + (0xff10, "cntps_tval_el1"), + (0xff11, "cntps_ctl_el1"), + (0xff12, "cntps_cval_el1"), +]; + +/// The name for an encoding, if it has one. +pub fn lookup(enc: u16) -> Option<&'static str> { + SYSREGS + .binary_search_by_key(&enc, |(e, _)| *e) + .ok() + .map(|i| SYSREGS[i].1) +} diff --git a/crates/e5r-arch/src/aarch64/text.rs b/crates/e5r-arch/src/aarch64/text.rs new file mode 100644 index 0000000..b2e4dd3 --- /dev/null +++ b/crates/e5r-arch/src/aarch64/text.rs @@ -0,0 +1,251 @@ +//! AArch64 text output, formatted the way objdump formats it. +//! +//! Matching objdump is not an aesthetic choice: it is what makes the G3 parity +//! gate a string comparison instead of a judgment call. + +use std::fmt::Write; + +use crate::insn::{AddrMode, Extend, Insn, Mem, Operand, Reg, RegClass, Width}; + +use super::cond_name; + +/// How to render addresses and immediates. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Style { + /// Print branch targets as bare hex with no `0x`, which is what objdump + /// does. Off, they print as `0x...` for a reader. + pub objdump: bool, +} + +/// Render a register. +fn reg(out: &mut String, r: Reg) { + match r.class { + RegClass::Sp => out.push_str(if r.width == Width::W64 { "sp" } else { "wsp" }), + RegClass::Zr => out.push_str(if r.width == Width::W64 { "xzr" } else { "wzr" }), + RegClass::Gpr => { + let _ = write!( + out, + "{}{}", + if r.width == Width::W64 { 'x' } else { 'w' }, + r.num + ); + } + RegClass::Vec => { + let prefix = match r.width { + Width::W8 => 'b', + Width::W16 => 'h', + Width::W32 => 's', + Width::W64 => 'd', + Width::W128 => 'q', + }; + let _ = write!(out, "{prefix}{}", r.num); + } + RegClass::Sys => { + let _ = write!(out, "s{}", r.num); + } + RegClass::Pc => out.push_str("pc"), + // x86 banks, which AArch64 never produces. + RegClass::GprHigh | RegClass::Seg => out.push('?'), + RegClass::Flags => out.push_str("nzcv"), + } +} + +/// Plain immediates print as hex; negatives keep a minus rather than wrapping. +fn imm(out: &mut String, v: i64) { + if v < 0 { + let _ = write!(out, "#-{:#x}", v.unsigned_abs()); + } else { + let _ = write!(out, "#{v:#x}"); + } +} + +/// Counts, indices and displacements print as decimal. +fn count(out: &mut String, v: i64) { + let _ = write!(out, "#{v}"); +} + +fn mem(out: &mut String, m: Mem, _style: Style) { + out.push('['); + if let Some(b) = m.base { + reg(out, b); + } + match m.mode { + AddrMode::PostIndex => { + out.push_str("], "); + count(out, m.disp); + } + AddrMode::Offset | AddrMode::PreIndex => { + if let Some((ix, ext, amount)) = m.index { + out.push_str(", "); + reg(out, ix); + // A plain lsl #0 is not printed. + if ext == Extend::LslZero { + out.push_str(", lsl #0"); + } else if !(ext == Extend::Lsl && amount == 0) { + let _ = write!(out, ", {}", ext.as_str()); + if amount != 0 { + let _ = write!(out, " #{amount}"); + } + } + } else if m.disp != 0 { + out.push_str(", "); + count(out, m.disp); + } + out.push(']'); + if m.mode == AddrMode::PreIndex { + out.push('!'); + } + } + } +} + +/// Render one operand. +fn operand(out: &mut String, op: &Operand, style: Style) { + match op { + Operand::Reg(r) => reg(out, *r), + Operand::Shifted(r, sh, n) => { + reg(out, *r); + let _ = write!(out, ", {} #{n}", sh.as_str()); + } + Operand::Extended(r, ext, n) => { + reg(out, *r); + let _ = write!(out, ", {}", ext.as_str()); + if *n != 0 { + let _ = write!(out, " #{n}"); + } + } + Operand::Imm(v) => imm(out, *v), + Operand::UImm(v) => { + let _ = write!(out, "#{v:#x}"); + } + Operand::Count(v) => count(out, *v), + Operand::ShiftOp(sh, n) => { + let _ = write!(out, "{} #{n}", sh.as_str()); + } + Operand::Name(n) => out.push_str(n), + Operand::FpImm(b) => { + let _ = write!(out, "#{}", fp_text(*b)); + } + Operand::Vector(n, lanes) => { + let _ = write!(out, "v{n}.{}", lanes.as_str()); + } + Operand::VectorLane(n, width, index) => { + let letter = match width { + Width::W8 => 'b', + Width::W16 => 'h', + Width::W32 => 's', + _ => 'd', + }; + let _ = write!(out, "v{n}.{letter}[{index}]"); + } + Operand::VectorList(first, count, lanes) => { + out.push('{'); + for i in 0..*count { + if i > 0 { + out.push_str(", "); + } + let _ = write!(out, "v{}.{}", (first + i) % 32, lanes.as_str()); + } + out.push('}'); + } + Operand::Addr(a) => { + if style.objdump { + let _ = write!(out, "{:x}", a.get()); + } else { + let _ = write!(out, "{:#x}", a.get()); + } + } + Operand::Mem(m) => mem(out, *m, style), + Operand::Cond(c) => out.push_str(cond_name(*c)), + Operand::Sys(v) => out.push_str(&super::sysreg_name(*v)), + } +} + +/// A floating point literal in objdump's `%.18e` spelling. +fn fp_text(bits: u64) -> String { + let v = f64::from_bits(bits); + let s = format!("{v:.18e}"); + match s.split_once('e') { + Some((m, e)) => { + let exp: i32 = e.parse().unwrap_or(0); + format!("{m}e{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs()) + } + None => s, + } +} + +/// Render a whole instruction as `mnemonic\toperands`. +pub fn format(i: &Insn, style: Style) -> String { + let mut out = String::with_capacity(32); + out.push_str(i.mnemonic); + for (n, op) in i.operands().iter().enumerate() { + out.push_str(if n == 0 { "\t" } else { ", " }); + operand(&mut out, op, style); + } + out +} + +/// A shift whose amount is zero and kind is `lsl` prints as a bare register, +/// which the decoder already arranges; this is here so the invariant is tested. +#[cfg(test)] +mod tests { + use super::*; + use crate::insn::Flow; + use e5r_core::Addr; + + #[test] + fn registers_print_by_width() { + let mut s = String::new(); + reg(&mut s, Reg::gpr(3, Width::W32)); + reg(&mut s, Reg::gpr(3, Width::W64)); + assert_eq!(s, "w3x3"); + } + + #[test] + fn immediates_are_hex_and_counts_are_decimal() { + let mut s = String::new(); + imm(&mut s, -16); + assert_eq!(s, "#-0x10"); + let mut d = String::new(); + count(&mut d, -16); + assert_eq!(d, "#-16"); + } + + #[test] + fn post_index_moves_the_bracket() { + let mut s = String::new(); + mem( + &mut s, + Mem { + seg: None, + base: Some(Reg { + class: RegClass::Sp, + num: 31, + width: Width::W64, + }), + index: None, + disp: 16, + mode: AddrMode::PostIndex, + size: 8, + }, + Style::default(), + ); + assert_eq!(s, "[sp], #16"); + } + + #[test] + fn fp_literals_match_objdumps_spelling() { + assert_eq!(fp_text(1.0f64.to_bits()), "1.000000000000000000e+00"); + assert_eq!(fp_text(10.0f64.to_bits()), "1.000000000000000000e+01"); + assert_eq!(fp_text(0.125f64.to_bits()), "1.250000000000000000e-01"); + assert_eq!(fp_text((-2.0f64).to_bits()), "-2.000000000000000000e+00"); + } + + #[test] + fn objdump_style_drops_the_hex_prefix_on_targets() { + let mut i = Insn::new(Addr(0x1000), 4, "b", Flow::Branch(Addr(0x2000))); + i.push(Operand::Addr(Addr(0x2000))); + assert_eq!(format(&i, Style { objdump: true }), "b\t2000"); + assert_eq!(format(&i, Style::default()), "b\t0x2000"); + } +} diff --git a/crates/e5r-arch/src/arm/a32.rs b/crates/e5r-arch/src/arm/a32.rs new file mode 100644 index 0000000..ca567ad --- /dev/null +++ b/crates/e5r-arch/src/arm/a32.rs @@ -0,0 +1,802 @@ +//! A32 decode. +//! +//! The top-level split follows the architecture reference manual's A5 decode +//! tree on bits 27:25, and each group below is one of its tables. Condition +//! 15 is not a condition at all but a second instruction space, so it leaves +//! first. + +use e5r_core::Addr; + +use crate::insn::{AddrMode, Flow, Insn, Mem, Operand, Shift}; + +use super::text::{decimal, index, minus_zero}; +use super::{ + AL, BARRIERS, MSR_MASKS, bit, bits, cm, core_list, reg, sext, shift_by_reg, shifted, + so_imm_ops, vfp, wb_reg, +}; + +const DP: [&[&str; 16]; 16] = [ + conds!("and"), + conds!("eor"), + conds!("sub"), + conds!("rsb"), + conds!("add"), + conds!("adc"), + conds!("sbc"), + conds!("rsc"), + conds!("tst"), + conds!("teq"), + conds!("cmp"), + conds!("cmn"), + conds!("orr"), + conds!("mov"), + conds!("bic"), + conds!("mvn"), +]; + +const DP_S: [&[&str; 16]; 16] = [ + conds!("ands"), + conds!("eors"), + conds!("subs"), + conds!("rsbs"), + conds!("adds"), + conds!("adcs"), + conds!("sbcs"), + conds!("rscs"), + conds!("tst"), + conds!("teq"), + conds!("cmp"), + conds!("cmn"), + conds!("orrs"), + conds!("movs"), + conds!("bics"), + conds!("mvns"), +]; + +const SHIFTS: [&[&str; 16]; 4] = [conds!("lsl"), conds!("lsr"), conds!("asr"), conds!("ror")]; +const SHIFTS_S: [&[&str; 16]; 4] = [ + conds!("lsls"), + conds!("lsrs"), + conds!("asrs"), + conds!("rors"), +]; + +/// Decode one A32 word. +pub fn decode(w: u32, addr: Addr) -> Option { + let cond = bits(w, 31, 28); + if cond == 15 { + return unconditional(w, addr); + } + match bits(w, 27, 25) { + 0b000 => dp_misc(w, cond, addr), + 0b001 => dp_imm(w, cond, addr), + 0b010 => ldst(w, cond, addr, false), + 0b011 if bit(w, 4) == 0 => ldst(w, cond, addr, true), + 0b011 => media(w, cond, addr), + 0b100 => block(w, cond, addr), + 0b101 => branch(w, cond, addr), + // Coprocessors 10 and 11 are the floating point unit and have their + // own decoder; every other coprocessor is the generic space. + 0b110 => vfp::ldst(w, cond, addr, 4).or_else(|| super::coproc(w, cond, addr, false)), + _ => vfp::dp(w, cond, addr, 4).or_else(|| super::coproc(w, cond, addr, false)), + } +} + +fn at(addr: Addr, mn: &'static str, flow: Flow) -> Insn { + Insn::new(addr, 4, mn, flow) +} + +/// Table A5-2: the register data-processing space shares bits 27:25 with the +/// multiplies, the halfword loads and stores, and the branch-and-exchange +/// corner, and only bits 7:4 tell them apart. +fn dp_misc(w: u32, cond: u32, addr: Addr) -> Option { + let control = bits(w, 24, 20) & 0b11001 == 0b10000; + if bit(w, 4) == 0 { + return if control { + if bit(w, 7) == 0 { + misc(w, cond, addr) + } else { + None // halfword multiply and accumulate + } + } else { + dp_reg(w, cond, addr, None) + }; + } + if bit(w, 7) == 0 { + return if control { + misc(w, cond, addr) + } else { + dp_reg(w, cond, addr, Some(bits(w, 11, 8))) + }; + } + match bits(w, 7, 4) { + 0b1001 if bit(w, 24) == 0 => multiply(w, cond, addr), + 0b1001 => sync(w, cond, addr), + _ => extra_ldst(w, cond, addr), + } +} + +/// Data processing with a register source, optionally shifted by `by`. +fn dp_reg(w: u32, cond: u32, addr: Addr, by: Option) -> Option { + let op = bits(w, 24, 21) as usize; + let s = bit(w, 20) == 1; + let (rd, rn, rm) = (bits(w, 15, 12), bits(w, 19, 16), bits(w, 3, 0)); + let (ty, imm5) = (bits(w, 6, 5), bits(w, 11, 7)); + let table = if s { &DP_S } else { &DP }; + + // A `mov` whose source is shifted is spelled as the shift. + if op == 0b1101 { + let flow = pc_write(rd, rm, by.is_none() && ty == 0 && imm5 == 0); + let shift_mn = cm( + if s { + SHIFTS_S[ty as usize] + } else { + SHIFTS[ty as usize] + }, + cond, + ); + let mut i = match by { + Some(_) => at(addr, shift_mn, flow), + None if ty == 0 && imm5 == 0 => at(addr, cm(table[op], cond), flow), + None if ty == 3 && imm5 == 0 => at( + addr, + cm(if s { conds!("rrxs") } else { conds!("rrx") }, cond), + flow, + ), + None => at(addr, shift_mn, flow), + }; + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rm))); + match by { + Some(rs) => { + i.push(Operand::Reg(reg(rs))); + } + None if ty == 0 && imm5 == 0 => {} + None if ty == 3 && imm5 == 0 => {} + None => { + let (_, n) = super::decode_shift(ty, imm5); + i.push(Operand::Count(n as i64)); + } + } + return Some(i); + } + + let src = match by { + Some(_) => Operand::Reg(reg(rm)), + None => shifted(rm, ty, imm5), + }; + let mut i = at(addr, cm(table[op], cond), pc_write(rd, rm, false)); + if (0b1000..=0b1011).contains(&op) { + if !s { + return None; + } + i.push(Operand::Reg(reg(rn))); + } else if op == 0b1111 { + i.push(Operand::Reg(reg(rd))); + } else { + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rn))); + } + i.push(src); + if let Some(rs) = by { + i.push(Operand::Name(shift_by_reg(ty, rs))); + } + Some(i) +} + +/// Writing the pc is a branch, and moving the link register back into it is a +/// return. A predicated one keeps the branch and loses the fall-through, which +/// is the closest [`Flow`] can come to a conditional indirect transfer. +fn pc_write(rd: u32, rm: u32, plain_mov: bool) -> Flow { + match (rd, rm, plain_mov) { + (15, 14, true) => Flow::Return, + (15, _, _) => Flow::IndirectBranch, + _ => Flow::Next, + } +} + +/// Table A5-3: `bx`, `clz`, `mrs`, `msr` and the saturating adds. +fn misc(w: u32, cond: u32, addr: Addr) -> Option { + let op = bits(w, 22, 21); + let (rd, rm) = (bits(w, 15, 12), bits(w, 3, 0)); + match bits(w, 6, 4) { + 0b000 if bit(w, 9) == 0 && op & 1 == 0 => { + let mut i = at(addr, cm(conds!("mrs"), cond), Flow::Next); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Name(if bit(w, 22) == 1 { "spsr" } else { "apsr" })); + Some(i) + } + 0b000 if bit(w, 9) == 0 => { + let mask = bit(w, 22) << 4 | bits(w, 19, 16); + let mut i = at(addr, cm(conds!("msr"), cond), Flow::Next); + i.push(Operand::Name(MSR_MASKS[mask as usize])) + .push(Operand::Reg(reg(rm))); + Some(i) + } + 0b001 if op == 0b01 => { + let flow = if rm == 14 { + Flow::Return + } else { + Flow::IndirectBranch + }; + let mut i = at(addr, cm(conds!("bx"), cond), flow); + i.push(Operand::Reg(reg(rm))); + Some(i) + } + 0b001 if op == 0b11 => { + let mut i = at(addr, cm(conds!("clz"), cond), Flow::Next); + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rm))); + Some(i) + } + 0b010 if op == 0b01 => { + let mut i = at(addr, cm(conds!("bxj"), cond), Flow::IndirectBranch); + i.push(Operand::Reg(reg(rm))); + Some(i) + } + 0b011 if op == 0b01 => { + let mut i = at(addr, cm(conds!("blx"), cond), Flow::IndirectCall); + i.push(Operand::Reg(reg(rm))); + Some(i) + } + 0b101 => { + const SAT: [&[&str; 16]; 4] = [ + conds!("qadd"), + conds!("qsub"), + conds!("qdadd"), + conds!("qdsub"), + ]; + let mut i = at(addr, cm(SAT[op as usize], cond), Flow::Next); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rm))) + .push(Operand::Reg(reg(bits(w, 19, 16)))); + Some(i) + } + 0b111 if op == 0b01 => { + let imm = bits(w, 19, 8) << 4 | bits(w, 3, 0); + let mut i = at(addr, cm(conds!("bkpt"), cond), Flow::Trap); + i.push(Operand::UImm(imm as u64)); + Some(i) + } + _ => None, + } +} + +/// Table A5-4, minus the halfword multiplies: `mul` through `smlal`. +fn multiply(w: u32, cond: u32, addr: Addr) -> Option { + let s = bit(w, 20) == 1; + let (rd, ra, rm, rn) = ( + bits(w, 19, 16), + bits(w, 15, 12), + bits(w, 11, 8), + bits(w, 3, 0), + ); + let long = |t: &'static [&str; 16], ts: &'static [&str; 16]| { + let mut i = at(addr, cm(if s { ts } else { t }, cond), Flow::Next); + i.push(Operand::Reg(reg(ra))) + .push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rm))); + i + }; + Some(match bits(w, 23, 21) { + 0b000 => { + let mut i = at( + addr, + cm(if s { conds!("muls") } else { conds!("mul") }, cond), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rm))); + i + } + 0b001 => { + let mut i = at( + addr, + cm(if s { conds!("mlas") } else { conds!("mla") }, cond), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rm))) + .push(Operand::Reg(reg(ra))); + i + } + 0b010 if !s => long(conds!("umaal"), conds!("umaal")), + 0b011 if !s => { + let mut i = at(addr, cm(conds!("mls"), cond), Flow::Next); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rm))) + .push(Operand::Reg(reg(ra))); + i + } + 0b100 => long(conds!("umull"), conds!("umulls")), + 0b101 => long(conds!("umlal"), conds!("umlals")), + 0b110 => long(conds!("smull"), conds!("smulls")), + 0b111 => long(conds!("smlal"), conds!("smlals")), + _ => return None, + }) +} + +/// The load-exclusive family; the rest of table A5-5 is left alone. +fn sync(w: u32, cond: u32, addr: Addr) -> Option { + let (rn, rd, rt) = (bits(w, 19, 16), bits(w, 15, 12), bits(w, 3, 0)); + let (mn, load, size) = match bits(w, 23, 20) { + 0b1000 => (cm(conds!("strex"), cond), false, 4), + 0b1001 => (cm(conds!("ldrex"), cond), true, 4), + 0b1100 => (cm(conds!("strexb"), cond), false, 1), + 0b1101 => (cm(conds!("ldrexb"), cond), true, 1), + 0b1110 => (cm(conds!("strexh"), cond), false, 2), + 0b1111 => (cm(conds!("ldrexh"), cond), true, 2), + _ => return None, + }; + let mem = Operand::Mem(Mem::base_disp(reg(rn), 0, size)); + let mut i = at(addr, mn, Flow::Next); + if load { + i.push(Operand::Reg(reg(rd))).push(mem); + } else { + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rt))) + .push(mem); + } + Some(i) +} + +/// Table A5-10: the halfword, signed-byte and doubleword accesses, whose +/// displacements are eight bits and print in decimal. +fn extra_ldst(w: u32, cond: u32, addr: Addr) -> Option { + let (p, u, imm, wb, l) = ( + bit(w, 24) == 1, + bit(w, 23) == 1, + bit(w, 22) == 1, + bit(w, 21) == 1, + bit(w, 20) == 1, + ); + if !p && wb { + return None; // the unprivileged forms + } + let (rn, rt) = (bits(w, 19, 16), bits(w, 15, 12)); + let (mn, size, pair) = match (bits(w, 6, 5), l) { + (0b01, false) => (cm(conds!("strh"), cond), 2, false), + (0b01, true) => (cm(conds!("ldrh"), cond), 2, false), + (0b10, false) => (cm(conds!("ldrd"), cond), 8, true), + (0b10, true) => (cm(conds!("ldrsb"), cond), 1, false), + (0b11, false) => (cm(conds!("strd"), cond), 8, true), + _ => (cm(conds!("ldrsh"), cond), 2, false), + }; + if pair && rt & 1 == 1 { + return None; + } + let mode = match (p, wb) { + (false, _) => AddrMode::PostIndex, + (true, true) => AddrMode::PreIndex, + (true, false) => AddrMode::Offset, + }; + let mut m = Mem { + seg: None, + base: Some(reg(rn)), + index: None, + disp: 0, + mode, + size, + }; + if imm { + let off = (bits(w, 11, 8) << 4 | bits(w, 3, 0)) as i64; + m.disp = if u { off } else { -off }; + if !u && off == 0 { + m = minus_zero(m); + } + } else { + if !u { + return None; // a subtracted index has nowhere to live in `Mem` + } + m.index = index(reg(bits(w, 3, 0)), 0); + } + let mut i = at(addr, mn, Flow::Next); + i.push(Operand::Reg(reg(rt))); + if pair { + i.push(Operand::Reg(reg(rt + 1))); + } + i.push(Operand::Mem(decimal(m))); + Some(i) +} + +/// Table A5-6, plus `movw`, `movt` and the hints that share its slots. +fn dp_imm(w: u32, cond: u32, addr: Addr) -> Option { + let (rd, rn) = (bits(w, 15, 12), bits(w, 19, 16)); + match bits(w, 24, 20) { + 0b10000 | 0b10100 => { + let imm = bits(w, 19, 16) << 12 | bits(w, 11, 0); + let wide = bit(w, 22) == 1; + let mut i = at( + addr, + cm(if wide { conds!("movt") } else { conds!("movw") }, cond), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::UImm(imm as u64)); + Some(i) + } + 0b10010 if rn == 0 && rd == 0b1111 => { + const HINTS: [&[&str; 16]; 5] = [ + conds!("nop"), + conds!("yield"), + conds!("wfe"), + conds!("wfi"), + conds!("sev"), + ]; + let n = bits(w, 7, 0) as usize; + (n < HINTS.len()).then(|| at(addr, cm(HINTS[n], cond), Flow::Next)) + } + 0b10010 | 0b10110 => { + let mask = bit(w, 22) << 4 | rn; + let mut i = at(addr, cm(conds!("msr"), cond), Flow::Next); + let (v, rot) = so_imm_ops(bits(w, 11, 0), true); + i.push(Operand::Name(MSR_MASKS[mask as usize])).push(v); + if let Some(rot) = rot { + i.push(rot); + } + Some(i) + } + _ => { + let op = bits(w, 24, 21) as usize; + let s = bit(w, 20) == 1; + let table = if s { &DP_S } else { &DP }; + // A `mov` to the pc reads better as a bit pattern than as a + // negative number, which is the one place LLVM prints it unsigned. + let (value, rot) = so_imm_ops(bits(w, 11, 0), op == 0b1101 && rd == 15); + let flow = if rd == 15 && !(0b1000..=0b1011).contains(&op) { + Flow::IndirectBranch + } else { + Flow::Next + }; + let mut i = at(addr, cm(table[op], cond), flow); + match op { + 0b1000..=0b1011 => { + i.push(Operand::Reg(reg(rn))); + } + 0b1101 | 0b1111 => { + i.push(Operand::Reg(reg(rd))); + } + _ => { + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rn))); + } + } + i.push(value); + if let Some(rot) = rot { + i.push(rot); + } + Some(i) + } + } +} + +/// Tables A5-15 and A5-16: the word and byte accesses. +fn ldst(w: u32, cond: u32, addr: Addr, reg_form: bool) -> Option { + let (p, u, b, wb, l) = ( + bit(w, 24) == 1, + bit(w, 23) == 1, + bit(w, 22) == 1, + bit(w, 21) == 1, + bit(w, 20) == 1, + ); + // `P == 0 && W == 1` is the unprivileged form, which accesses memory with + // the permissions of user mode however privileged the caller is. It is a + // post-index by construction and an operating system's own instruction: + // the one it copies a user pointer with. + let unprivileged = !p && wb; + let (rn, rt) = (bits(w, 19, 16), bits(w, 15, 12)); + let mode = match (p, wb) { + (false, _) => AddrMode::PostIndex, + (true, true) => AddrMode::PreIndex, + (true, false) => AddrMode::Offset, + }; + let mut m = Mem { + seg: None, + base: Some(reg(rn)), + index: None, + disp: 0, + mode, + size: if b { 1 } else { 4 }, + }; + if reg_form { + if !u || bits(w, 6, 5) != 0 { + return None; // a subtracted or non-`lsl` index has nowhere to live + } + m.index = index(reg(bits(w, 3, 0)), bits(w, 11, 7) as u8); + } else { + let off = bits(w, 11, 0) as i64; + m.disp = if u { off } else { -off }; + if !u && off == 0 && mode != AddrMode::Offset { + m = minus_zero(m); + } + } + if mode == AddrMode::PostIndex { + m = decimal(m); + } + let mn = match (unprivileged, l, b) { + (false, true, false) => cm(conds!("ldr"), cond), + (false, true, true) => cm(conds!("ldrb"), cond), + (false, false, false) => cm(conds!("str"), cond), + (false, false, true) => cm(conds!("strb"), cond), + (true, true, false) => cm(conds!("ldrt"), cond), + (true, true, true) => cm(conds!("ldrbt"), cond), + (true, false, false) => cm(conds!("strt"), cond), + (true, false, true) => cm(conds!("strbt"), cond), + }; + let flow = if l && rt == 15 { + Flow::IndirectBranch + } else { + Flow::Next + }; + let mut i = at(addr, mn, flow); + i.push(Operand::Reg(reg(rt))).push(Operand::Mem(m)); + Some(i) +} + +/// Table A5-13: the extends, the bitfields, `rev`, the divides and `udf`. +fn media(w: u32, cond: u32, addr: Addr) -> Option { + let (op1, op2) = (bits(w, 24, 20), bits(w, 7, 5)); + let (rd, rn, rm) = (bits(w, 15, 12), bits(w, 19, 16), bits(w, 3, 0)); + match (op1, op2) { + (0b11111, 0b111) => { + let imm = bits(w, 19, 8) << 4 | rm; + let mut i = at(addr, cm(conds!("udf"), cond), Flow::Trap); + i.push(Operand::UImm(imm as u64)); + Some(i) + } + (0b11010 | 0b11011 | 0b11110 | 0b11111, 0b010 | 0b110) => { + let signed = op1 < 0b11110; + let mut i = at( + addr, + cm( + if signed { + conds!("sbfx") + } else { + conds!("ubfx") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rm))) + .push(Operand::UImm(bits(w, 11, 7) as u64)) + .push(Operand::UImm(bits(w, 20, 16) as u64 + 1)); + Some(i) + } + (0b11100 | 0b11101, 0b000 | 0b100) => { + let lsb = bits(w, 11, 7); + let width = bits(w, 20, 16) as i64 - lsb as i64 + 1; + if width < 1 { + return None; + } + let mut i = at( + addr, + cm( + if rm == 15 { + conds!("bfc") + } else { + conds!("bfi") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))); + if rm != 15 { + i.push(Operand::Reg(reg(rm))); + } + i.push(Operand::Count(lsb as i64)) + .push(Operand::Count(width)); + Some(i) + } + (0b10001 | 0b10011, 0b000) => { + let mut i = at( + addr, + cm( + if op1 == 0b10001 { + conds!("sdiv") + } else { + conds!("udiv") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rm))) + .push(Operand::Reg(reg(bits(w, 11, 8)))); + Some(i) + } + // `ssat` and `usat` clamp a shifted value into a signed or unsigned + // field of a chosen width. The saturate-position field is one more + // than the encoding holds for the signed form and exactly it for the + // unsigned one, which is the architecture's own asymmetry. + (0b01010 | 0b01011 | 0b01110 | 0b01111, _) if bits(w, 5, 4) == 0b01 => { + let signed = op1 < 0b01110; + let sat = if signed { rn + 1 } else { rn }; + let shift = bits(w, 11, 7); + let asr = bit(w, 6) == 1; + let mut i = at( + addr, + cm( + if signed { + conds!("ssat") + } else { + conds!("usat") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Count(sat as i64)) + .push(Operand::Reg(reg(rm))); + if shift != 0 || asr { + let (kind, n) = if asr { + (Shift::Asr, if shift == 0 { 32 } else { shift as u8 }) + } else { + (Shift::Lsl, shift as u8) + }; + i.push(Operand::ShiftOp(kind, n)); + } + Some(i) + } + (0b01011, 0b001) | (0b01011, 0b101) | (0b01111, 0b001) | (0b01111, 0b101) + if rn == 15 && bits(w, 11, 8) == 15 => + { + let mn = match (op1, op2) { + (0b01011, 0b001) => conds!("rev"), + (0b01011, 0b101) => conds!("rev16"), + (0b01111, 0b001) => conds!("rbit"), + _ => conds!("revsh"), + }; + let mut i = at(addr, cm(mn, cond), Flow::Next); + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rm))); + Some(i) + } + (0b01000 | 0b01010 | 0b01011 | 0b01100 | 0b01110 | 0b01111, 0b011) + if bits(w, 9, 8) == 0 => + { + let plain = rn == 15; + let mn = match (op1, plain) { + (0b01000, true) => conds!("sxtb16"), + (0b01000, false) => conds!("sxtab16"), + (0b01010, true) => conds!("sxtb"), + (0b01010, false) => conds!("sxtab"), + (0b01011, true) => conds!("sxth"), + (0b01011, false) => conds!("sxtah"), + (0b01100, true) => conds!("uxtb16"), + (0b01100, false) => conds!("uxtab16"), + (0b01110, true) => conds!("uxtb"), + (0b01110, false) => conds!("uxtab"), + (0b01111, true) => conds!("uxth"), + _ => conds!("uxtah"), + }; + let mut i = at(addr, cm(mn, cond), Flow::Next); + i.push(Operand::Reg(reg(rd))); + if !plain { + i.push(Operand::Reg(reg(rn))); + } + i.push(Operand::Reg(reg(rm))); + let rotate = bits(w, 11, 10); + if rotate != 0 { + i.push(Operand::Name(ROTATES[rotate as usize])); + } + Some(i) + } + _ => None, + } +} + +/// The four rotate amounts an extend can apply, which print after its source. +const ROTATES: [&str; 4] = ["ror #0", "ror #8", "ror #16", "ror #24"]; + +/// Table A5-21: load and store multiple, and the `push` and `pop` spellings +/// LLVM prefers for the two that use the stack pointer. +fn block(w: u32, cond: u32, addr: Addr) -> Option { + if bit(w, 22) == 1 { + return None; // the exception-return forms + } + let (p, u, wb, l) = (bit(w, 24), bit(w, 23), bit(w, 21) == 1, bit(w, 20) == 1); + let rn = bits(w, 19, 16); + let list = bits(w, 15, 0); + if list == 0 { + return None; + } + let stack = rn == 13 && wb && list.count_ones() >= 2; + let flow = if l && list >> 15 == 1 { + if rn == 13 { + Flow::Return + } else { + Flow::IndirectBranch + } + } else { + Flow::Next + }; + const LDM: [&[&str; 16]; 4] = [ + conds!("ldmda"), + conds!("ldm"), + conds!("ldmdb"), + conds!("ldmib"), + ]; + const STM: [&[&str; 16]; 4] = [ + conds!("stmda"), + conds!("stm"), + conds!("stmdb"), + conds!("stmib"), + ]; + let kind = (p << 1 | u) as usize; + let mut i = match (l, kind, stack) { + (true, 0b01, true) => at(addr, cm(conds!("pop"), cond), flow), + (false, 0b10, true) => at(addr, cm(conds!("push"), cond), flow), + (true, _, _) => at(addr, cm(LDM[kind], cond), flow), + (false, _, _) => at(addr, cm(STM[kind], cond), flow), + }; + if !stack || !matches!((l, kind), (true, 0b01) | (false, 0b10)) { + if wb { + i.push(Operand::Name(wb_reg(rn))); + } else { + i.push(Operand::Reg(reg(rn))); + } + } + i.push(core_list(list)); + Some(i) +} + +fn branch(w: u32, cond: u32, addr: Addr) -> Option { + let target = super::target(addr, 8 + sext(bits(w, 23, 0), 24) * 4); + let (mn, flow) = if bit(w, 24) == 1 { + (cm(conds!("bl"), cond), Flow::Call(target)) + } else if cond == AL { + (cm(conds!("b"), cond), Flow::Branch(target)) + } else { + (cm(conds!("b"), cond), Flow::CondBranch(target)) + }; + let mut i = at(addr, mn, flow); + i.push(Operand::Addr(target)); + Some(i) +} + +/// Condition 15 is a second instruction space: the barriers, `pld`, and the +/// `blx` that changes instruction set. +fn unconditional(w: u32, addr: Addr) -> Option { + if bits(w, 27, 25) == 0b101 { + let off = sext(bits(w, 23, 0), 24) * 4 + (bit(w, 24) as i64) * 2; + let target = super::target(addr, 8 + off); + let mut i = at(addr, "blx", Flow::Call(target)); + i.push(Operand::Addr(target)); + return Some(i); + } + if bits(w, 27, 4) == 0x57ff0 { + let option = Operand::Name(BARRIERS[bits(w, 3, 0) as usize]); + let mn = match bits(w, 7, 4) { + 0b0100 => "dsb", + 0b0101 => "dmb", + 0b0110 => "isb", + 0b0001 => return Some(at(addr, "clrex", Flow::Next)), + _ => return None, + }; + let mut i = at(addr, mn, Flow::Next); + i.push(option); + return Some(i); + } + if bits(w, 27, 26) == 0b01 && bit(w, 24) == 1 && bits(w, 15, 12) == 0b1111 && bit(w, 4) == 0 { + // pld and pli, whose only operand is the address they touch. + let mn = match (bit(w, 22), bit(w, 25)) { + (0, 0) => "pld", + (1, 0) => "pldw", + _ => return None, + }; + let off = bits(w, 11, 0) as i64; + let m = Mem::base_disp( + reg(bits(w, 19, 16)), + if bit(w, 23) == 1 { off } else { -off }, + 1, + ); + let mut i = at(addr, mn, Flow::Next); + i.push(Operand::Mem(m)); + return Some(i); + } + None +} diff --git a/crates/e5r-arch/src/arm/mod.rs b/crates/e5r-arch/src/arm/mod.rs new file mode 100644 index 0000000..9cfcb30 --- /dev/null +++ b/crates/e5r-arch/src/arm/mod.rs @@ -0,0 +1,514 @@ +//! ARM A32 and Thumb-2 decoder. +//! +//! A32 is fixed 32-bit and decodes from a word and an address, the way A64 +//! does. T32 does not: an instruction is two or four bytes, and inside an IT +//! block it is conditional with nothing in its own halfwords saying so. +//! [`ItState`] carries that across a block and [`Mode`] is how a caller says +//! which instruction set an address holds, because the bytes never say. +//! +//! The condition is part of the mnemonic in every ARM listing, so mnemonics +//! come from per-opcode tables of sixteen spellings rather than from a suffix +//! glued on at print time: [`Insn::mnemonic`] is a `&'static str`. + +// Encoding literals are grouped the way the architecture reference manual draws +// them, which is what makes them checkable against the manual. +#![allow(clippy::unusual_byte_groupings)] + +use e5r_core::Addr; + +use crate::insn::{AddrMode, Flow, Insn, Mem, Operand, Reg, RegClass, Shift, Width}; + +/// Bits `hi..=lo` of `w`. +#[inline] +pub(crate) const fn bits(w: u32, hi: u32, lo: u32) -> u32 { + (w >> lo) & ((1u32 << (hi - lo + 1)) - 1) +} + +/// Bit `n` of `w`. +#[inline] +pub(crate) const fn bit(w: u32, n: u32) -> u32 { + (w >> n) & 1 +} + +/// Sign-extend the low `n` bits of `v`. +#[inline] +pub(crate) const fn sext(v: u32, n: u32) -> i64 { + let s = 64 - n; + ((v as u64) << s) as i64 >> s +} + +/// The sixteen spellings of one mnemonic: `eq` through `le`, then `al`, which +/// is spelled by leaving the mnemonic alone, then the condition that has no +/// name, which only an IT block with an unpredictable first condition can +/// produce. A condition that prints inside the mnemonic and an `&'static str` +/// mnemonic together mean every opcode needs its own table; `concat!` builds +/// them at compile time. +macro_rules! conds { + ($p:literal) => { + conds!($p, "") + }; + ($p:literal, $s:literal) => { + &[ + concat!($p, "eq", $s), + concat!($p, "ne", $s), + concat!($p, "hs", $s), + concat!($p, "lo", $s), + concat!($p, "mi", $s), + concat!($p, "pl", $s), + concat!($p, "vs", $s), + concat!($p, "vc", $s), + concat!($p, "hi", $s), + concat!($p, "ls", $s), + concat!($p, "ge", $s), + concat!($p, "lt", $s), + concat!($p, "gt", $s), + concat!($p, "le", $s), + concat!($p, $s), + concat!($p, "", $s), + ] + }; +} + +/// `lsl r3` and the other 63: a shift by a register is not an operand kind, so +/// it rides as a name appended after the register it shifts. +macro_rules! shift_regs { + ($s:literal) => { + [ + concat!($s, " r0"), + concat!($s, " r1"), + concat!($s, " r2"), + concat!($s, " r3"), + concat!($s, " r4"), + concat!($s, " r5"), + concat!($s, " r6"), + concat!($s, " r7"), + concat!($s, " r8"), + concat!($s, " r9"), + concat!($s, " r10"), + concat!($s, " r11"), + concat!($s, " r12"), + concat!($s, " sp"), + concat!($s, " lr"), + concat!($s, " pc"), + ] + }; +} + +pub mod a32; +pub mod t32; +pub mod text; +pub mod vfp; + +pub use t32::ItState; +pub use text::format; + +/// The `al` encoding, which prints as no suffix at all. +pub(crate) const AL: u32 = 14; + +/// Pick a mnemonic's spelling for `cond` out of a [`conds!`] table. +#[inline] +pub(crate) fn cm(t: &'static [&'static str; 16], cond: u32) -> &'static str { + t[(cond as usize) & 15] +} + +const SHIFT_BY_REG: [[&str; 16]; 4] = [ + shift_regs!("lsl"), + shift_regs!("lsr"), + shift_regs!("asr"), + shift_regs!("ror"), +]; + +/// The condition names as a listing spells them on their own, where `al` is +/// written out: the operand of an `it`. +pub(crate) const COND_NAMES: [&str; 16] = [ + "eq", "ne", "hs", "lo", "mi", "pl", "vs", "vc", "hi", "ls", "ge", "lt", "gt", "le", "al", "nv", +]; + +/// One of the sixteen core registers. Thirteen and fifteen get their own +/// classes so that stack and pc-relative analysis works without a table. +#[inline] +pub(crate) fn reg(n: u32) -> Reg { + match n & 15 { + 13 => Reg { + class: RegClass::Sp, + num: 13, + width: Width::W32, + }, + 15 => Reg { + class: RegClass::Pc, + num: 15, + width: Width::W32, + }, + n => Reg::gpr(n as u8, Width::W32), + } +} + +/// A single or double precision VFP register. +#[inline] +pub(crate) fn vreg(n: u32, double: bool) -> Reg { + Reg::vec(n as u8, if double { Width::W64 } else { Width::W32 }) +} + +/// A core register list rides in [`Operand::Sys`], the one operand whose +/// meaning its decoder owns. Tag zero is a sixteen-bit core mask. +#[inline] +pub(crate) fn core_list(mask: u32) -> Operand { + Operand::Sys(mask & 0xffff) +} + +/// The sixteen coprocessor numbers and the sixteen coprocessor registers, as +/// a listing spells them. +const PN: [&str; 16] = [ + "p0", "p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9", "p10", "p11", "p12", "p13", "p14", + "p15", +]; +const CN: [&str; 16] = [ + "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13", "c14", + "c15", +]; + +/// The generic coprocessor instructions, which A32 and T32 encode identically. +/// +/// `cdp`, `mcr`, `mrc`, `mcrr`, `mrrc`, `ldc` and `stc` move data and commands +/// between the core and a coprocessor. Coprocessors 10 and 11 are the floating +/// point unit and have their own decoder; everything else is this, and it is +/// what a system, a debug unit or a cache controller is driven through. The +/// `2` forms are the same instructions in the unconditional space, which is a +/// second instruction space rather than a condition. +/// +/// `len` is 4 either way: the T32 encodings are the A32 word unchanged, which +/// is why one decoder serves both. +pub(crate) fn coproc(w: u32, cond: u32, addr: Addr, two: bool) -> Option { + fn at(addr: Addr, mn: &'static str, flow: Flow) -> Insn { + Insn::new(addr, 4, mn, flow) + } + let coproc = bits(w, 11, 8); + // The floating point unit's own space, decoded elsewhere. + if coproc == 10 || coproc == 11 { + return None; + } + let p = PN[coproc as usize]; + let group = bits(w, 27, 24); + let mut i = if group == 0b1110 && bit(w, 4) == 1 { + // `mcr` writes a core register to the coprocessor, `mrc` reads one. + let load = bit(w, 20) == 1; + let mn = match (load, two) { + (true, false) => cm(conds!("mrc"), cond), + (false, false) => cm(conds!("mcr"), cond), + (true, true) => "mrc2", + (false, true) => "mcr2", + }; + let mut i = at(addr, mn, Flow::Next); + i.push(Operand::Name(p)) + .push(Operand::Count(bits(w, 23, 21) as i64)) + .push(Operand::Reg(reg(bits(w, 15, 12)))) + .push(Operand::Name(CN[bits(w, 19, 16) as usize])) + .push(Operand::Name(CN[bits(w, 3, 0) as usize])) + .push(Operand::Count(bits(w, 7, 5) as i64)); + i + } else if group == 0b1110 { + let mn = if two { "cdp2" } else { cm(conds!("cdp"), cond) }; + let mut i = at(addr, mn, Flow::Next); + i.push(Operand::Name(p)) + .push(Operand::Count(bits(w, 23, 20) as i64)) + .push(Operand::Name(CN[bits(w, 15, 12) as usize])) + .push(Operand::Name(CN[bits(w, 19, 16) as usize])) + .push(Operand::Name(CN[bits(w, 3, 0) as usize])) + .push(Operand::Count(bits(w, 7, 5) as i64)); + i + } else if group == 0b1100 && bits(w, 23, 21) == 0b010 { + // The two-register forms, which move a 64-bit value in one go. + let load = bit(w, 20) == 1; + let mn = match (load, two) { + (true, false) => cm(conds!("mrrc"), cond), + (false, false) => cm(conds!("mcrr"), cond), + (true, true) => "mrrc2", + (false, true) => "mcrr2", + }; + let mut i = at(addr, mn, Flow::Next); + i.push(Operand::Name(p)) + .push(Operand::Count(bits(w, 7, 4) as i64)) + .push(Operand::Reg(reg(bits(w, 15, 12)))) + .push(Operand::Reg(reg(bits(w, 19, 16)))) + .push(Operand::Name(CN[bits(w, 3, 0) as usize])); + i + } else if matches!(group, 0b1100 | 0b1101) { + // `ldc` and `stc`, which move words to and from memory. The `l` suffix + // is the long form, which the D bit selects. + let load = bit(w, 20) == 1; + let long = bit(w, 22) == 1; + let mn = match (load, long, two) { + (true, false, false) => cm(conds!("ldc"), cond), + (true, true, false) => cm(conds!("ldc", "l"), cond), + (false, false, false) => cm(conds!("stc"), cond), + (false, true, false) => cm(conds!("stc", "l"), cond), + (true, false, true) => "ldc2", + (true, true, true) => "ldc2l", + (false, false, true) => "stc2", + (false, true, true) => "stc2l", + }; + let (p_bit, u, wb) = (bit(w, 24) == 1, bit(w, 23) == 1, bit(w, 21) == 1); + if !p_bit && !wb && !u { + // Neither indexed nor written back, which is the unindexed form + // this does not model rather than a form to invent. + return None; + } + let mode = match (p_bit, wb) { + (false, _) => AddrMode::PostIndex, + (true, true) => AddrMode::PreIndex, + (true, false) => AddrMode::Offset, + }; + let off = bits(w, 7, 0) as i64 * 4; + let m = Mem { + seg: None, + base: Some(reg(bits(w, 19, 16))), + index: None, + disp: if u { off } else { -off }, + mode, + size: 4, + }; + let mut i = at(addr, mn, Flow::Next); + i.push(Operand::Name(p)) + .push(Operand::Name(CN[bits(w, 15, 12) as usize])) + .push(Operand::Mem(m)); + i + } else { + return None; + }; + i.len = 4; + Some(i) +} + +/// A VFP list is always a run: tag 1 single, tag 2 double, first register in +/// bits 15:8 and the count in bits 7:0. +#[inline] +pub(crate) fn vfp_list(double: bool, first: u32, count: u32) -> Operand { + Operand::Sys((if double { 2 } else { 1 }) << 28 | (first & 0xff) << 8 | (count & 0xff)) +} + +/// The A32 modified immediate: an eight-bit value rotated right by twice the +/// four-bit field above it. +#[inline] +pub(crate) fn so_imm(imm12: u32) -> u32 { + bits(imm12, 7, 0).rotate_right(bits(imm12, 11, 8) * 2) +} + +/// How LLVM spells one. It prints the value when the encoding uses the +/// smallest rotate that can name it, and the encoded `#bits, #rot` pair when +/// it does not, because only the pair assembles back to these bits. The value +/// is signed except where a listing would rather read the bit pattern. +pub(crate) fn so_imm_ops(imm12: u32, unsigned: bool) -> (Operand, Option) { + let value = so_imm(imm12); + if canonical_so_imm(value) == Some(imm12) { + let v = if unsigned { + value as i64 + } else { + value as i32 as i64 + }; + return (Operand::Count(v), None); + } + ( + Operand::Count(bits(imm12, 7, 0) as i64), + Some(Operand::Count(bits(imm12, 11, 8) as i64 * 2)), + ) +} + +/// The canonical encoding of a value, if a single rotate can name it. +fn canonical_so_imm(v: u32) -> Option { + if v <= 0xff { + return Some(v); + } + let amt = so_imm_rotate(v); + ((!255u32).rotate_right(amt) & v == 0).then(|| v.rotate_left(amt) | (amt >> 1) << 8) +} + +/// The smallest even rotate that brings a value into eight bits, with the +/// retry LLVM does for patterns like `0xf000000f`. +fn so_imm_rotate(v: u32) -> u32 { + if v & !255 == 0 { + return 0; + } + let amt = v.trailing_zeros() & !1; + if v.rotate_right(amt) & !255 == 0 { + return (32 - amt) & 31; + } + if v & 63 != 0 { + let amt = (v & !63).trailing_zeros() & !1; + if v.rotate_right(amt) & !255 == 0 { + return (32 - amt) & 31; + } + } + 0 +} + +/// `ThumbExpandImm`, which spends four of its twelve bits on byte-replication +/// patterns before falling back to a rotate. +pub(crate) fn t2_imm(v: u32) -> u32 { + let b = bits(v, 7, 0); + if bits(v, 11, 10) == 0 { + match bits(v, 9, 8) { + 0 => b, + 1 => (b << 16) | b, + 2 => (b << 24) | (b << 8), + _ => (b << 24) | (b << 16) | (b << 8) | b, + } + } else { + (0x80 | bits(v, 6, 0)).rotate_right(v >> 7) + } +} + +/// ARM addresses are thirty-two bits, so a branch that runs off either end +/// wraps there and not in the sixty-four-bit [`Addr`]. +#[inline] +pub(crate) fn target(addr: Addr, off: i64) -> Addr { + Addr((addr.get() as u32).wrapping_add(off as u32) as u64) +} + +/// The shift a two-bit type and a five-bit amount name. A zero amount means 32 +/// for `lsr` and `asr`, and `rrx` for `ror`, which is [`Shift::Ror`] by zero. +pub(crate) fn decode_shift(ty: u32, imm5: u32) -> (Shift, u8) { + match ty { + 0 => (Shift::Lsl, imm5 as u8), + 1 => (Shift::Lsr, if imm5 == 0 { 32 } else { imm5 as u8 }), + 2 => (Shift::Asr, if imm5 == 0 { 32 } else { imm5 as u8 }), + _ => (Shift::Ror, imm5 as u8), + } +} + +/// A register with an immediate shift, as a data-processing source. `lsl #0` +/// is no shift at all and prints as the bare register. +pub(crate) fn shifted(rm: u32, ty: u32, imm5: u32) -> Operand { + if ty == 0 && imm5 == 0 { + return Operand::Reg(reg(rm)); + } + let (sh, n) = decode_shift(ty, imm5); + Operand::Shifted(reg(rm), sh, n) +} + +/// The name for a shift by a register, to follow the register it shifts. +pub(crate) fn shift_by_reg(ty: u32, rs: u32) -> &'static str { + SHIFT_BY_REG[(ty & 3) as usize][(rs & 15) as usize] +} + +/// A base register with writeback, `r1!`, which is not an operand kind either. +const WB_REGS: [&str; 16] = [ + "r0!", "r1!", "r2!", "r3!", "r4!", "r5!", "r6!", "r7!", "r8!", "r9!", "r10!", "r11!", "r12!", + "sp!", "lr!", "pc!", +]; + +/// The spelling of a base register that the instruction writes back. +#[inline] +pub(crate) fn wb_reg(n: u32) -> &'static str { + WB_REGS[(n & 15) as usize] +} + +/// Barrier options by their four-bit field; the unnamed ones print as numbers. +pub(crate) const BARRIERS: [&str; 16] = [ + "#0x0", "oshld", "oshst", "osh", "#0x4", "nshld", "nshst", "nsh", "#0x8", "ishld", "ishst", + "ish", "#0xc", "ld", "st", "sy", +]; + +/// The status register field spellings of a `msr` mask, `SPSR` above `CPSR`. +pub(crate) const MSR_MASKS: [&str; 32] = [ + "CPSR", + "CPSR_c", + "CPSR_x", + "CPSR_xc", + "APSR_g", + "CPSR_sc", + "CPSR_sx", + "CPSR_sxc", + "APSR_nzcvq", + "CPSR_fc", + "CPSR_fx", + "CPSR_fxc", + "APSR_nzcvqg", + "CPSR_fsc", + "CPSR_fsx", + "CPSR_fsxc", + "SPSR", + "SPSR_c", + "SPSR_x", + "SPSR_xc", + "SPSR_s", + "SPSR_sc", + "SPSR_sx", + "SPSR_sxc", + "SPSR_f", + "SPSR_fc", + "SPSR_fx", + "SPSR_fxc", + "SPSR_fs", + "SPSR_fsc", + "SPSR_fsx", + "SPSR_fsxc", +]; + +/// Which instruction set an address holds. Nothing in the bytes says, so the +/// caller does; in an ELF the `$a` and `$t` mapping symbols are where it comes +/// from, and the low bit of a branch target is where it changes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Mode { + /// A32, four bytes per instruction. + A32, + /// T32, two or four. + T32, +} + +/// Decode one A32 instruction. +pub fn decode(bytes: &[u8], addr: Addr) -> Option { + let w = u32::from_le_bytes(bytes.get(..4)?.try_into().ok()?); + decode_word(w, addr) +} + +/// Decode one A32 instruction from its word, the way A64 does. +pub fn decode_word(w: u32, addr: Addr) -> Option { + a32::decode(w, addr) +} + +/// Decode one T32 instruction under `it`, and report the IT state the next one +/// runs under. +pub fn decode_thumb(bytes: &[u8], addr: Addr, it: ItState) -> Option<(Insn, ItState)> { + t32::decode(bytes, addr, it) +} + +/// Decode in either instruction set. The IT state is ignored in A32, which has +/// a condition field in every word instead. +pub fn decode_in(mode: Mode, bytes: &[u8], addr: Addr, it: ItState) -> Option<(Insn, ItState)> { + match mode { + Mode::A32 => decode(bytes, addr).map(|i| (i, ItState::default())), + Mode::T32 => decode_thumb(bytes, addr, it), + } +} + +/// A cursor over a run of T32 instructions, which is the only way to decode +/// one correctly: the IT state comes from the instructions before it. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct Thumb { + it: ItState, +} + +impl Thumb { + /// A cursor outside any IT block. + pub fn new() -> Thumb { + Thumb::default() + } + + /// The IT state the next instruction will run under. + pub fn state(self) -> ItState { + self.it + } + + /// Resume at a point whose IT state is known, or reset at a point where it + /// is not: a branch target is never inside an IT block. + pub fn resume(&mut self, it: ItState) { + self.it = it; + } + + /// Decode the next instruction and advance the IT state over it. + pub fn decode(&mut self, bytes: &[u8], addr: Addr) -> Option { + let (i, next) = t32::decode(bytes, addr, self.it)?; + self.it = next; + Some(i) + } +} diff --git a/crates/e5r-arch/src/arm/t32.rs b/crates/e5r-arch/src/arm/t32.rs new file mode 100644 index 0000000..208a942 --- /dev/null +++ b/crates/e5r-arch/src/arm/t32.rs @@ -0,0 +1,1547 @@ +//! T32 decode, and the IT state it cannot be done without. +//! +//! Sixteen-bit encodings come first; a halfword whose top five bits are +//! `11101`, `11110` or `11111` starts a thirty-two-bit one instead. The `.w` +//! suffix on the wide spellings is not decoration: LLVM prints it exactly +//! where a narrow encoding of the same operation exists, so it is part of the +//! mnemonic table and not something the formatter can add. + +use e5r_core::Addr; + +use crate::insn::{AddrMode, Cond, Flow, Insn, Mem, Operand}; + +use super::text::{always, decimal, index, minus_zero}; +use super::{AL, bit, bits, cm, core_list, reg, sext, shifted, t2_imm, vfp, wb_reg}; + +/// The eight ITSTATE bits: the block's condition in 7:4, what remains of its +/// mask in 3:0. All-zero low bits mean no block is in progress. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +pub struct ItState(pub u8); + +impl ItState { + /// True while instructions are still being predicated by a block. + pub fn in_block(self) -> bool { + self.0 & 0xf != 0 + } + + /// The condition the next instruction runs under, if any. + pub fn cond(self) -> Option { + self.in_block().then_some(Cond(self.0 >> 4)) + } + + /// The state one instruction later. + pub fn advance(self) -> ItState { + if !self.in_block() || self.0 & 7 == 0 { + return ItState(0); + } + ItState((self.0 & 0xe0) | ((self.0 << 1) & 0x1f)) + } + + fn start(firstcond: u32, mask: u32) -> ItState { + ItState((firstcond << 4 | mask) as u8) + } + + fn value(self) -> u32 { + self.cond().map_or(AL, |c| c.0 as u32) + } +} + +/// Decode one T32 instruction, and report the state the next one runs under. +pub fn decode(bytes: &[u8], addr: Addr, it: ItState) -> Option<(Insn, ItState)> { + let hw1 = u16::from_le_bytes(bytes.get(..2)?.try_into().ok()?) as u32; + let cond = it.value(); + if hw1 >> 11 >= 0b11101 { + let hw2 = u16::from_le_bytes(bytes.get(2..4)?.try_into().ok()?) as u32; + let i = wide(hw1 << 16 | hw2, cond, it.in_block(), addr)?; + return Some((i, it.advance())); + } + let i = narrow(hw1, cond, it.in_block(), addr)?; + // An `it` replaces the state rather than advancing it. + let next = if hw1 & 0xff00 == 0xbf00 && hw1 & 0xf != 0 { + ItState::start(it_cond(bits(hw1, 7, 4)), bits(hw1, 3, 0)) + } else { + it.advance() + }; + Some((i, next)) +} + +fn at(addr: Addr, len: u8, mn: &'static str, flow: Flow) -> Insn { + Insn::new(addr, len, mn, flow) +} + +fn branch_flow(cond: u32, target: Addr) -> Flow { + if cond == AL { + Flow::Branch(target) + } else { + Flow::CondBranch(target) + } +} + +/// A sixteen-bit encoding that sets the flags outside an IT block and does not +/// set them inside one, which is the whole difference between `adds` and +/// `addlo` in a listing. +fn flagged( + in_it: bool, + cond: u32, + plain: &'static [&str; 16], + setting: &'static str, +) -> &'static str { + if in_it { cm(plain, cond) } else { setting } +} + +const NARROW_DP: [&[&str; 16]; 16] = [ + conds!("and"), + conds!("eor"), + conds!("lsl"), + conds!("lsr"), + conds!("asr"), + conds!("adc"), + conds!("sbc"), + conds!("ror"), + conds!("tst"), + conds!("rsb"), + conds!("cmp"), + conds!("cmn"), + conds!("orr"), + conds!("mul"), + conds!("bic"), + conds!("mvn"), +]; + +const NARROW_DP_S: [&str; 16] = [ + "ands", "eors", "lsls", "lsrs", "asrs", "adcs", "sbcs", "rors", "tst", "rsbs", "cmp", "cmn", + "orrs", "muls", "bics", "mvns", +]; + +const NARROW_SHIFT: [&[&str; 16]; 3] = [conds!("lsl"), conds!("lsr"), conds!("asr")]; +const NARROW_SHIFT_S: [&str; 3] = ["lsls", "lsrs", "asrs"]; + +/// The sixteen-bit encodings, table A6-1. +fn narrow(hw: u32, cond: u32, in_it: bool, addr: Addr) -> Option { + let rd = bits(hw, 2, 0); + let rn = bits(hw, 5, 3); + match bits(hw, 15, 10) { + 0b000000..=0b001111 => shift_add_sub(hw, cond, in_it, addr), + 0b010000 => { + let op = bits(hw, 9, 6) as usize; + let mn = flagged(in_it, cond, NARROW_DP[op], NARROW_DP_S[op]); + let mut i = at(addr, 2, mn, Flow::Next); + match op { + 0b1001 => { + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Count(0)); + } + 0b1101 => { + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rd))); + } + _ => { + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rn))); + } + } + Some(i) + } + 0b010001 => special(hw, cond, addr), + 0b010010 | 0b010011 => { + let m = always(Mem::base_disp(reg(15), bits(hw, 7, 0) as i64 * 4, 4)); + let mut i = at(addr, 2, cm(conds!("ldr"), cond), Flow::Next); + i.push(Operand::Reg(reg(bits(hw, 10, 8)))) + .push(Operand::Mem(m)); + Some(i) + } + 0b010100..=0b010111 => { + const OPS: [(&[&str; 16], u64); 8] = [ + (conds!("str"), 4), + (conds!("strh"), 2), + (conds!("strb"), 1), + (conds!("ldrsb"), 1), + (conds!("ldr"), 4), + (conds!("ldrh"), 2), + (conds!("ldrb"), 1), + (conds!("ldrsh"), 2), + ]; + let (mn, size) = OPS[bits(hw, 11, 9) as usize]; + let mut m = Mem::base_disp(reg(rn), 0, size); + m.index = index(reg(bits(hw, 8, 6)), 0); + let mut i = at(addr, 2, cm(mn, cond), Flow::Next); + i.push(Operand::Reg(reg(rd))).push(Operand::Mem(m)); + Some(i) + } + 0b011000..=0b100111 => ldst_imm5(hw, cond, addr), + 0b101000 | 0b101001 => { + let mut i = at(addr, 2, cm(conds!("adr"), cond), Flow::Next); + i.push(Operand::Reg(reg(bits(hw, 10, 8)))) + .push(Operand::Count(bits(hw, 7, 0) as i64 * 4)); + Some(i) + } + 0b101010 | 0b101011 => { + let mut i = at(addr, 2, cm(conds!("add"), cond), Flow::Next); + i.push(Operand::Reg(reg(bits(hw, 10, 8)))) + .push(Operand::Reg(reg(13))) + .push(Operand::UImm(bits(hw, 7, 0) as u64 * 4)); + Some(i) + } + 0b101100..=0b101111 => misc(hw, cond, addr), + 0b110000..=0b110011 => { + let rn = bits(hw, 10, 8); + let list = bits(hw, 7, 0); + if list == 0 { + return None; + } + let load = bit(hw, 11) == 1; + let wb = !load || list >> rn & 1 == 0; + let mut i = at( + addr, + 2, + cm(if load { conds!("ldm") } else { conds!("stm") }, cond), + Flow::Next, + ); + if wb { + i.push(Operand::Name(wb_reg(rn))); + } else { + i.push(Operand::Reg(reg(rn))); + } + i.push(core_list(list)); + Some(i) + } + 0b110100..=0b110111 => { + let c = bits(hw, 11, 8); + let imm = bits(hw, 7, 0); + match c { + 0b1110 => Some(match imm { + // Two of the two hundred and fifty six are the EABI's. + 0xfe => at(addr, 2, "trap", Flow::Trap), + 0xf9 => at(addr, 2, "__brkdiv0", Flow::Trap), + _ => { + let mut i = at(addr, 2, "udf", Flow::Trap); + i.push(Operand::UImm(imm as u64)); + i + } + }), + 0b1111 => { + let mut i = at(addr, 2, cm(conds!("svc"), cond), Flow::Syscall); + i.push(Operand::UImm(imm as u64)); + Some(i) + } + _ => { + let target = super::target(addr, 4 + sext(imm, 8) * 2); + let c = if in_it { cond } else { c }; + let mut i = at(addr, 2, cm(conds!("b"), c), Flow::CondBranch(target)); + i.push(Operand::Addr(target)); + Some(i) + } + } + } + 0b111000 | 0b111001 => { + let target = super::target(addr, 4 + sext(bits(hw, 10, 0), 11) * 2); + let mut i = at(addr, 2, cm(conds!("b"), cond), branch_flow(cond, target)); + i.push(Operand::Addr(target)); + Some(i) + } + _ => None, + } +} + +/// Table A6-2: the shifts, adds, subtracts, moves and compares that make up a +/// quarter of the sixteen-bit space. +fn shift_add_sub(hw: u32, cond: u32, in_it: bool, addr: Addr) -> Option { + let (rd, rn) = (bits(hw, 2, 0), bits(hw, 5, 3)); + match bits(hw, 13, 11) { + 0b011 => { + let sub = bit(hw, 9) == 1; + let imm = bit(hw, 10) == 1; + let mn = if sub { + flagged(in_it, cond, conds!("sub"), "subs") + } else { + flagged(in_it, cond, conds!("add"), "adds") + }; + let mut i = at(addr, 2, mn, Flow::Next); + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rn))); + if imm { + i.push(Operand::UImm(bits(hw, 8, 6) as u64)); + } else { + i.push(Operand::Reg(reg(bits(hw, 8, 6)))); + } + Some(i) + } + op @ 0b000..=0b010 => { + let imm5 = bits(hw, 10, 6); + // `lsl` by nothing is how a register move is encoded, and it is + // the one sixteen-bit form that keeps setting the flags inside an + // IT block rather than taking the block's condition. + if op == 0 && imm5 == 0 { + let mut i = at(addr, 2, "movs", Flow::Next); + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rn))); + return Some(i); + } + let mn = flagged( + in_it, + cond, + NARROW_SHIFT[op as usize], + NARROW_SHIFT_S[op as usize], + ); + let amount = if op != 0 && imm5 == 0 { 32 } else { imm5 }; + let mut i = at(addr, 2, mn, Flow::Next); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::UImm(amount as u64)); + Some(i) + } + op => { + let rdn = bits(hw, 10, 8); + let imm = Operand::UImm(bits(hw, 7, 0) as u64); + let mn = match op { + 0b100 => flagged(in_it, cond, conds!("mov"), "movs"), + 0b101 => cm(conds!("cmp"), cond), + 0b110 => flagged(in_it, cond, conds!("add"), "adds"), + _ => flagged(in_it, cond, conds!("sub"), "subs"), + }; + let mut i = at(addr, 2, mn, Flow::Next); + i.push(Operand::Reg(reg(rdn))).push(imm); + Some(i) + } + } +} + +/// Table A6-3: the forms that reach the high registers, and `bx`. +fn special(hw: u32, cond: u32, addr: Addr) -> Option { + let rm = bits(hw, 6, 3); + let rdn = bit(hw, 7) << 3 | bits(hw, 2, 0); + match bits(hw, 9, 8) { + 0b11 => { + let call = bit(hw, 7) == 1; + let flow = match (call, rm) { + (true, _) => Flow::IndirectCall, + (false, 14) => Flow::Return, + (false, _) => Flow::IndirectBranch, + }; + let mut i = at( + addr, + 2, + cm(if call { conds!("blx") } else { conds!("bx") }, cond), + flow, + ); + i.push(Operand::Reg(reg(rm))); + Some(i) + } + op => { + if op == 0b00 && (rm == 13 || rdn == 13) { + let mut i = at(addr, 2, cm(conds!("add"), cond), Flow::Next); + // The stack pointer always prints as the second operand, so + // adding to it and adding it read differently. + if rm == 13 { + i.push(Operand::Reg(reg(rdn))) + .push(Operand::Reg(reg(13))) + .push(Operand::Reg(reg(rdn))); + } else { + i.push(Operand::Reg(reg(13))).push(Operand::Reg(reg(rm))); + } + return Some(i); + } + let (mn, flow) = match op { + 0b00 => ( + cm(conds!("add"), cond), + if rdn == 15 { + Flow::IndirectBranch + } else { + Flow::Next + }, + ), + 0b01 => (cm(conds!("cmp"), cond), Flow::Next), + _ => ( + cm(conds!("mov"), cond), + match (rdn, rm) { + (15, 14) => Flow::Return, + (15, _) => Flow::IndirectBranch, + _ => Flow::Next, + }, + ), + }; + let mut i = at(addr, 2, mn, flow); + i.push(Operand::Reg(reg(rdn))).push(Operand::Reg(reg(rm))); + Some(i) + } + } +} + +/// The immediate-offset loads and stores, whose scale follows their width. +fn ldst_imm5(hw: u32, cond: u32, addr: Addr) -> Option { + let load = bit(hw, 11) == 1; + let (rt, rn, imm5) = (bits(hw, 2, 0), bits(hw, 5, 3), bits(hw, 10, 6)); + let (mn, size, base, off) = match bits(hw, 15, 12) { + 0b0110 => ( + if load { conds!("ldr") } else { conds!("str") }, + 4, + rn, + imm5 * 4, + ), + 0b0111 => ( + if load { conds!("ldrb") } else { conds!("strb") }, + 1, + rn, + imm5, + ), + 0b1000 => ( + if load { conds!("ldrh") } else { conds!("strh") }, + 2, + rn, + imm5 * 2, + ), + _ => ( + if load { conds!("ldr") } else { conds!("str") }, + 4, + 13, + bits(hw, 7, 0) * 4, + ), + }; + let rt = if bits(hw, 15, 12) == 0b1001 { + bits(hw, 10, 8) + } else { + rt + }; + let mut i = at(addr, 2, cm(mn, cond), Flow::Next); + i.push(Operand::Reg(reg(rt))) + .push(Operand::Mem(Mem::base_disp(reg(base), off as i64, size))); + Some(i) +} + +/// Table A6-4: the sixteen-bit odds and ends, `push` and `pop` among them. +fn misc(hw: u32, cond: u32, addr: Addr) -> Option { + let (rd, rm) = (bits(hw, 2, 0), bits(hw, 5, 3)); + match bits(hw, 11, 8) { + 0b0000 => { + let sub = bit(hw, 7) == 1; + let mut i = at( + addr, + 2, + cm(if sub { conds!("sub") } else { conds!("add") }, cond), + Flow::Next, + ); + i.push(Operand::Reg(reg(13))) + .push(Operand::UImm(bits(hw, 6, 0) as u64 * 4)); + Some(i) + } + 0b0001 | 0b0011 | 0b1001 | 0b1011 => { + let off = bit(hw, 9) << 6 | bits(hw, 7, 3) << 1; + let target = super::target(addr, 4 + off as i64); + let mut i = at( + addr, + 2, + if bit(hw, 11) == 1 { "cbnz" } else { "cbz" }, + Flow::CondBranch(target), + ); + i.push(Operand::Reg(reg(rd))).push(Operand::Addr(target)); + Some(i) + } + 0b0010 => { + const OPS: [&[&str; 16]; 4] = [ + conds!("sxth"), + conds!("sxtb"), + conds!("uxth"), + conds!("uxtb"), + ]; + let mut i = at(addr, 2, cm(OPS[bits(hw, 7, 6) as usize], cond), Flow::Next); + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rm))); + Some(i) + } + 0b0100 | 0b0101 | 0b1100 | 0b1101 => { + let pop = bit(hw, 11) == 1; + let extra = bit(hw, 8) << if pop { 15 } else { 14 }; + let list = bits(hw, 7, 0) | extra; + if list == 0 { + return None; + } + let flow = if pop && list >> 15 == 1 { + Flow::Return + } else { + Flow::Next + }; + let mut i = at( + addr, + 2, + cm(if pop { conds!("pop") } else { conds!("push") }, cond), + flow, + ); + i.push(core_list(list)); + Some(i) + } + 0b1010 => { + let mn = match bits(hw, 7, 6) { + 0b00 => conds!("rev"), + 0b01 => conds!("rev16"), + 0b11 => conds!("revsh"), + _ => return None, + }; + let mut i = at(addr, 2, cm(mn, cond), Flow::Next); + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rm))); + Some(i) + } + 0b1110 => { + let mut i = at(addr, 2, "bkpt", Flow::Trap); + i.push(Operand::UImm(bits(hw, 7, 0) as u64)); + Some(i) + } + 0b1111 => { + let mask = bits(hw, 3, 0); + let first = bits(hw, 7, 4); + if mask != 0 { + // An unpredictable first condition is read as `al`, but the + // block still predicates with the encoded value. + let named = it_cond(first); + let mut i = at(addr, 2, it_name(named, mask), Flow::Next); + i.push(Operand::Cond(Cond(named as u8))); + return Some(i); + } + const HINTS: [&[&str; 16]; 5] = [ + conds!("nop"), + conds!("yield"), + conds!("wfe"), + conds!("wfi"), + conds!("sev"), + ]; + (first < 5).then(|| at(addr, 2, cm(HINTS[first as usize], cond), Flow::Next)) + } + _ => None, + } +} + +/// An unpredictable first condition is read as `al`, which then makes every +/// `e` instruction in the block carry the condition that has no name. +fn it_cond(first: u32) -> u32 { + if first == 15 { AL } else { first } +} + +/// The spelling of an `it`: one letter per instruction after the first, `t` +/// where it takes the block condition and `e` where it takes the inverse. +fn it_name(first: u32, mask: u32) -> &'static str { + const NAMES: [&str; 15] = [ + "it", "ite", "itt", "itee", "itet", "itte", "ittt", "iteee", "iteet", "itete", "itett", + "ittee", "ittet", "ittte", "itttt", + ]; + // The lowest set bit terminates the mask; everything above it is one + // instruction, and each such bit is `t` when it matches the condition's + // low bit. + let len = 4 - mask.trailing_zeros(); + let mut key = 0usize; + for n in 0..len.saturating_sub(1) { + let b = mask >> (3 - n) & 1; + key = key << 1 | ((b == first & 1) as usize); + } + NAMES[(1 << (len - 1)) - 1 + key] +} + +/// The thirty-two-bit encodings, table A6-9. +fn wide(w: u32, cond: u32, in_it: bool, addr: Addr) -> Option { + let op2 = bits(w, 26, 20); + match bits(w, 28, 27) { + // The coprocessor space, which for VFP is the A32 word unchanged. + 0b01 if op2 & 0b1000000 != 0 => match bits(w, 27, 24) { + 0b1100 | 0b1101 => { + vfp::ldst(w, cond, addr, 4).or_else(|| super::coproc(w, cond, addr, false)) + } + 0b1110 => vfp::dp(w, cond, addr, 4).or_else(|| super::coproc(w, cond, addr, false)), + _ => None, + }, + 0b01 if op2 & 0b1100100 == 0b0000000 => block(w, cond, addr), + 0b01 if op2 & 0b1100100 == 0b0000100 => dual(w, cond, addr), + 0b01 => dp_shifted(w, cond, addr), + 0b10 if bit(w, 15) == 1 => control(w, cond, in_it, addr), + 0b10 if op2 & 0b0100000 == 0 => dp_imm(w, cond, addr), + 0b10 => dp_plain_imm(w, cond, addr), + _ => group11(w, cond, addr), + } +} + +const WIDE_DP: [&[&str; 16]; 16] = [ + conds!("and", ".w"), + conds!("bic", ".w"), + conds!("orr", ".w"), + conds!("orn"), + conds!("eor", ".w"), + conds!("and", ".w"), + conds!("and", ".w"), + conds!("and", ".w"), + conds!("add", ".w"), + conds!("and", ".w"), + conds!("adc", ".w"), + conds!("sbc", ".w"), + conds!("and", ".w"), + conds!("sub", ".w"), + // `rsb` is the one shifted-register form LLVM spells without `.w`. + conds!("rsb"), + conds!("and", ".w"), +]; + +/// Data processing with a shifted register, which always takes `.w` except for +/// `rsb` and `rrx`, matching LLVM's tables rather than any rule. +fn dp_shifted(w: u32, cond: u32, addr: Addr) -> Option { + let op = bits(w, 24, 21) as usize; + let s = bit(w, 20) == 1; + let (rn, rd, rm) = (bits(w, 19, 16), bits(w, 11, 8), bits(w, 3, 0)); + let imm5 = bits(w, 14, 12) << 2 | bits(w, 7, 6); + let ty = bits(w, 5, 4); + let test = matches!(op, 0b0000 | 0b0100 | 0b1000 | 0b1101) && s && rd == 15; + + if op == 0b0010 && rn == 15 { + return move_shifted(cond, addr, s, rd, rm, ty, imm5); + } + if op == 0b0011 && rn == 15 { + let mut i = at( + addr, + 4, + cm( + if s { + conds!("mvns", ".w") + } else { + conds!("mvn", ".w") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))).push(shifted(rm, ty, imm5)); + return Some(i); + } + let mn = if test { + match op { + 0b0000 => cm(conds!("tst", ".w"), cond), + 0b0100 => cm(conds!("teq", ".w"), cond), + 0b1000 => cm(conds!("cmn", ".w"), cond), + _ => cm(conds!("cmp", ".w"), cond), + } + } else { + wide_dp_name(op, s, cond)? + }; + let mut i = at(addr, 4, mn, Flow::Next); + if test { + i.push(Operand::Reg(reg(rn))); + } else { + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rn))); + } + i.push(shifted(rm, ty, imm5)); + Some(i) +} + +/// The shifted-register and modified-immediate groups share an opcode field, +/// and the `s` forms need their own spellings. +fn wide_dp_name(op: usize, s: bool, cond: u32) -> Option<&'static str> { + const S_FORMS: [&[&str; 16]; 16] = [ + conds!("ands", ".w"), + conds!("bics", ".w"), + conds!("orrs", ".w"), + conds!("orns"), + conds!("eors", ".w"), + conds!("ands", ".w"), + conds!("ands", ".w"), + conds!("ands", ".w"), + conds!("adds", ".w"), + conds!("ands", ".w"), + conds!("adcs", ".w"), + conds!("sbcs", ".w"), + conds!("ands", ".w"), + conds!("subs", ".w"), + conds!("rsbs"), + conds!("ands", ".w"), + ]; + // `pkh` shares the opcode field but not the operand shape. + if matches!(op, 0b0101 | 0b0110 | 0b0111 | 0b1001 | 0b1100 | 0b1111) { + return None; + } + Some(cm(if s { S_FORMS[op] } else { WIDE_DP[op] }, cond)) +} + +/// `mov` with a shift is spelled as the shift, and its amount prints in hex +/// where the same amount inside a shifted operand prints in decimal. +fn move_shifted( + cond: u32, + addr: Addr, + s: bool, + rd: u32, + rm: u32, + ty: u32, + imm5: u32, +) -> Option { + let plain: [&[&str; 16]; 4] = [ + conds!("lsl", ".w"), + conds!("lsr", ".w"), + conds!("asr", ".w"), + conds!("ror", ".w"), + ]; + let setting: [&[&str; 16]; 4] = [ + conds!("lsls", ".w"), + conds!("lsrs", ".w"), + conds!("asrs", ".w"), + conds!("rors", ".w"), + ]; + let mut i = if ty == 0 && imm5 == 0 { + at( + addr, + 4, + cm( + if s { + conds!("movs", ".w") + } else { + conds!("mov", ".w") + }, + cond, + ), + Flow::Next, + ) + } else if ty == 3 && imm5 == 0 { + at( + addr, + 4, + cm(if s { conds!("rrxs") } else { conds!("rrx") }, cond), + Flow::Next, + ) + } else { + at( + addr, + 4, + cm( + if s { + setting[ty as usize] + } else { + plain[ty as usize] + }, + cond, + ), + Flow::Next, + ) + }; + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rm))); + if !(imm5 == 0 && (ty == 0 || ty == 3)) { + let (_, n) = super::decode_shift(ty, imm5); + i.push(Operand::UImm(n as u64)); + } + Some(i) +} + +/// Data processing with a modified immediate. Only the arithmetic and the +/// moves take `.w` here; the logical forms do not. +fn dp_imm(w: u32, cond: u32, addr: Addr) -> Option { + const PLAIN: [&[&str; 16]; 16] = [ + conds!("and"), + conds!("bic"), + conds!("orr"), + conds!("orn"), + conds!("eor"), + conds!("and"), + conds!("and"), + conds!("and"), + conds!("add", ".w"), + conds!("and"), + conds!("adc"), + conds!("sbc"), + conds!("and"), + conds!("sub", ".w"), + conds!("rsb", ".w"), + conds!("and"), + ]; + const S_FORMS: [&[&str; 16]; 16] = [ + conds!("ands"), + conds!("bics"), + conds!("orrs"), + conds!("orns"), + conds!("eors"), + conds!("ands"), + conds!("ands"), + conds!("ands"), + conds!("adds", ".w"), + conds!("ands"), + conds!("adcs"), + conds!("sbcs"), + conds!("ands"), + conds!("subs", ".w"), + conds!("rsbs", ".w"), + conds!("ands"), + ]; + let op = bits(w, 24, 21) as usize; + if matches!(op, 0b0101 | 0b0110 | 0b0111 | 0b1001 | 0b1100 | 0b1111) { + return None; + } + let s = bit(w, 20) == 1; + let (rn, rd) = (bits(w, 19, 16), bits(w, 11, 8)); + let value = t2_imm(bit(w, 26) << 11 | bits(w, 14, 12) << 8 | bits(w, 7, 0)); + let imm = Operand::UImm(value as u64); + let test = matches!(op, 0b0000 | 0b0100 | 0b1000 | 0b1101) && s && rd == 15; + if test { + let mn = match op { + 0b0000 => conds!("tst", ".w"), + 0b0100 => conds!("teq", ".w"), + 0b1000 => conds!("cmn", ".w"), + _ => conds!("cmp", ".w"), + }; + let mut i = at(addr, 4, cm(mn, cond), Flow::Next); + i.push(Operand::Reg(reg(rn))).push(imm); + return Some(i); + } + let mut i = if op == 0b0010 && rn == 15 { + let mut i = at( + addr, + 4, + cm( + if s { + conds!("movs", ".w") + } else { + conds!("mov", ".w") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))); + i + } else if op == 0b0011 && rn == 15 { + let mut i = at( + addr, + 4, + cm(if s { conds!("mvns") } else { conds!("mvn") }, cond), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))); + i + } else { + let mut i = at( + addr, + 4, + cm(if s { S_FORMS[op] } else { PLAIN[op] }, cond), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rn))); + i + }; + i.push(imm); + Some(i) +} + +/// The plain binary immediates: `movw`, `movt`, `addw`, the bitfields. +fn dp_plain_imm(w: u32, cond: u32, addr: Addr) -> Option { + let (rn, rd) = (bits(w, 19, 16), bits(w, 11, 8)); + let imm12 = bit(w, 26) << 11 | bits(w, 14, 12) << 8 | bits(w, 7, 0); + let lsb = bits(w, 14, 12) << 2 | bits(w, 7, 6); + match bits(w, 24, 20) { + 0b00000 | 0b01010 if rn == 15 => { + let sub = bit(w, 23) == 1; + let mut i = at(addr, 4, cm(conds!("adr", ".w"), cond), Flow::Next); + let off = imm12 as i64; + i.push(Operand::Reg(reg(rd))) + .push(Operand::Count(if sub { -off } else { off })); + Some(i) + } + op @ (0b00000 | 0b01010) => { + let mut i = at( + addr, + 4, + cm( + if op == 0 { + conds!("addw") + } else { + conds!("subw") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::UImm(imm12 as u64)); + Some(i) + } + 0b00100 | 0b01100 => { + let imm16 = rn << 12 | imm12; + let mut i = at( + addr, + 4, + cm( + if bit(w, 23) == 1 { + conds!("movt") + } else { + conds!("movw") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::UImm(imm16 as u64)); + Some(i) + } + 0b10100 | 0b11100 => { + let signed = bit(w, 23) == 0; + let mut i = at( + addr, + 4, + cm( + if signed { + conds!("sbfx") + } else { + conds!("ubfx") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::UImm(lsb as u64)) + .push(Operand::UImm(bits(w, 4, 0) as u64 + 1)); + Some(i) + } + 0b10110 => { + let width = bits(w, 4, 0) as i64 - lsb as i64 + 1; + if width < 1 { + return None; + } + let mut i = at( + addr, + 4, + cm( + if rn == 15 { + conds!("bfc") + } else { + conds!("bfi") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))); + if rn != 15 { + i.push(Operand::Reg(reg(rn))); + } + i.push(Operand::Count(lsb as i64)) + .push(Operand::Count(width)); + Some(i) + } + _ => None, + } +} + +/// Branches, and the control instructions that share their slot. +fn control(w: u32, cond: u32, in_it: bool, addr: Addr) -> Option { + let s = bit(w, 26); + let (j1, j2) = (bit(w, 13), bit(w, 11)); + match (bit(w, 14), bit(w, 12)) { + (0, 0) => { + // The barriers, the hints and the system register moves share this + // space with the conditional branch, in the rows where the field + // the branch reads as its condition is 1110 or 1111 and is not one. + // `misc_control` tests for those rows exactly, so asking it first + // is what distinguishes them; a conditional branch cannot match its + // guards, because its condition is below 1110 by construction. + if let Some(i) = misc_control(w, cond, addr) { + return Some(i); + } + if let Some(i) = special_move(w, cond, addr) { + return Some(i); + } + let c = bits(w, 25, 22); + if c >= 0b1110 { + return None; + } + let off = sext( + s << 20 | j2 << 19 | j1 << 18 | bits(w, 21, 16) << 12 | bits(w, 10, 0) << 1, + 21, + ); + let target = super::target(addr, 4 + off); + // Inside an IT block a conditional branch is unpredictable, and + // the block's condition is the one that prints. + let c = if in_it { cond } else { c }; + let mut i = at(addr, 4, cm(conds!("b", ".w"), c), Flow::CondBranch(target)); + i.push(Operand::Addr(target)); + Some(i) + } + (hi, lo) => { + let i1 = 1 - (j1 ^ s); + let i2 = 1 - (j2 ^ s); + let off = sext( + s << 24 | i1 << 23 | i2 << 22 | bits(w, 25, 16) << 12 | bits(w, 10, 0) << 1, + 25, + ); + // `blx` changes instruction set, so its target is A32-aligned and + // measured from a pc rounded down to a word. + let exchange = hi == 1 && lo == 0; + if exchange && bit(w, 0) == 1 { + return None; + } + let base = if exchange { + Addr(addr.get() & !3) + } else { + addr + }; + let target = super::target(base, 4 + off); + let (mn, flow) = match (hi, lo) { + (0, _) => (cm(conds!("b", ".w"), cond), branch_flow(cond, target)), + (_, 1) => (cm(conds!("bl"), cond), Flow::Call(target)), + _ => (cm(conds!("blx"), cond), Flow::Call(target)), + }; + let mut i = at(addr, 4, mn, flow); + i.push(Operand::Addr(target)); + Some(i) + } + } +} + +/// The special registers a Thumb `msr` and `mrs` name. +/// +/// These are the M-profile registers: the encoding does not exist on +/// A-profile, which is why naming them needs no knowledge of the image. A +/// number inside the architecture's three groups with no name of its own is +/// printed as the number, which is what it is; anything outside them is not +/// decoded, because a register the architecture does not define is not one. +fn special_register(sysm: u32) -> Option<&'static str> { + Some(match sysm { + 0 => "apsr", + 1 => "iapsr", + 2 => "eapsr", + 3 => "xpsr", + 5 => "ipsr", + 6 => "epsr", + 7 => "iepsr", + 8 => "msp", + 9 => "psp", + 16 => "primask", + 17 => "basepri", + 18 => "basepri_max", + 19 => "faultmask", + 20 => "control", + _ => return None, + }) +} + +/// The special register as an operand: its name, or its number inside the +/// space the architecture defines. +fn special_operand(sysm: u32) -> Option { + if let Some(name) = special_register(sysm) { + return Some(Operand::Name(name)); + } + // `SYSm[7:3]` selects the group: 0 is the program status registers, 1 the + // stack pointers, 2 the masks. Above that there is nothing. + (sysm >> 3 <= 2).then_some(Operand::Count(sysm as i64)) +} + +/// `msr` and `mrs`, which move between a core register and a special one. +/// +/// They sit in the branch-and-miscellaneous space alongside the barriers, in +/// the rows where the field a conditional branch reads as its condition is not +/// one. Firmware is built out of these: masking interrupts, switching stacks +/// and reading the exception number all go through them. +fn special_move(w: u32, cond: u32, addr: Addr) -> Option { + let op = bits(w, 26, 20); + let sysm = bits(w, 7, 0); + let name = special_operand(sysm)?; + if op == 0b0111000 || op == 0b0111001 { + // `msr , Rn`. + let rn = bits(w, 19, 16); + if rn == 15 || rn == 13 { + return None; + } + let mut i = at(addr, 4, cm(conds!("msr"), cond), Flow::Next); + i.push(name).push(Operand::Reg(reg(rn))); + return Some(i); + } + if op == 0b0111110 || op == 0b0111111 { + // `mrs Rd, `. + let rd = bits(w, 11, 8); + if rd == 15 || rd == 13 || bits(w, 19, 16) != 0b1111 { + return None; + } + let mut i = at(addr, 4, cm(conds!("mrs"), cond), Flow::Next); + i.push(Operand::Reg(reg(rd))).push(name); + return Some(i); + } + None +} + +/// The barriers and hints that live among the branches. +fn misc_control(w: u32, cond: u32, addr: Addr) -> Option { + if bits(w, 25, 20) == 0b111011 { + let option = Operand::Name(super::BARRIERS[bits(w, 3, 0) as usize]); + let mn = match bits(w, 7, 4) { + 0b0100 => "dsb", + 0b0101 => "dmb", + 0b0110 => "isb", + 0b0010 => return Some(at(addr, 4, "clrex", Flow::Next)), + _ => return None, + }; + let mut i = at(addr, 4, mn, Flow::Next); + i.push(option); + return Some(i); + } + if bits(w, 25, 20) == 0b111010 && bits(w, 10, 8) == 0 { + const HINTS: [&[&str; 16]; 5] = [ + conds!("nop", ".w"), + conds!("yield", ".w"), + conds!("wfe", ".w"), + conds!("wfi", ".w"), + conds!("sev", ".w"), + ]; + let n = bits(w, 7, 0) as usize; + return (n < HINTS.len()).then(|| at(addr, 4, cm(HINTS[n], cond), Flow::Next)); + } + None +} + +/// Load and store multiple, whose `push` and `pop` spellings take `.w`. +fn block(w: u32, cond: u32, addr: Addr) -> Option { + let (wb, l) = (bit(w, 21) == 1, bit(w, 20) == 1); + let rn = bits(w, 19, 16); + let list = bits(w, 15, 0); + if list.count_ones() < 2 { + return None; + } + let db = match bits(w, 24, 23) { + 0b01 => false, + 0b10 => true, + _ => return None, + }; + let stack = rn == 13 && wb; + let flow = if l && list >> 15 == 1 { + if rn == 13 { + Flow::Return + } else { + Flow::IndirectBranch + } + } else { + Flow::Next + }; + let (mn, implicit) = match (l, db, stack) { + (true, false, true) => (cm(conds!("pop", ".w"), cond), true), + (false, true, true) => (cm(conds!("push", ".w"), cond), true), + (true, false, _) => (cm(conds!("ldm", ".w"), cond), false), + (true, true, _) => (cm(conds!("ldmdb"), cond), false), + (false, false, _) => (cm(conds!("stm", ".w"), cond), false), + (false, true, _) => (cm(conds!("stmdb"), cond), false), + }; + let mut i = at(addr, 4, mn, flow); + if !implicit { + if wb { + i.push(Operand::Name(wb_reg(rn))); + } else { + i.push(Operand::Reg(reg(rn))); + } + } + i.push(core_list(list)); + Some(i) +} + +/// Load and store dual, the exclusives, and the table branches. +fn dual(w: u32, cond: u32, addr: Addr) -> Option { + let (rn, rt, rt2) = (bits(w, 19, 16), bits(w, 15, 12), bits(w, 11, 8)); + match (bits(w, 24, 23), bits(w, 21, 20)) { + (0b00, 0b00) => { + let m = Mem::base_disp(reg(rn), bits(w, 7, 0) as i64 * 4, 4); + let mut i = at(addr, 4, cm(conds!("strex"), cond), Flow::Next); + i.push(Operand::Reg(reg(rt2))) + .push(Operand::Reg(reg(rt))) + .push(Operand::Mem(m)); + Some(i) + } + (0b00, 0b01) => { + let m = Mem::base_disp(reg(rn), bits(w, 7, 0) as i64 * 4, 4); + let mut i = at(addr, 4, cm(conds!("ldrex"), cond), Flow::Next); + i.push(Operand::Reg(reg(rt))).push(Operand::Mem(m)); + Some(i) + } + (0b01, 0b01) if bits(w, 7, 4) <= 1 => { + let half = bit(w, 4) == 1; + let mut m = Mem::base_disp(reg(rn), 0, if half { 2 } else { 1 }); + m.index = index(reg(bits(w, 3, 0)), half as u8); + let mut i = at( + addr, + 4, + cm(if half { conds!("tbh") } else { conds!("tbb") }, cond), + Flow::IndirectBranch, + ); + i.push(Operand::Mem(m)); + Some(i) + } + _ => { + let (p, u, wb, l) = ( + bit(w, 24) == 1, + bit(w, 23) == 1, + bit(w, 21) == 1, + bit(w, 20) == 1, + ); + if !p && !wb { + return None; + } + let mode = match (p, wb) { + (false, _) => AddrMode::PostIndex, + (true, true) => AddrMode::PreIndex, + (true, false) => AddrMode::Offset, + }; + let off = bits(w, 7, 0) as i64 * 4; + let mut m = decimal(Mem { + seg: None, + base: Some(reg(rn)), + index: None, + disp: if u { off } else { -off }, + mode, + size: 8, + }); + if !u && off == 0 { + m = minus_zero(m); + } + let mut i = at( + addr, + 4, + cm(if l { conds!("ldrd") } else { conds!("strd") }, cond), + Flow::Next, + ); + i.push(Operand::Reg(reg(rt))) + .push(Operand::Reg(reg(rt2))) + .push(Operand::Mem(m)); + Some(i) + } + } +} + +/// The `op1 == 11` half: the single loads and stores, the register +/// data-processing forms, and the multiplies. +fn group11(w: u32, cond: u32, addr: Addr) -> Option { + let op2 = bits(w, 26, 20); + if op2 & 0b1110000 == 0b0100000 { + return dp_reg(w, cond, addr); + } + if op2 & 0b1111000 == 0b0110000 { + return multiply(w, cond, addr); + } + if op2 & 0b1111000 == 0b0111000 { + return multiply_long(w, cond, addr); + } + if op2 & 0b1000000 == 0 { + return ldst(w, cond, addr); + } + // The coprocessor space again, in the second of the two halves it spans. + // These are the `2` forms: the same instructions, encoded where A32 puts + // its unconditional space, so they carry no condition. + match bits(w, 27, 24) { + 0b1100..=0b1110 => super::coproc(w, cond, addr, true), + _ => None, + } +} + +/// The single-item loads and stores, whose `.w` marks the twelve-bit and +/// register forms and whose absence marks the eight-bit ones. +fn ldst(w: u32, cond: u32, addr: Addr) -> Option { + let signed = bit(w, 24) == 1; + let wide_imm = bit(w, 23) == 1; + let size = bits(w, 22, 21); + let load = bit(w, 20) == 1; + let (rn, rt) = (bits(w, 19, 16), bits(w, 15, 12)); + // `rt == 15` on a store, or on anything narrower than a word, is the + // preload hint rather than a transfer. On a word load it is a load into + // the program counter, which is a branch and is decoded as one: A32 has + // always read it that way, and a Thumb epilogue that cannot use + // `pop {.., pc}` writes `ldr pc, [sp], #4` instead. + if size == 3 || (signed && (!load || size == 2)) { + return None; + } + if rt == 15 && !(load && !signed && size == 2) { + return None; + } + let bytes = 1u64 << size; + let names: [&[&str; 16]; 6] = [ + conds!("strb", ".w"), + conds!("ldrb", ".w"), + conds!("strh", ".w"), + conds!("ldrh", ".w"), + conds!("str", ".w"), + conds!("ldr", ".w"), + ]; + let narrow_names: [&[&str; 16]; 6] = [ + conds!("strb"), + conds!("ldrb"), + conds!("strh"), + conds!("ldrh"), + conds!("str"), + conds!("ldr"), + ]; + let signed_names: [&[&str; 16]; 2] = [conds!("ldrsb", ".w"), conds!("ldrsh", ".w")]; + let signed_narrow: [&[&str; 16]; 2] = [conds!("ldrsb"), conds!("ldrsh")]; + let pick = |wide: bool| -> &'static [&'static str; 16] { + if signed { + if wide { + signed_names[size as usize] + } else { + signed_narrow[size as usize] + } + } else if wide { + names[(size * 2 + load as u32) as usize] + } else { + narrow_names[(size * 2 + load as u32) as usize] + } + }; + + if rn == 15 { + if !load { + return None; + } + let off = bits(w, 11, 0) as i64; + let mut m = always(Mem::base_disp( + reg(15), + if wide_imm { off } else { -off }, + bytes, + )); + if !wide_imm && off == 0 { + m = minus_zero(m); + } + let flow = if rt == 15 { + Flow::IndirectBranch + } else { + Flow::Next + }; + let mut i = at(addr, 4, cm(pick(true), cond), flow); + i.push(Operand::Reg(reg(rt))).push(Operand::Mem(m)); + return Some(i); + } + if wide_imm { + let m = Mem::base_disp(reg(rn), bits(w, 11, 0) as i64, bytes); + let flow = if rt == 15 { + Flow::IndirectBranch + } else { + Flow::Next + }; + let mut i = at(addr, 4, cm(pick(true), cond), flow); + i.push(Operand::Reg(reg(rt))).push(Operand::Mem(m)); + return Some(i); + } + if bits(w, 11, 6) == 0 { + let mut m = Mem::base_disp(reg(rn), 0, bytes); + m.index = index(reg(bits(w, 3, 0)), bits(w, 5, 4) as u8); + let flow = if rt == 15 { + Flow::IndirectBranch + } else { + Flow::Next + }; + let mut i = at(addr, 4, cm(pick(true), cond), flow); + i.push(Operand::Reg(reg(rt))).push(Operand::Mem(m)); + return Some(i); + } + if bit(w, 11) == 0 { + return None; + } + let (p, u, wb) = (bit(w, 10) == 1, bit(w, 9) == 1, bit(w, 8) == 1); + if p && u && !wb { + const T_FORMS: [&[&str; 16]; 6] = [ + conds!("strbt"), + conds!("ldrbt"), + conds!("strht"), + conds!("ldrht"), + conds!("strt"), + conds!("ldrt"), + ]; + const T_SIGNED: [&[&str; 16]; 2] = [conds!("ldrsbt"), conds!("ldrsht")]; + let mn = if signed { + T_SIGNED[size as usize] + } else { + T_FORMS[(size * 2 + load as u32) as usize] + }; + let m = Mem::base_disp(reg(rn), bits(w, 7, 0) as i64, bytes); + let mut i = at(addr, 4, cm(mn, cond), Flow::Next); + i.push(Operand::Reg(reg(rt))).push(Operand::Mem(decimal(m))); + return Some(i); + } + if !p && !wb { + return None; + } + let mode = match (p, wb) { + (false, _) => AddrMode::PostIndex, + (true, true) => AddrMode::PreIndex, + (true, false) => AddrMode::Offset, + }; + let off = bits(w, 7, 0) as i64; + let mut m = decimal(Mem { + seg: None, + base: Some(reg(rn)), + index: None, + disp: if u { off } else { -off }, + mode, + size: bytes, + }); + if !u && off == 0 { + m = minus_zero(m); + } + // A word load into the program counter off the stack with a post-increment + // is the return idiom a Thumb epilogue uses when it cannot say + // `pop {.., pc}`: it reads the return address and jumps to it. Any other + // load into the program counter is a branch whose target is in memory. + let flow = match (rt, rn, mode) { + (15, 13, AddrMode::PostIndex) => Flow::Return, + (15, _, _) => Flow::IndirectBranch, + _ => Flow::Next, + }; + let mut i = at(addr, 4, cm(pick(false), cond), flow); + i.push(Operand::Reg(reg(rt))).push(Operand::Mem(m)); + Some(i) +} + +/// Shifts by a register, the extends, and the `clz` corner. +fn dp_reg(w: u32, cond: u32, addr: Addr) -> Option { + let (rn, rd, rm) = (bits(w, 19, 16), bits(w, 11, 8), bits(w, 3, 0)); + if bits(w, 15, 12) != 0b1111 { + return None; + } + if bit(w, 23) == 0 && bits(w, 7, 4) == 0 { + const PLAIN: [&[&str; 16]; 4] = [ + conds!("lsl", ".w"), + conds!("lsr", ".w"), + conds!("asr", ".w"), + conds!("ror", ".w"), + ]; + const SETS: [&[&str; 16]; 4] = [ + conds!("lsls", ".w"), + conds!("lsrs", ".w"), + conds!("asrs", ".w"), + conds!("rors", ".w"), + ]; + let op = bits(w, 22, 21) as usize; + let s = bit(w, 20) == 1; + let mut i = at( + addr, + 4, + cm(if s { SETS[op] } else { PLAIN[op] }, cond), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rm))); + return Some(i); + } + if bit(w, 23) == 0 && bits(w, 7, 6) == 0b10 { + // The four with a sixteen-bit counterpart take `.w`; the two that + // pack halfwords do not, and neither do the accumulating forms. + const PLAIN: [&[&str; 16]; 6] = [ + conds!("sxth", ".w"), + conds!("uxth", ".w"), + conds!("sxtb16"), + conds!("uxtb16"), + conds!("sxtb", ".w"), + conds!("uxtb", ".w"), + ]; + const WITH: [&[&str; 16]; 6] = [ + conds!("sxtah"), + conds!("uxtah"), + conds!("sxtab16"), + conds!("uxtab16"), + conds!("sxtab"), + conds!("uxtab"), + ]; + let op = bits(w, 22, 20) as usize; + if op >= PLAIN.len() { + return None; + } + let plain = rn == 15; + let mut i = at( + addr, + 4, + cm(if plain { PLAIN[op] } else { WITH[op] }, cond), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))); + if !plain { + i.push(Operand::Reg(reg(rn))); + } + i.push(Operand::Reg(reg(rm))); + let rotate = bits(w, 5, 4); + if rotate != 0 { + const ROTATES: [&str; 4] = ["ror #0", "ror #8", "ror #16", "ror #24"]; + i.push(Operand::Name(ROTATES[rotate as usize])); + } + return Some(i); + } + if bits(w, 23, 22) == 0b10 && bits(w, 7, 6) == 0b10 { + if rn != rm { + return None; // the source is encoded twice and must agree + } + let mn = match (bits(w, 21, 20), bits(w, 5, 4)) { + (0b01, 0b00) => conds!("rev", ".w"), + (0b01, 0b01) => conds!("rev16", ".w"), + (0b01, 0b10) => conds!("rbit"), + (0b01, 0b11) => conds!("revsh", ".w"), + (0b11, 0b00) => conds!("clz"), + _ => return None, + }; + let mut i = at(addr, 4, cm(mn, cond), Flow::Next); + i.push(Operand::Reg(reg(rd))).push(Operand::Reg(reg(rm))); + return Some(i); + } + None +} + +/// `mul`, `mla` and `mls`; the signed multiply corner is left alone. +fn multiply(w: u32, cond: u32, addr: Addr) -> Option { + if bits(w, 22, 20) != 0 || bits(w, 7, 5) != 0 { + return None; + } + let (rn, ra, rd, rm) = ( + bits(w, 19, 16), + bits(w, 15, 12), + bits(w, 11, 8), + bits(w, 3, 0), + ); + let mls = bit(w, 4) == 1; + if ra == 15 && mls { + return None; + } + let mut i = at( + addr, + 4, + cm( + if mls { + conds!("mls") + } else if ra == 15 { + conds!("mul") + } else { + conds!("mla") + }, + cond, + ), + Flow::Next, + ); + i.push(Operand::Reg(reg(rd))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rm))); + if ra != 15 { + i.push(Operand::Reg(reg(ra))); + } + Some(i) +} + +/// The long multiplies and the divides, which share a table. +fn multiply_long(w: u32, cond: u32, addr: Addr) -> Option { + let (rn, lo, hi, rm) = ( + bits(w, 19, 16), + bits(w, 15, 12), + bits(w, 11, 8), + bits(w, 3, 0), + ); + let op2 = bits(w, 7, 4); + let mn = match (bits(w, 22, 20), op2) { + (0b000, 0b0000) => conds!("smull"), + (0b010, 0b0000) => conds!("umull"), + (0b100, 0b0000) => conds!("smlal"), + (0b110, 0b0000) => conds!("umlal"), + (0b110, 0b0110) => conds!("umaal"), + (0b001, 0b1111) => conds!("sdiv"), + (0b011, 0b1111) => conds!("udiv"), + _ => return None, + }; + let divide = op2 == 0b1111; + let mut i = at(addr, 4, cm(mn, cond), Flow::Next); + if divide { + i.push(Operand::Reg(reg(hi))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rm))); + } else { + i.push(Operand::Reg(reg(lo))) + .push(Operand::Reg(reg(hi))) + .push(Operand::Reg(reg(rn))) + .push(Operand::Reg(reg(rm))); + } + Some(i) +} diff --git a/crates/e5r-arch/src/arm/text.rs b/crates/e5r-arch/src/arm/text.rs new file mode 100644 index 0000000..1c21cce --- /dev/null +++ b/crates/e5r-arch/src/arm/text.rs @@ -0,0 +1,296 @@ +//! ARM text output, formatted the way llvm-objdump formats it. +//! +//! Matching the oracle exactly is what makes the parity gate a string +//! comparison instead of a judgment call, so the quirks are copied rather than +//! tidied: a data-processing immediate prints as a signed decimal, a `movw` +//! immediate as hex, and a memory displacement as whichever LLVM's printer for +//! that addressing mode chose. + +use std::fmt::Write; + +use crate::insn::{AddrMode, Extend, Insn, Mem, Operand, Reg, RegClass, Shift, Width}; + +use super::COND_NAMES; + +/// Register names, where three of the sixteen have a role name instead. +pub(crate) fn reg(out: &mut String, r: Reg) { + match r.class { + RegClass::Sp => out.push_str("sp"), + RegClass::Pc => out.push_str("pc"), + RegClass::Gpr if r.num == 14 => out.push_str("lr"), + RegClass::Gpr => { + let _ = write!(out, "r{}", r.num); + } + RegClass::Vec => { + let prefix = match r.width { + Width::W128 => 'q', + Width::W64 => 'd', + _ => 's', + }; + let _ = write!(out, "{prefix}{}", r.num); + } + _ => out.push('?'), + } +} + +/// A register list, `{r4, r5, lr}` or a run of VFP registers. +fn list(out: &mut String, v: u32) { + out.push('{'); + let mut first = true; + let mut sep = |out: &mut String| { + if !first { + out.push_str(", "); + } + first = false; + }; + match v >> 28 { + 0 => { + for n in 0..16 { + if v >> n & 1 == 1 { + sep(out); + reg(out, super::reg(n)); + } + } + } + tag => { + let (base, count) = ((v >> 8) & 0xff, v & 0xff); + for n in 0..count { + sep(out); + reg(out, super::vreg(base + n, tag == 2)); + } + } + } + out.push('}'); +} + +/// A shift that follows a register operand. A `ror` by zero is `rrx`. +fn shift(out: &mut String, sh: Shift, n: u8) { + if sh == Shift::Ror && n == 0 { + out.push_str(", rrx"); + } else { + let _ = write!(out, ", {} #{n}", sh.as_str()); + } +} + +fn mem(out: &mut String, m: Mem) { + let flags = m.seg.map_or(0, |r| r.num); + let decimal = flags & DEC != 0; + let force = flags & ALW != 0; + let neg_zero = flags & NEG != 0; + let disp = |out: &mut String, v: i64| { + if neg_zero { + out.push_str(if decimal { "#-0" } else { "#-0x0" }); + } else if decimal { + let _ = write!(out, "#{v}"); + } else if v < 0 { + let _ = write!(out, "#-{:#x}", v.unsigned_abs()); + } else { + let _ = write!(out, "#{v:#x}"); + } + }; + out.push('['); + if let Some(b) = m.base { + reg(out, b); + } + let index = |out: &mut String| { + if let Some((ix, _, amount)) = m.index { + out.push_str(", "); + reg(out, ix); + if amount != 0 { + let _ = write!(out, ", lsl #{amount}"); + } + true + } else { + false + } + }; + match m.mode { + AddrMode::PostIndex => { + out.push(']'); + if !index(out) { + out.push_str(", "); + disp(out, m.disp); + } + } + AddrMode::Offset => { + if !index(out) && (m.disp != 0 || force || neg_zero) { + out.push_str(", "); + disp(out, m.disp); + } + out.push(']'); + } + AddrMode::PreIndex => { + if !index(out) { + out.push_str(", "); + disp(out, m.disp); + } + out.push_str("]!"); + } + } +} + +/// A VFP literal in LLVM's `%e` spelling, which is six fraction digits and at +/// least two exponent digits. +fn fp_text(bits: u64) -> String { + let s = format!("{:.6e}", f64::from_bits(bits)); + match s.split_once('e') { + Some((m, e)) => { + let exp: i32 = e.parse().unwrap_or(0); + format!("{m}e{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs()) + } + None => s, + } +} + +fn operand(out: &mut String, op: &Operand) { + match op { + Operand::Reg(r) => reg(out, *r), + Operand::Shifted(r, sh, n) => { + reg(out, *r); + shift(out, *sh, *n); + } + Operand::Extended(r, ext, n) => { + reg(out, *r); + let _ = write!(out, ", {} #{n}", ext.as_str()); + } + Operand::Imm(v) => { + if *v < 0 { + let _ = write!(out, "#-{:#x}", v.unsigned_abs()); + } else { + let _ = write!(out, "#{v:#x}"); + } + } + Operand::UImm(v) => { + let _ = write!(out, "#{v:#x}"); + } + Operand::Count(v) => { + let _ = write!(out, "#{v}"); + } + Operand::ShiftOp(sh, n) => { + let _ = write!(out, "{} #{n}", sh.as_str()); + } + Operand::Name(n) => out.push_str(n), + Operand::FpImm(b) => { + let _ = write!(out, "#{}", fp_text(*b)); + } + Operand::Addr(a) => { + let _ = write!(out, "{:#x}", a.get()); + } + Operand::Mem(m) => mem(out, *m), + Operand::Cond(c) => out.push_str(COND_NAMES[(c.0 & 15) as usize]), + Operand::Sys(v) => list(out, *v), + // AArch64 lane spellings, which no ARM encoding here produces. + Operand::Vector(..) | Operand::VectorLane(..) | Operand::VectorList(..) => out.push('?'), + } +} + +/// Render a whole instruction as `mnemonic\toperands`. +pub fn format(i: &Insn) -> String { + let mut out = String::with_capacity(32); + out.push_str(i.mnemonic); + for (n, op) in i.operands().iter().enumerate() { + out.push_str(if n == 0 { "\t" } else { ", " }); + operand(&mut out, op); + } + out +} + +/// The segment field is dead on ARM, so it carries the two things [`Mem`] has +/// nowhere else to say, both of them the addressing mode's rather than the +/// instruction's: whether the displacement prints in decimal, and whether a +/// zero one prints at all. `Seg` is an x86 bank, so the marker can never be +/// mistaken for a real ARM operand. +const DEC: u8 = 1; +const ALW: u8 = 2; +const NEG: u8 = 4; + +fn mark(mut m: Mem, flags: u8) -> Mem { + m.seg = Some(Reg { + class: RegClass::Seg, + num: m.seg.map_or(0, |r| r.num) | flags, + width: Width::W32, + }); + m +} + +/// The eight-bit and scaled displacements, which print in decimal. +pub(crate) fn decimal(m: Mem) -> Mem { + mark(m, DEC) +} + +/// The literal loads, which print `[pc, #0x0]` rather than `[pc]`. +pub(crate) fn always(m: Mem) -> Mem { + mark(m, ALW) +} + +/// A subtracting offset of zero, which a listing spells `#-0`: the sign is in +/// the encoding and survives the arithmetic that loses it. +pub(crate) fn minus_zero(m: Mem) -> Mem { + mark(m, NEG) +} + +/// An index register with no scale and no sign, the only indexed form the +/// shared [`Mem`] can hold: its `Extend` cannot say `lsr` and it cannot say +/// subtract, so the decoders decline those rather than print them wrong. +pub(crate) fn index(rm: Reg, amount: u8) -> Option<(Reg, Extend, u8)> { + Some((rm, Extend::Lsl, amount)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::insn::Flow; + use e5r_core::Addr; + + #[test] + fn role_registers_print_by_name() { + let mut s = String::new(); + for n in [3, 12, 13, 14, 15] { + reg(&mut s, super::super::reg(n)); + s.push(' '); + } + assert_eq!(s, "r3 r12 sp lr pc "); + } + + #[test] + fn a_ror_by_zero_is_rrx() { + let mut s = String::new(); + shift(&mut s, Shift::Ror, 0); + shift(&mut s, Shift::Ror, 3); + assert_eq!(s, ", rrx, ror #3"); + } + + #[test] + fn register_lists_expand() { + let mut s = String::new(); + list(&mut s, 0x5030); + assert_eq!(s, "{r4, r5, r12, lr}"); + let mut v = String::new(); + list(&mut v, 2 << 28 | 8 << 8 | 3); + assert_eq!(v, "{d8, d9, d10}"); + } + + #[test] + fn displacements_follow_the_addressing_mode() { + let base = Mem::base_disp(super::super::reg(1), -4, 4); + let mut hex = String::new(); + mem(&mut hex, base); + assert_eq!(hex, "[r1, #-0x4]"); + let mut dec = String::new(); + mem(&mut dec, decimal(base)); + assert_eq!(dec, "[r1, #-4]"); + } + + #[test] + fn fp_literals_match_llvms_spelling() { + assert_eq!(fp_text(1.0f64.to_bits()), "1.000000e+00"); + assert_eq!(fp_text((-0.125f64).to_bits()), "-1.250000e-01"); + } + + #[test] + fn targets_keep_their_prefix() { + let mut i = Insn::new(Addr(0x1000), 4, "b", Flow::Branch(Addr(0x2000))); + i.push(Operand::Addr(Addr(0x2000))); + assert_eq!(format(&i), "b\t0x2000"); + } +} diff --git a/crates/e5r-arch/src/arm/vfp.rs b/crates/e5r-arch/src/arm/vfp.rs new file mode 100644 index 0000000..a0d13e8 --- /dev/null +++ b/crates/e5r-arch/src/arm/vfp.rs @@ -0,0 +1,324 @@ +//! VFP, shared by both instruction sets. +//! +//! A T32 coprocessor instruction is bit for bit the A32 one, so both decoders +//! hand the same word here and only the condition differs: A32 reads it from +//! bits 31:28, T32 from the IT state. + +use e5r_core::Addr; + +use crate::insn::{AddrMode, Flow, Insn, Mem, Operand}; + +use super::text::{decimal, minus_zero}; +use super::{bit, bits, cm, reg, vfp_list, vreg, wb_reg}; + +/// One mnemonic in both precisions, single first. +macro_rules! fp { + ($b:literal) => { + [conds!($b, ".f32"), conds!($b, ".f64")] + }; +} + +/// The VFP system registers by their four-bit field. +const VFP_SYSREGS: [&str; 16] = [ + "fpsid", "fpscr", "s2", "s3", "s4", "mvfr2", "mvfr1", "mvfr0", "fpexc", "fpinst", "fpinst2", + "s11", "s12", "s13", "s14", "s15", +]; + +/// Bits 27:25 == 110: the coprocessor loads and stores, which for VFP are +/// `vldr`, `vstr`, the multiple forms, and the 64-bit core transfers. +pub fn ldst(w: u32, cond: u32, addr: Addr, len: u8) -> Option { + let double = match bits(w, 11, 8) { + 0b1010 => false, + 0b1011 => true, + _ => return None, + }; + if bits(w, 24, 21) == 0b0010 { + return core_pair(w, cond, addr, len, double); + } + let (p, u, wb, l) = (bit(w, 24), bit(w, 23), bit(w, 21) == 1, bit(w, 20) == 1); + let rn = bits(w, 19, 16); + let vd = bits(w, 15, 12); + let d = bit(w, 22); + let first = if double { d << 4 | vd } else { vd << 1 | d }; + + if p == 1 && !wb { + let off = bits(w, 7, 0) as i64 * 4; + let mut m = decimal(Mem { + seg: None, + base: Some(reg(rn)), + index: None, + disp: if u == 1 { off } else { -off }, + mode: AddrMode::Offset, + size: if double { 8 } else { 4 }, + }); + if u == 0 && off == 0 { + m = minus_zero(m); + } + let mut i = Insn::new( + addr, + len, + cm(if l { conds!("vldr") } else { conds!("vstr") }, cond), + Flow::Next, + ); + i.push(Operand::Reg(vreg(first, double))) + .push(Operand::Mem(m)); + return Some(i); + } + if p == u { + return None; + } + let imm8 = bits(w, 7, 0); + if double && imm8 & 1 == 1 { + return None; // the deprecated `fldmx` and `fstmx` forms + } + // A run that would leave the register file is truncated rather than + // rejected, which is what LLVM's decoder does with it. + let mut count = if double { imm8 / 2 } else { imm8 }; + if count == 0 { + return None; + } + if double && (count > 16 || first + count > 32) { + count = 16.min(32 - first); + } else if !double && first + count > 32 { + count = 32 - first; + } + let stack = rn == 13 && wb; + // `vpush` is the decrement-before store and `vpop` the increment-after + // load, the two directions a stack needs. + let (mn, implicit) = match (l, p == 1, stack) { + (true, false, true) => (cm(conds!("vpop"), cond), true), + (false, true, true) => (cm(conds!("vpush"), cond), true), + (true, false, _) => (cm(conds!("vldmia"), cond), false), + (true, true, _) => (cm(conds!("vldmdb"), cond), false), + (false, false, _) => (cm(conds!("vstmia"), cond), false), + (false, true, _) => (cm(conds!("vstmdb"), cond), false), + }; + let mut i = Insn::new(addr, len, mn, Flow::Next); + if !implicit { + if wb { + i.push(Operand::Name(wb_reg(rn))); + } else { + i.push(Operand::Reg(reg(rn))); + } + } + i.push(vfp_list(double, first, count)); + Some(i) +} + +/// `vmov` between a pair of core registers and one double or two singles. +fn core_pair(w: u32, cond: u32, addr: Addr, len: u8, double: bool) -> Option { + let (rt, rt2) = (bits(w, 15, 12), bits(w, 19, 16)); + let vm = bits(w, 3, 0); + let m = bit(w, 5); + let first = if double { m << 4 | vm } else { vm << 1 | m }; + let mut i = Insn::new(addr, len, cm(conds!("vmov"), cond), Flow::Next); + let core = [Operand::Reg(reg(rt)), Operand::Reg(reg(rt2))]; + let vec: Vec = if double { + vec![Operand::Reg(vreg(first, true))] + } else { + vec![ + Operand::Reg(vreg(first, false)), + Operand::Reg(vreg(first + 1, false)), + ] + }; + let (a, b) = if bit(w, 20) == 1 { + (&core[..], &vec[..]) + } else { + (&vec[..], &core[..]) + }; + for op in a.iter().chain(b) { + i.push(*op); + } + Some(i) +} + +/// Bits 27:25 == 111: VFP arithmetic, the core transfers, and `svc`. +pub fn dp(w: u32, cond: u32, addr: Addr, len: u8) -> Option { + if bit(w, 24) == 1 { + let mut i = Insn::new(addr, len, cm(conds!("svc"), cond), Flow::Syscall); + i.push(Operand::UImm(bits(w, 23, 0) as u64)); + return Some(i); + } + let double = match bits(w, 11, 8) { + 0b1010 => false, + 0b1011 => true, + _ => return None, + }; + if bit(w, 4) == 1 { + return transfer(w, cond, addr, len, double); + } + arith(w, cond, addr, len, double) +} + +/// Between a core register and a single-precision one, or the status register. +fn transfer(w: u32, cond: u32, addr: Addr, len: u8, double: bool) -> Option { + if double { + return None; // the lane transfers, which are Advanced SIMD + } + let (rt, vn, n) = (bits(w, 15, 12), bits(w, 19, 16), bit(w, 7)); + let load = bit(w, 20) == 1; + match bits(w, 23, 21) { + 0b000 => { + let mut i = Insn::new(addr, len, cm(conds!("vmov"), cond), Flow::Next); + let (core, vec) = ( + Operand::Reg(reg(rt)), + Operand::Reg(vreg(vn << 1 | n, false)), + ); + if load { + i.push(core).push(vec); + } else { + i.push(vec).push(core); + } + Some(i) + } + 0b111 => { + let sys = Operand::Name(VFP_SYSREGS[vn as usize]); + // Only the status register has the flags spelling. + let flags = vn == 1; + let mut i = Insn::new( + addr, + len, + cm(if load { conds!("vmrs") } else { conds!("vmsr") }, cond), + Flow::Next, + ); + if load { + let dst = if rt == 15 && flags { + Operand::Name("APSR_nzcv") + } else { + Operand::Reg(reg(rt)) + }; + i.push(dst).push(sys); + } else { + i.push(sys).push(Operand::Reg(reg(rt))); + } + Some(i) + } + _ => None, + } +} + +/// The three-register arithmetic and the `1x11` corner that holds everything +/// else: the immediate move, the unary operations, the compares and `vcvt`. +fn arith(w: u32, cond: u32, addr: Addr, len: u8, double: bool) -> Option { + let sz = double as usize; + let num = |v: u32, hi: u32| if double { hi << 4 | v } else { v << 1 | hi }; + let d = num(bits(w, 15, 12), bit(w, 22)); + let n = num(bits(w, 19, 16), bit(w, 7)); + let m = num(bits(w, 3, 0), bit(w, 5)); + let opc3 = bit(w, 6); + let three = |mn: &'static str| { + let mut i = Insn::new(addr, len, mn, Flow::Next); + i.push(Operand::Reg(vreg(d, double))) + .push(Operand::Reg(vreg(n, double))) + .push(Operand::Reg(vreg(m, double))); + i + }; + let table = match bit(w, 23) << 2 | bits(w, 21, 20) { + 0b000 => [fp!("vmla"), fp!("vmls")], + 0b001 => [fp!("vnmls"), fp!("vnmla")], + 0b010 => [fp!("vmul"), fp!("vnmul")], + 0b011 => [fp!("vadd"), fp!("vsub")], + 0b100 if opc3 == 0 => [fp!("vdiv"), fp!("vdiv")], + 0b101 => [fp!("vfnms"), fp!("vfnma")], + 0b110 => [fp!("vfma"), fp!("vfms")], + 0b111 => return other(w, cond, addr, len, double), + _ => return None, + }; + Some(three(cm(table[opc3 as usize][sz], cond))) +} + +/// The `opc1 == 1x11` group, where four more bits pick the operation. +fn other(w: u32, cond: u32, addr: Addr, len: u8, double: bool) -> Option { + let sz = double as usize; + let num = |v: u32, hi: u32, dbl: bool| if dbl { hi << 4 | v } else { v << 1 | hi }; + let d = num(bits(w, 15, 12), bit(w, 22), double); + let m = num(bits(w, 3, 0), bit(w, 5), double); + let opc2 = bits(w, 19, 16); + let opc3 = bits(w, 7, 6); + + if opc3 & 1 == 0 { + // The immediate, whose eight bits expand the same way at both widths. + let imm8 = bits(w, 19, 16) << 4 | bits(w, 3, 0); + let mut i = Insn::new(addr, len, cm(fp!("vmov")[sz], cond), Flow::Next); + i.push(Operand::Reg(vreg(d, double))) + .push(Operand::FpImm(expand_imm(imm8))); + return Some(i); + } + let unary = |mn: &'static str| { + let mut i = Insn::new(addr, len, mn, Flow::Next); + i.push(Operand::Reg(vreg(d, double))) + .push(Operand::Reg(vreg(m, double))); + i + }; + match (opc2, opc3) { + (0b0000, 0b01) => Some(unary(cm(fp!("vmov")[sz], cond))), + (0b0000, 0b11) => Some(unary(cm(fp!("vabs")[sz], cond))), + (0b0001, 0b01) => Some(unary(cm(fp!("vneg")[sz], cond))), + (0b0001, 0b11) => Some(unary(cm(fp!("vsqrt")[sz], cond))), + (0b0100, 0b01) => Some(unary(cm(fp!("vcmp")[sz], cond))), + (0b0100, 0b11) => Some(unary(cm(fp!("vcmpe")[sz], cond))), + (0b0101, 0b01) | (0b0101, 0b11) => { + let mn = if opc3 == 0b01 { + fp!("vcmp")[sz] + } else { + fp!("vcmpe")[sz] + }; + let mut i = Insn::new(addr, len, cm(mn, cond), Flow::Next); + i.push(Operand::Reg(vreg(d, double))) + .push(Operand::Count(0)); + Some(i) + } + (0b0111, 0b11) => { + // Between the two precisions: the destination is the other one. + const CVT: [&[&str; 16]; 2] = [conds!("vcvt", ".f64.f32"), conds!("vcvt", ".f32.f64")]; + let dst = num(bits(w, 15, 12), bit(w, 22), !double); + let mut i = Insn::new(addr, len, cm(CVT[sz], cond), Flow::Next); + i.push(Operand::Reg(vreg(dst, !double))) + .push(Operand::Reg(vreg(m, double))); + Some(i) + } + (0b1000, 0b01) | (0b1000, 0b11) => { + const CVT: [[&[&str; 16]; 2]; 2] = [ + [conds!("vcvt", ".f32.u32"), conds!("vcvt", ".f64.u32")], + [conds!("vcvt", ".f32.s32"), conds!("vcvt", ".f64.s32")], + ]; + let src = num(bits(w, 3, 0), bit(w, 5), false); + let mut i = Insn::new( + addr, + len, + cm(CVT[(opc3 >> 1) as usize][sz], cond), + Flow::Next, + ); + i.push(Operand::Reg(vreg(d, double))) + .push(Operand::Reg(vreg(src, false))); + Some(i) + } + (0b1100, _) | (0b1101, _) => { + const CVT: [[&[&str; 16]; 2]; 2] = [ + [conds!("vcvtr", ".u32.f32"), conds!("vcvtr", ".u32.f64")], + [conds!("vcvtr", ".s32.f32"), conds!("vcvtr", ".s32.f64")], + ]; + const CVTZ: [[&[&str; 16]; 2]; 2] = [ + [conds!("vcvt", ".u32.f32"), conds!("vcvt", ".u32.f64")], + [conds!("vcvt", ".s32.f32"), conds!("vcvt", ".s32.f64")], + ]; + let signed = (opc2 & 1) as usize; + let table = if opc3 >> 1 == 1 { &CVTZ } else { &CVT }; + let dst = num(bits(w, 15, 12), bit(w, 22), false); + let mut i = Insn::new(addr, len, cm(table[signed][sz], cond), Flow::Next); + i.push(Operand::Reg(vreg(dst, false))) + .push(Operand::Reg(vreg(m, double))); + Some(i) + } + _ => None, + } +} + +/// `VFPExpandImm`, as the double it names; the single-precision encoding names +/// the same value with fewer bits behind it. +fn expand_imm(imm8: u32) -> u64 { + let sign = (imm8 >> 7) as u64 & 1; + let b6 = (imm8 >> 6) & 1; + let exp = ((1 - b6) << 10 | if b6 == 1 { 0xff << 2 } else { 0 } | (imm8 >> 4) & 3) as u64; + sign << 63 | exp << 52 | ((imm8 & 0xf) as u64) << 48 +} diff --git a/crates/e5r-arch/src/insn.rs b/crates/e5r-arch/src/insn.rs new file mode 100644 index 0000000..36cc705 --- /dev/null +++ b/crates/e5r-arch/src/insn.rs @@ -0,0 +1,460 @@ +//! The decoded instruction, shared by every decoder. +//! +//! Allocation-free: operands live in a fixed array, so decoding a million +//! instructions costs no heap traffic. Five is enough for every AArch64 and +//! x86 form, since a shift or extend rides inside its register operand. + +use std::fmt; + +use e5r_core::Addr; + +/// Register width in bits. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Width { + /// 8-bit. + W8, + /// 16-bit. + W16, + /// 32-bit. + W32, + /// 64-bit. + W64, + /// 128-bit, a vector register. + W128, +} + +impl Width { + /// Size in bytes. + pub const fn bytes(self) -> u64 { + match self { + Width::W8 => 1, + Width::W16 => 2, + Width::W32 => 4, + Width::W64 => 8, + Width::W128 => 16, + } + } +} + +/// What kind of register an operand names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RegClass { + /// General purpose. + Gpr, + /// The high byte of one of the first four x86 registers: `ah`, `ch`, `dh`, + /// `bh`. A separate bank because `ah` is not the low byte of `rax`, and + /// code that treats it as one is wrong in a way that is hard to see. + GprHigh, + /// An x86 segment register. + Seg, + /// The stack pointer, which AArch64 encodes as register 31 in some forms. + Sp, + /// The zero register, which is register 31 in the other forms. + Zr, + /// Vector or floating point. + Vec, + /// A system register. + Sys, + /// The program counter, where an architecture exposes it. + Pc, + /// The condition flags. + Flags, +} + +/// One register. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Reg { + /// Which bank. + pub class: RegClass, + /// Number within the bank. + pub num: u8, + /// Width being used, which can be narrower than the register. + pub width: Width, +} + +impl Reg { + /// A general purpose register. + pub const fn gpr(num: u8, width: Width) -> Reg { + Reg { + class: RegClass::Gpr, + num, + width, + } + } + + /// A vector or floating point register. + pub const fn vec(num: u8, width: Width) -> Reg { + Reg { + class: RegClass::Vec, + num, + width, + } + } + + /// True when writes to this register go nowhere. + pub fn is_zero(self) -> bool { + self.class == RegClass::Zr + } +} + +/// Shift applied to a register operand. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Shift { + /// Logical left. + Lsl, + /// Logical right. + Lsr, + /// Arithmetic right. + Asr, + /// Rotate right. + Ror, + /// Shift left filling with ones, which only the SIMD immediate forms use. + Msl, +} + +impl Shift { + /// The lowercase name. + pub const fn as_str(self) -> &'static str { + match self { + Shift::Lsl => "lsl", + Shift::Lsr => "lsr", + Shift::Asr => "asr", + Shift::Ror => "ror", + Shift::Msl => "msl", + } + } +} + +/// Extension applied to a register operand before use. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Extend { + /// Unsigned byte. + Uxtb, + /// Unsigned halfword. + Uxth, + /// Unsigned word. + Uxtw, + /// Unsigned doubleword, which is a no-op and prints as `lsl` when shifted. + Uxtx, + /// Signed byte. + Sxtb, + /// Signed halfword. + Sxth, + /// Signed word. + Sxtw, + /// Signed doubleword. + Sxtx, + /// A plain shift with no extension, for indexed addressing. + Lsl, + /// `lsl #0`, which the architecture prints explicitly when the scale bit is + /// set even though the amount is zero. + LslZero, +} + +impl Extend { + /// The lowercase name. + pub const fn as_str(self) -> &'static str { + match self { + Extend::Uxtb => "uxtb", + Extend::Uxth => "uxth", + Extend::Uxtw => "uxtw", + Extend::Uxtx => "uxtx", + Extend::Sxtb => "sxtb", + Extend::Sxth => "sxth", + Extend::Sxtw => "sxtw", + Extend::Sxtx => "sxtx", + Extend::Lsl | Extend::LslZero => "lsl", + } + } +} + +/// How a memory operand computes its address. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddrMode { + /// `[base, #disp]`. + Offset, + /// `[base, #disp]!`, base updated before the access. + PreIndex, + /// `[base], #disp`, base updated after the access. + PostIndex, +} + +/// A memory reference. +/// +/// General enough for both architectures: AArch64 always has a base and never +/// a segment, x86 can have neither base nor index (`[0x1234]`) and can scale +/// its index by 1, 2, 4 or 8. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Mem { + /// Segment override, on the architectures that have one. + pub seg: Option, + /// Base register, absent for an absolute or index-only address. + pub base: Option, + /// Index register with its extension and shift amount. The shift is the + /// log2 of x86's scale. + pub index: Option<(Reg, Extend, u8)>, + /// Constant displacement. + pub disp: i64, + /// Whether the base is updated. + pub mode: AddrMode, + /// Bytes transferred, for reporting and for data typing. + pub size: u64, +} + +impl Mem { + /// True when the base is the program counter, so the displacement is + /// relative to the end of the instruction. + pub fn is_pc_relative(&self) -> bool { + self.base.map(|b| b.class) == Some(RegClass::Pc) + } + + /// The absolute address a pc-relative operand names, given where the + /// instruction ends. + pub fn pc_target(&self, insn_end: Addr) -> Option { + self.is_pc_relative() + .then(|| insn_end.wrapping_offset(self.disp)) + } + + /// A plain `[base + disp]`. + pub const fn base_disp(base: Reg, disp: i64, size: u64) -> Mem { + Mem { + seg: None, + base: Some(base), + index: None, + disp, + mode: AddrMode::Offset, + size, + } + } +} + +/// How a SIMD register's bits are divided into lanes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Lanes { + /// Eight bytes. + B8, + /// Sixteen bytes. + B16, + /// Four halfwords. + H4, + /// Eight halfwords. + H8, + /// Two words. + S2, + /// Four words. + S4, + /// One doubleword. + D1, + /// Two doublewords. + D2, + /// One quadword. + Q1, +} + +impl Lanes { + /// The suffix a listing prints, as in `v0.16b`. + pub const fn as_str(self) -> &'static str { + match self { + Lanes::B8 => "8b", + Lanes::B16 => "16b", + Lanes::H4 => "4h", + Lanes::H8 => "8h", + Lanes::S2 => "2s", + Lanes::S4 => "4s", + Lanes::D1 => "1d", + Lanes::D2 => "2d", + Lanes::Q1 => "1q", + } + } +} + +/// A condition code, in the architecture's own numbering. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Cond(pub u8); + +/// One operand. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Operand { + /// A plain register. + Reg(Reg), + /// A register with a shift applied. + Shifted(Reg, Shift, u8), + /// A register with an extension applied. + Extended(Reg, Extend, u8), + /// A constant, printed in hex. + Imm(i64), + /// A bit pattern, printed unsigned at the operand's width. A logical + /// immediate of all-ones is `0xffffffffffffffff`, not `-0x1`. + UImm(u64), + /// A count, index or bit position, printed in decimal. Shift amounts, bit + /// numbers and bitfield widths read as decimal everywhere, so they are a + /// different operand rather than a formatter special case. + Count(i64), + /// A bare shift qualifier, as in `movk w0, #0x5a5a, lsl #16`. + ShiftOp(Shift, u8), + /// A named option, as in `bti c`. The decoder supplies the name. + Name(&'static str), + /// A floating point literal, held as its IEEE-754 double bit pattern so the + /// operand stays `Eq` and the instruction stays comparable. + FpImm(u64), + /// A SIMD register viewed as lanes, as in `v0.16b`. + Vector(u8, Lanes), + /// One lane of a SIMD register, as in `v0.s[2]`. + VectorLane(u8, Width, u8), + /// A list of consecutive SIMD registers, as in `{v0.16b, v1.16b}`. The + /// fields are the first register, how many there are, and the lanes. + VectorList(u8, u8, Lanes), + /// An absolute address, the resolved target of a branch or a pc-relative + /// computation. Kept distinct from `Imm` so analysis can find targets + /// without re-deriving them. + Addr(Addr), + /// A memory reference. + Mem(Mem), + /// A condition code. + Cond(Cond), + /// A system register or barrier option, printed by the decoder's namer. + Sys(u32), +} + +/// What the instruction does to control flow. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Flow { + /// Execution continues at the next instruction. + Next, + /// Unconditional branch to a known address. + Branch(Addr), + /// Branch taken or not; both successors are live. + CondBranch(Addr), + /// Unconditional branch through a register. + IndirectBranch, + /// Call to a known address; the callee is expected to return. + Call(Addr), + /// Call through a register. + IndirectCall, + /// Return to the caller. + Return, + /// Execution stops here: a breakpoint or an undefined instruction. + Trap, + /// A system call, which returns. + Syscall, +} + +impl Flow { + /// True when the next instruction in address order is a successor. + pub fn falls_through(self) -> bool { + matches!( + self, + Flow::Next | Flow::CondBranch(_) | Flow::Call(_) | Flow::IndirectCall | Flow::Syscall + ) + } + + /// The direct target, when there is one. + pub fn target(self) -> Option { + match self { + Flow::Branch(a) | Flow::CondBranch(a) | Flow::Call(a) => Some(a), + _ => None, + } + } + + /// True when this ends a basic block. + pub fn ends_block(self) -> bool { + !matches!(self, Flow::Next) + } + + /// True when this transfers to a callee. + pub fn is_call(self) -> bool { + matches!(self, Flow::Call(_) | Flow::IndirectCall) + } +} + +/// The most operands any supported encoding uses. +/// +/// Six, which is what ARM's `mcr` and `mrc` take: a coprocessor, an opcode, +/// a core register, two coprocessor registers and a second opcode. Everything +/// else needs fewer. [`Insn::push`] drops anything past this rather than +/// growing, so a decoder that needs more would silently produce an instruction +/// missing an operand -- which is why this is the widest encoding and not a +/// round number. +pub const MAX_OPERANDS: usize = 6; + +/// A decoded instruction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Insn { + /// Where it starts. + pub addr: Addr, + /// How many bytes it occupies. + pub len: u8, + /// The mnemonic, already resolved through any alias the architecture + /// prefers, so `sbfm` with the right fields arrives as `asr`. + pub mnemonic: &'static str, + /// What the flow analysis needs to know. + pub flow: Flow, + /// A prefix that modifies the whole instruction rather than an operand: + /// x86's `lock`, `rep` and `repne`. Printed ahead of the mnemonic. + pub prefix: Option<&'static str>, + ops: [Operand; MAX_OPERANDS], + n_ops: u8, +} + +impl Insn { + /// Build an instruction with no operands. + pub fn new(addr: Addr, len: u8, mnemonic: &'static str, flow: Flow) -> Insn { + Insn { + addr, + len, + mnemonic, + flow, + prefix: None, + ops: [Operand::Imm(0); MAX_OPERANDS], + n_ops: 0, + } + } + + /// Append an operand. Beyond [`MAX_OPERANDS`] it is dropped, which cannot + /// happen for any encoding the decoders produce. + pub fn push(&mut self, op: Operand) -> &mut Self { + if (self.n_ops as usize) < MAX_OPERANDS { + self.ops[self.n_ops as usize] = op; + self.n_ops += 1; + } + self + } + + /// Replace one operand, for a spelling that changes how it reads. + pub fn set_operand(&mut self, n: usize, op: Operand) { + if n < self.n_ops as usize { + self.ops[n] = op; + } + } + + /// The operands. + pub fn operands(&self) -> &[Operand] { + &self.ops[..self.n_ops as usize] + } + + /// One past the last byte. + pub fn end(&self) -> Addr { + self.addr.wrapping_offset(self.len as i64) + } + + /// The address of the next instruction in address order. + pub fn next(&self) -> Addr { + self.end() + } + + /// Every address this instruction can transfer to directly. + pub fn successors(&self) -> impl Iterator { + let target = match self.flow { + Flow::Branch(a) | Flow::CondBranch(a) => Some(a), + _ => None, + }; + let fallthrough = self.flow.falls_through().then(|| self.next()); + target.into_iter().chain(fallthrough) + } +} + +impl fmt::Display for Insn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.mnemonic) + } +} diff --git a/crates/e5r-arch/src/lib.rs b/crates/e5r-arch/src/lib.rs new file mode 100644 index 0000000..9adb851 --- /dev/null +++ b/crates/e5r-arch/src/lib.rs @@ -0,0 +1,47 @@ +//! Instruction decoders. +//! +//! One [`Insn`] type for every architecture, so analysis is written once. +//! Decoders are hand-written for the architectures where speed matters and +//! resolve aliases themselves: `sbfm` with a full-width extraction is `asr`, +//! and analysis that has to know both spellings has a bug waiting in it. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod aarch64; +pub mod arm; +pub mod insn; +pub mod x86; + +pub use insn::{ + AddrMode, Cond, Extend, Flow, Insn, Lanes, MAX_OPERANDS, Mem, Operand, Reg, RegClass, Shift, + Width, +}; + +use e5r_core::{Addr, Arch}; + +/// Decode one instruction for `arch`, or `None` when the bytes are not an +/// allocated encoding. +pub fn decode(arch: &Arch, bytes: &[u8], addr: Addr) -> Option { + match arch { + Arch::AArch64 => aarch64::decode(bytes, addr), + Arch::X86_64 => x86::decode(bytes, addr), + Arch::X86 => x86::decode32(bytes, addr), + Arch::Arm => arm::decode(bytes, addr), + // Outside an IT block, which is where a caller that hands over one + // address at a time always is. `arm::Thumb` walks a run and carries + // the state across it; this is the single-instruction view. + Arch::Thumb => arm::decode_thumb(bytes, addr, arm::ItState::default()).map(|(i, _)| i), + _ => None, + } +} + +/// Render an instruction the way the architecture's usual disassembler does. +pub fn format(arch: &Arch, i: &Insn, objdump: bool) -> String { + match arch { + Arch::AArch64 => aarch64::format(i, aarch64::text::Style { objdump }), + Arch::X86_64 | Arch::X86 => x86::format(i, x86::Style::default()), + Arch::Arm | Arch::Thumb => arm::format(i), + _ => i.mnemonic.to_string(), + } +} diff --git a/crates/e5r-arch/src/x86/mod.rs b/crates/e5r-arch/src/x86/mod.rs new file mode 100644 index 0000000..eb6a0cf --- /dev/null +++ b/crates/e5r-arch/src/x86/mod.rs @@ -0,0 +1,1587 @@ +//! x86 decoder, 64-bit and 32-bit. +//! +//! Variable length, so decoding is a walk: legacy prefixes, then REX, then one +//! to three opcode bytes, then ModRM and SIB, then a displacement, then an +//! immediate. Each stage can end the instruction, and the length is whatever +//! the walk consumed. +//! +//! The two modes share one table and differ in [`Mode`]. The difference is not +//! cosmetic: the opcodes 32-bit mode spends on `inc`/`dec`, `pusha` and the +//! BCD instructions are REX prefixes in long mode, so a decoder that guesses +//! the mode desynchronizes on the first byte rather than misnaming one +//! instruction. + +pub mod table; +pub mod text; + +pub use text::{Style, format}; + +use e5r_core::Addr; + +use crate::insn::{AddrMode, Flow, Insn, Mem, Operand, Reg, RegClass, Width}; +use table::{CC, Entry, GROUPS, Op}; + +/// Which mode the processor is in, which the caller knows and the bytes do not. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + /// 64-bit mode: REX prefixes, 64-bit addresses, and no one-byte `inc`. + #[default] + Long, + /// 32-bit protected mode, which is what an i386 image holds. + Protected, +} + +/// The prefixes and fields gathered before the opcode is looked up. +#[derive(Clone, Copy)] +struct Prefixes { + /// The mode being decoded for. + mode: Mode, + /// `0x66`, which halves the operand size or selects an SSE variant. + opsize: bool, + /// `0x67`, which halves the address size. + addrsize: bool, + /// `0xf3`. + rep: bool, + /// `0xf2`. + repne: bool, + /// `0xf0`. + lock: bool, + /// A segment override, as a register number, if any. + seg: Option, + /// REX.W: 64-bit operand size. + w: bool, + /// REX.R: extends ModRM reg. + r: bool, + /// REX.X: extends the SIB index. + x: bool, + /// REX.B: extends ModRM r/m, SIB base, or the opcode register. + b: bool, + /// Whether any REX byte was present, which changes which byte registers + /// the low three bits name. + rex: bool, + /// Whether the last `0xf2` or `0xf3` sat where a mandatory prefix has to + /// sit: immediately before the opcode, or before the `0x66` or REX byte + /// that is. Anywhere else it is only a repeat prefix, and llvm then spells + /// it out even on a two-byte opcode that ignores it. + repeat_is_mandatory: bool, + /// The opcode defaults to 64-bit operands in long mode, so `push rbp` + /// needs no REX.W. True for push, pop, near call and near jmp. + default64: bool, + /// The opcode names a register through r/m whatever the mod field holds, + /// which is how `mov cr0, eax` encodes: the mod bits are ignored, not + /// reserved, so decoding them as memory reads displacement bytes that are + /// really the next instruction. + force_reg_rm: bool, +} + +impl Prefixes { + fn new(mode: Mode) -> Self { + Prefixes { + mode, + opsize: false, + addrsize: false, + rep: false, + repne: false, + lock: false, + seg: None, + w: false, + r: false, + x: false, + b: false, + rex: false, + repeat_is_mandatory: false, + default64: false, + force_reg_rm: false, + } + } + + fn long(&self) -> bool { + self.mode == Mode::Long + } + + /// The operand size in bytes for an `Ev`-style operand. + fn opsize_bytes(&self) -> u64 { + if !self.long() { + // No REX and nothing defaults to 64-bit, so `0x66` is the only + // thing that can move the size off the 32-bit default. + return if self.opsize { 2 } else { 4 }; + } + if self.w || (self.default64 && !self.opsize) { + 8 + } else if self.opsize { + 2 + } else { + 4 + } + } + + /// The address size in bytes. `0x67` halves it in both modes, which means + /// it selects 32-bit addressing in long mode and 16-bit addressing here. + fn addr_bytes(&self) -> u64 { + match (self.long(), self.addrsize) { + (true, false) => 8, + (true, true) => 4, + (false, false) => 4, + (false, true) => 2, + } + } + + fn addr_width(&self) -> Width { + width_of(self.addr_bytes()) + } + + /// The size of an operand the table forces to 64 bits. In 32-bit mode + /// there is no such width, so those operands are doublewords. + fn forced_qword(&self) -> u64 { + if self.long() { 8 } else { 4 } + } + + fn width(&self) -> Width { + match self.opsize_bytes() { + 8 => Width::W64, + 2 => Width::W16, + _ => Width::W32, + } + } +} + +/// A cursor over the instruction's bytes. +struct Cursor<'a> { + b: &'a [u8], + at: usize, +} + +impl<'a> Cursor<'a> { + fn u8(&mut self) -> Option { + let v = *self.b.get(self.at)?; + self.at += 1; + Some(v) + } + + fn peek(&self) -> Option { + self.b.get(self.at).copied() + } + + fn i8(&mut self) -> Option { + Some(self.u8()? as i8 as i64) + } + + fn u16(&mut self) -> Option { + let a = self.u8()? as u64; + let b = self.u8()? as u64; + Some(a | (b << 8)) + } + + fn i16(&mut self) -> Option { + Some(self.u16()? as u16 as i16 as i64) + } + + fn u32(&mut self) -> Option { + let mut v = 0u64; + for i in 0..4 { + v |= (self.u8()? as u64) << (8 * i); + } + Some(v) + } + + fn i32(&mut self) -> Option { + Some(self.u32()? as u32 as i32 as i64) + } + + fn u64(&mut self) -> Option { + let mut v = 0u64; + for i in 0..8 { + v |= (self.u8()? as u64) << (8 * i); + } + Some(v) + } +} + +/// A general purpose register at a width, honouring the REX rule for byte +/// registers: without REX, numbers 4 to 7 name `ah`, `ch`, `dh` and `bh`. +fn gpr(num: u8, width: Width, rex: bool) -> Reg { + if width == Width::W8 && !rex && (4..8).contains(&num) { + return Reg { + class: RegClass::GprHigh, + num: num - 4, + width, + }; + } + Reg::gpr(num, width) +} + +/// The MMX register file, which prints by name: it aliases the x87 stack and +/// [`RegClass`] has no class of its own for it. +const MM: [&str; 8] = ["mm0", "mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm7"]; + +fn xmm(num: u8) -> Reg { + Reg { + class: RegClass::Vec, + num, + width: Width::W128, + } +} + +/// Debug register names, which print by name because [`RegClass`] has a single +/// system class and it already spells itself `cr`. +const DR: [&str; 16] = [ + "dr0", "dr1", "dr2", "dr3", "dr4", "dr5", "dr6", "dr7", "dr8", "dr9", "dr10", "dr11", "dr12", + "dr13", "dr14", "dr15", +]; + +fn width_of(bytes: u64) -> Width { + match bytes { + 1 => Width::W8, + 2 => Width::W16, + 8 => Width::W64, + 16 => Width::W128, + _ => Width::W32, + } +} + +/// Decoded ModRM, SIB and displacement. +struct ModRm { + reg: u8, + /// The r/m operand, already built. `None` until [`decode_modrm`] runs. + rm: RmOperand, +} + +enum RmOperand { + Reg(u8), + Mem(Mem), +} + +/// The base and index register pairs of 16-bit addressing, indexed by r/m. +/// `NONE16` marks the half of a pair that is absent. These forms exist only +/// under `0x67` in 32-bit mode, where they are the survivors of real mode. +const NONE16: u8 = 0xff; +const BASE16: [(u8, u8); 8] = [ + (3, 6), + (3, 7), + (5, 6), + (5, 7), + (6, NONE16), + (7, NONE16), + (5, NONE16), + (3, NONE16), +]; + +/// Decode ModRM plus any SIB and displacement. +fn decode_modrm(c: &mut Cursor<'_>, p: &Prefixes, size: u64) -> Option { + let modrm = c.u8()?; + let md = if p.force_reg_rm { 3 } else { modrm >> 6 }; + let reg = ((modrm >> 3) & 7) | if p.r { 8 } else { 0 }; + let rm = modrm & 7; + + if md == 3 { + return Some(ModRm { + reg, + rm: RmOperand::Reg(rm | if p.b { 8 } else { 0 }), + }); + } + + let seg = p.seg.map(|s| Reg { + class: RegClass::Seg, + num: s, + width: Width::W16, + }); + + if p.addr_bytes() == 2 { + // 16-bit addressing: no SIB, a fixed set of base and index pairs, and + // `[bp]` with mod 0 spelled as an absolute 16-bit displacement. + let (b, x) = BASE16[rm as usize]; + let mut base = None; + let mut index = None; + let mut disp = 0i64; + if rm == 6 && md == 0 { + disp = c.i16()?; + } else { + base = Some(Reg::gpr(b, Width::W16)); + if x != NONE16 { + index = Some((Reg::gpr(x, Width::W16), crate::insn::Extend::Lsl, 0)); + } + } + match md { + 1 => disp = disp.wrapping_add(c.i8()?), + 2 => disp = disp.wrapping_add(c.i16()?), + _ => {} + } + return Some(ModRm { + reg, + rm: RmOperand::Mem(Mem { + seg, + base, + index, + disp, + mode: AddrMode::Offset, + size, + }), + }); + } + + let addr_width = p.addr_width(); + let mut base: Option = None; + let mut index: Option<(Reg, crate::insn::Extend, u8)> = None; + let mut disp: i64 = 0; + let mut rip_relative = false; + + if rm == 4 { + // A SIB byte follows. + let sib = c.u8()?; + let scale = sib >> 6; + let idx = ((sib >> 3) & 7) | if p.x { 8 } else { 0 }; + let bse = (sib & 7) | if p.b { 8 } else { 0 }; + // Index 4 without REX.X means no index at all, and llvm then prints + // the zero register `eiz` in its place, so that the scale stays + // visible. The one exception is the canonical `[esp]` form, scale one + // over the stack pointer, which is how every `[esp + n]` encodes. + if idx != 4 { + index = Some((gpr(idx, addr_width, true), crate::insn::Extend::Lsl, scale)); + } else if scale != 0 || sib & 7 != 4 { + let no_base = sib & 7 == 5 && md == 0; + if !(p.long() && no_base) { + index = Some(( + Reg { + class: RegClass::Zr, + num: 0, + width: addr_width, + }, + crate::insn::Extend::Lsl, + scale, + )); + } + } + // Base 5 with mod 0 means a 32-bit displacement and no base. + if sib & 7 == 5 && md == 0 { + disp = c.i32()?; + } else { + base = Some(gpr(bse, addr_width, true)); + } + } else if rm == 5 && md == 0 { + // RIP-relative in long mode. In 32-bit mode the same encoding is the + // plain absolute address it always was. + disp = c.i32()?; + rip_relative = p.long(); + } else { + base = Some(gpr(rm | if p.b { 8 } else { 0 }, addr_width, true)); + } + + match md { + 1 => disp = disp.wrapping_add(c.i8()?), + 2 => disp = disp.wrapping_add(c.i32()?), + _ => {} + } + + if rip_relative { + // The displacement stays as the bytes encode it, relative to the end + // of the instruction. Resolving it here would make the text disagree + // with the encoding; `Mem::rip_target` does it for analysis. + base = Some(Reg { + class: RegClass::Pc, + num: 0, + // `0x67` shrinks the program counter the displacement is taken + // from, and llvm prints `eip` rather than `rip` when it does. + width: addr_width, + }); + } + + Some(ModRm { + reg, + rm: RmOperand::Mem(Mem { + seg, + base, + index, + disp, + mode: AddrMode::Offset, + size, + }), + }) +} + +/// The x87 stack registers, which print by name: [`RegClass`] has no class +/// for them and the whole file is eight registers wide. +const ST: [&str; 8] = [ + "st(0)", "st(1)", "st(2)", "st(3)", "st(4)", "st(5)", "st(6)", "st(7)", +]; + +/// Which operands an x87 register form prints. +enum StOps { + /// No operands. + None, + /// One stack register. + Sti, + /// `st` and a stack register. + StSti, + /// A stack register and `st`. + StiSt, + /// `ax`, which only `fnstsw` uses. + Ax, +} + +/// The register forms of the x87 escapes: `0xd8` to `0xdf` with a ModRM byte +/// of 0xc0 or above. The memory forms go through [`table::X87_MEM`] and the +/// ordinary ModRM walk; these do not, because the ModRM byte is an opcode +/// extension rather than an operand specifier. +fn x87_reg(op: u8, m: u8) -> Option<(&'static str, StOps)> { + let reg = ((m >> 3) & 7) as usize; + let name = |n: &'static str| if n.is_empty() { None } else { Some(n) }; + Some(match op { + 0xd8 => { + let n = name(table::X87_D8_REG[reg])?; + // `fcom` and `fcomp` read the stack top without naming it. + let form = if matches!(reg, 2 | 3) { + StOps::Sti + } else { + StOps::StSti + }; + (n, form) + } + 0xd9 => match m { + 0xc0..=0xc7 => ("fld", StOps::Sti), + 0xc8..=0xcf => ("fxch", StOps::Sti), + 0xd0 => ("fnop", StOps::None), + 0xe0..=0xff => (name(table::X87_D9_E0[(m - 0xe0) as usize])?, StOps::None), + _ => return None, + }, + 0xda => match m { + 0xc0..=0xdf => (fcmov(false, reg)?, StOps::StSti), + 0xe9 => ("fucompp", StOps::None), + _ => return None, + }, + 0xdb => match m { + 0xc0..=0xdf => (fcmov(true, reg)?, StOps::StSti), + 0xe2 => ("fnclex", StOps::None), + 0xe3 => ("fninit", StOps::None), + 0xe8..=0xef => ("fucomi", StOps::StSti), + 0xf0..=0xf7 => ("fcomi", StOps::StSti), + _ => return None, + }, + 0xdc => (name(table::X87_DC_REG[reg])?, StOps::StiSt), + 0xdd => (name(table::X87_DD_REG[reg])?, StOps::Sti), + 0xde => match m { + 0xd9 => ("fcompp", StOps::None), + 0xd0..=0xdf => return None, + _ => (name(table::X87_DE_REG[reg])?, StOps::StiSt), + }, + 0xdf => match m { + 0xc0..=0xc7 => ("ffreep", StOps::Sti), + 0xe0 => ("fnstsw", StOps::Ax), + 0xe8..=0xef => ("fucompi", StOps::StSti), + 0xf0..=0xf7 => ("fcompi", StOps::StSti), + _ => return None, + }, + _ => return None, + }) +} + +/// The conditional move names, which differ only by an `n`. +fn fcmov(negated: bool, reg: usize) -> Option<&'static str> { + const P: [&str; 4] = ["fcmovb", "fcmove", "fcmovbe", "fcmovu"]; + const N: [&str; 4] = ["fcmovnb", "fcmovne", "fcmovnbe", "fcmovnu"]; + let _ = table::X87_FCMOV; + if negated { N.get(reg) } else { P.get(reg) }.copied() +} + +/// Decode one of the eight x87 escapes. +fn decode_x87(c: &mut Cursor<'_>, p: &Prefixes, op: u8, addr: Addr) -> Option { + let prefix = if p.lock { + Some("lock") + } else if p.rep { + Some("rep") + } else if p.repne { + Some("repne") + } else { + None + }; + let m = c.peek()?; + if m >= 0xc0 { + c.at += 1; + let (name, form) = x87_reg(op, m)?; + let mut i = Insn::new(addr, c.at as u8, name, Flow::Next); + let sti = Operand::Name(ST[(m & 7) as usize]); + match form { + StOps::None => {} + StOps::Sti => { + i.push(sti); + } + StOps::StSti => { + i.push(Operand::Name("st")); + i.push(sti); + } + StOps::StiSt => { + i.push(sti); + i.push(Operand::Name("st")); + } + StOps::Ax => { + i.push(Operand::Reg(Reg::gpr(0, Width::W16))); + } + } + i.prefix = prefix_text( + prefix, + names_addr_size(p, &i).then_some(addr_prefix_name(p)), + ); + return Some(i); + } + let (name, size) = table::X87_MEM[(op - 0xd8) as usize][((m >> 3) & 7) as usize]; + if name.is_empty() { + return None; + } + let rm = decode_modrm(c, p, size as u64)?; + let RmOperand::Mem(mem) = rm.rm else { + return None; + }; + let mut i = Insn::new(addr, c.at as u8, name, Flow::Next); + i.push(Operand::Mem(mem)); + i.prefix = prefix_text( + prefix, + names_addr_size(p, &i).then_some(addr_prefix_name(p)), + ); + Some(i) +} + +/// Decode one instruction at `addr`, in 64-bit mode. +pub fn decode(bytes: &[u8], addr: Addr) -> Option { + decode_in(Mode::Long, bytes, addr) +} + +/// Decode one instruction at `addr`, in 32-bit protected mode. +pub fn decode32(bytes: &[u8], addr: Addr) -> Option { + decode_in(Mode::Protected, bytes, addr) +} + +/// Decode one instruction at `addr` in `mode`. +pub fn decode_in(mode: Mode, bytes: &[u8], addr: Addr) -> Option { + let mut c = Cursor { b: bytes, at: 0 }; + let mut p = Prefixes::new(mode); + + // Legacy prefixes, then at most one REX immediately before the opcode. + loop { + match c.peek()? { + 0x66 => p.opsize = true, + 0x67 => p.addrsize = true, + 0xf0 => p.lock = true, + // The last repeat prefix wins, and llvm reports that one. + b @ (0xf2 | 0xf3) => { + p.repne = b == 0xf2; + p.rep = b == 0xf3; + p.repeat_is_mandatory = match c.b.get(c.at + 1) { + Some(0x0f | 0x66) => true, + Some(n) => mode == Mode::Long && (0x40..0x50).contains(n), + None => false, + }; + } + 0x2e => p.seg = Some(1), + 0x36 => p.seg = Some(2), + 0x3e => p.seg = Some(3), + 0x26 => p.seg = Some(0), + 0x64 => p.seg = Some(4), + 0x65 => p.seg = Some(5), + _ => break, + } + c.at += 1; + } + if let Some(b) = c.peek() { + // Only long mode has REX. In 32-bit mode these bytes are `inc` and + // `dec`, and consuming one as a prefix would eat the next opcode. + if p.long() && (0x40..0x50).contains(&b) { + p.rex = true; + p.w = b & 8 != 0; + p.r = b & 4 != 0; + p.x = b & 2 != 0; + p.b = b & 1 != 0; + c.at += 1; + } + } + + let op = c.u8()?; + if (0xd8..=0xdf).contains(&op) { + return decode_x87(&mut c, &p, op, addr); + } + let (entry, two_byte, opcode) = if op == 0x0f { + let op2 = c.u8()?; + // The three-byte maps, which is where SSSE3 and SSE4 live. + if op2 == 0x38 || op2 == 0x3a { + let op3 = c.u8()?; + // With no prefix at all these maps hold the MMX forms; 0x66 + // selects the SSE ones, which is where the useful half lives. + let mmx = !p.opsize; + let t = match (op2 == 0x38, mmx) { + (true, false) => table::THREE_BYTE_38_66[op3 as usize], + (true, true) => table::THREE_BYTE_38[op3 as usize], + (false, false) => table::THREE_BYTE_3A_66[op3 as usize], + (false, true) => table::THREE_BYTE_3A[op3 as usize], + }; + if t.m.is_empty() { + return None; + } + p.opsize = false; + return build(&mut c, &p, t, op3, true, addr, bytes); + } + // `endbr32` and `endbr64` are the only two allocations at 0x0f 0x1e, + // and the ModRM byte picks between them rather than describing an + // operand, so neither the table nor the operand walk can express it. + if op2 == 0x1e && p.rep { + let name = match c.peek() { + Some(0xfa) => "endbr64", + Some(0xfb) => "endbr32", + _ => return None, + }; + c.at += 1; + return Some(Insn::new(addr, c.at as u8, name, Flow::Next)); + } + // A mandatory prefix selects a different instruction entirely, and + // REP/REPNE win over 0x66 when both are present. + let t = if p.rep { + table::TWO_BYTE_F3[op2 as usize] + } else if p.repne { + table::TWO_BYTE_F2[op2 as usize] + } else if p.opsize { + table::TWO_BYTE_66[op2 as usize] + } else { + table::TWO_BYTE[op2 as usize] + }; + // Fall back to the unprefixed map when the prefixed one has no entry, + // which is how 0x66 acts as a plain operand-size override on the + // integer instructions that share the map. + let t = if t.m.is_empty() && t.group == 0 { + table::TWO_BYTE[op2 as usize] + } else { + t + }; + (t, true, op2) + } else if p.long() { + (table::ONE_BYTE[op as usize], false, op) + } else { + (table::ONE_BYTE_32[op as usize], false, op) + }; + + if entry.m.is_empty() && entry.group == 0 { + return None; + } + + // The 0x66 that selected an SSE variant is a mandatory prefix, not an + // operand-size override, so `movd xmm0, edi` keeps its 32-bit source. + if two_byte && (p.opsize || p.rep || p.repne) { + let prefixed = if p.rep { + table::TWO_BYTE_F3[opcode as usize] + } else if p.repne { + table::TWO_BYTE_F2[opcode as usize] + } else { + table::TWO_BYTE_66[opcode as usize] + }; + if !prefixed.m.is_empty() { + p.opsize = false; + p.rep = false; + p.repne = false; + } + } + // push, pop and the near call and jmp cannot be 32-bit in long mode. + // Under 0xff only /2 to /6 are those; /0 and /1 are inc and dec, which + // keep the ordinary operand size. None of this applies in 32-bit mode, + // where the stack is four bytes wide and `0x66` is the only lever. + let ff_is_default64 = opcode == 0xff && c.peek().is_some_and(|m| matches!((m >> 3) & 7, 2..=6)); + p.default64 = p.long() + && !two_byte + && (matches!( + opcode, + 0x50..=0x5f | 0xe8 | 0xe9 | 0xc2 | 0xc3 | 0x8f | 0x68 | 0x6a + ) || ff_is_default64); + + p.force_reg_rm = two_byte && matches!(opcode, 0x20..=0x23); + + build(&mut c, &p, entry, opcode, two_byte, addr, bytes) +} + +/// Assemble the instruction from its table entry. +fn build( + c: &mut Cursor<'_>, + p: &Prefixes, + entry: Entry, + opcode: u8, + two_byte: bool, + addr: Addr, + all: &[u8], +) -> Option { + // Any operand that reads ModRM does so once, before the immediate. + let needs_modrm = entry.ops.iter().any(|o| { + matches!( + o, + Op::Eb + | Op::Ew + | Op::Ed + | Op::Ev + | Op::Eq + | Op::Gb + | Op::Gw + | Op::Gd + | Op::Gv + | Op::M + | Op::Mp + | Op::Vx + | Op::Wx + | Op::Ux + | Op::Pq + | Op::Qq + | Op::Nq + | Op::Ewm + | Op::Ebm + | Op::Sw + | Op::Cd + | Op::Dd + ) + }) || entry.group != 0; + + // A group's slots can touch different amounts of memory, and a few touch + // different amounts again when r/m is a register, so the size is looked up + // from the ModRM byte before it is consumed. + let group_size = if entry.group != 0 { + c.peek().and_then(|m| { + let sel = ((m >> 3) & 7) as usize; + if m >> 6 == 3 { + table::group_msize_reg(entry.group, sel) + } else { + table::group_msize(entry.group, sel) + } + }) + } else { + None + }; + // `lea` computes an address rather than accessing memory, and a far + // pointer load names no scalar either, so neither prints a size hint. + let rm_size = if entry.m == "lea" || entry.ops[1] == Op::Mp { + 0 + } else if let Some(n) = group_size { + n as u64 + } else if entry.msize != 0 { + entry.msize as u64 + } else { + rm_size_of(&entry, p) + }; + let modrm = if needs_modrm { + Some(decode_modrm(c, p, rm_size)?) + } else { + None + }; + + // A group's ModRM reg field picks the mnemonic. + let mut mnemonic = entry.m; + let mut ops = entry.ops; + if entry.group != 0 { + let g = GROUPS.get(entry.group as usize - 1)?; + let sel = (modrm.as_ref()?.reg & 7) as usize; + mnemonic = g.0[sel]; + if mnemonic.is_empty() { + return None; + } + // 0xf6 and 0xf7 /0 and /1 are both `test`, and both take an immediate + // the table cannot know about until the group is resolved. + if (entry.group == table::G_F6 || entry.group == table::G_F7) && sel <= 1 { + ops[1] = if entry.group == table::G_F6 { + Op::Ib + } else { + Op::Iz + }; + } + // `call`, `jmp` and `push` through 0xff are always 64-bit in long + // mode. In 32-bit mode they take the ordinary operand size. + if p.long() && entry.group == table::G_FF && matches!(sel, 2..=6) { + ops[0] = Op::Eq; + } + // `psrldq` and `pslldq` shift a whole 128-bit vector along, so they + // exist only in the 0x66 map. The MMX group shares the table with it + // and those two slots of the MMX group are unallocated, which the + // operand kind is what distinguishes. + if matches!(mnemonic, "psrldq" | "pslldq") && entry.ops[0] == Op::Nq { + return None; + } + // A far call or jump loads a segment and an offset from memory. The + // register form is not a shorter spelling of it, it is an encoding the + // processor faults on, and decoding one would hand the caller a + // control flow edge that never exists. + if entry.group == table::G_FF + && matches!(sel, 3 | 5) + && matches!(modrm.as_ref()?.rm, RmOperand::Reg(_)) + { + return None; + } + // 0x0f 0xae is two groups sharing one opcode: the fences when r/m is a + // register, the state-save instructions when it is memory. + if entry.group == table::G_0FAE && matches!(modrm.as_ref()?.rm, RmOperand::Reg(_)) { + mnemonic = GROUPS.get(table::G_0FAE_REG as usize - 1)?.0[sel]; + if mnemonic.is_empty() { + return None; + } + ops = [Op::None; 3]; + } + } + + // 0x0f 0x12 and 0x0f 0x16 load half a vector from memory, and with a + // register source there is no half to load: the encoding names the + // register-to-register move instead. The test is the mnemonic the table + // already resolved rather than the prefix bytes, because `0xf3` and + // `0xf2` reach these opcodes by two different routes: at 0x12 they select + // a real instruction of their own, and at 0x16 only `0xf3` does, so `0xf2` + // there falls back to `movhps` and takes this rewrite with it. + if two_byte + && matches!(opcode, 0x12 | 0x16) + && matches!(mnemonic, "movlps" | "movlpd" | "movhps" | "movhpd") + && matches!(modrm.as_ref()?.rm, RmOperand::Reg(_)) + { + mnemonic = if mnemonic.starts_with("movl") { + "movhlps" + } else { + "movlhps" + }; + ops = [Op::Vx, Op::Ux, Op::None]; + } + + // These name memory and only memory: the non-temporal stores bypass the + // cache, the half-vector moves transfer the half a register does not have, + // and `lddqu` exists to cross a cache line. A register in r/m is a fault + // rather than a shorter form, so the bytes are not an instruction. The + // half-vector loads are not on this list because their register form is + // `movhlps` and `movlhps`, which the rewrite above has already named. + if matches!( + mnemonic, + "movlps" + | "movhps" + | "movlpd" + | "movhpd" + | "movntps" + | "movntpd" + | "movntss" + | "movntsd" + | "movntq" + | "movntdq" + | "movnti" + | "lddqu" + ) && matches!(modrm.as_ref()?.rm, RmOperand::Reg(_)) + { + return None; + } + + // llvm prints `xchg` with the ModRM reg operand first when r/m is also a + // register, and the memory operand first when it is not. The table carries + // the memory order, so the register form swaps. + if !two_byte && matches!(opcode, 0x86 | 0x87) && matches!(modrm.as_ref()?.rm, RmOperand::Reg(_)) + { + ops.swap(0, 1); + } + + let moffs = ops.iter().any(|o| matches!(o, Op::Ob | Op::Ov)); + let mut i = Insn::new(addr, 0, mnemonic, Flow::Next); + + for kind in ops.iter() { + if *kind == Op::None { + continue; + } + let operand = match kind { + Op::Eb | Op::Ew | Op::Ed | Op::Ev | Op::Eq | Op::M | Op::Mp => { + let m = modrm.as_ref()?; + let w = width_of(rm_size); + match &m.rm { + RmOperand::Reg(r) => { + if matches!(kind, Op::M | Op::Mp) { + return None; // memory-only form with a register + } + Operand::Reg(gpr(*r, w, p.rex)) + } + RmOperand::Mem(mem) => Operand::Mem(*mem), + } + } + Op::Wx | Op::Ux => { + let m = modrm.as_ref()?; + match &m.rm { + RmOperand::Reg(r) => Operand::Reg(xmm(*r)), + RmOperand::Mem(mem) => { + if *kind == Op::Ux { + return None; + } + Operand::Mem(*mem) + } + } + } + Op::Qq | Op::Nq => { + let m = modrm.as_ref()?; + match &m.rm { + RmOperand::Reg(r) => Operand::Name(MM[(*r & 7) as usize]), + RmOperand::Mem(mem) => { + if *kind == Op::Nq { + return None; + } + Operand::Mem(*mem) + } + } + } + // The segment store writes a word to memory and the whole register + // when the destination is one. + Op::Ewm | Op::Ebm => { + let m = modrm.as_ref()?; + let size = if *kind == Op::Ebm { 1 } else { 2 }; + match &m.rm { + RmOperand::Reg(r) => Operand::Reg(gpr(*r, p.width(), p.rex)), + RmOperand::Mem(mem) => Operand::Mem(Mem { size, ..*mem }), + } + } + Op::Gb => Operand::Reg(gpr(modrm.as_ref()?.reg, Width::W8, p.rex)), + Op::Gw => Operand::Reg(gpr(modrm.as_ref()?.reg, Width::W16, p.rex)), + Op::Gd => Operand::Reg(gpr(modrm.as_ref()?.reg, Width::W32, p.rex)), + Op::Gv => Operand::Reg(gpr(modrm.as_ref()?.reg, p.width(), p.rex)), + Op::Vx => Operand::Reg(xmm(modrm.as_ref()?.reg)), + Op::Pq => Operand::Name(MM[(modrm.as_ref()?.reg & 7) as usize]), + Op::Sw => Operand::Reg(Reg { + class: RegClass::Seg, + num: modrm.as_ref()?.reg & 7, + width: Width::W16, + }), + Op::Sr => Operand::Reg(Reg { + class: RegClass::Seg, + num: (opcode >> 3) & 7, + width: Width::W16, + }), + Op::Cd => Operand::Reg(Reg { + class: RegClass::Sys, + num: modrm.as_ref()?.reg, + width: Width::W64, + }), + // A debug register is not a control register, and [`RegClass`] has + // one system class for both, so this one prints by name. + Op::Dd => Operand::Name(DR[(modrm.as_ref()?.reg & 15) as usize]), + // A far pointer immediate is stored offset first and printed + // segment first, and llvm prints the segment as a signed word. + Op::Ap => { + let off = if p.opsize { c.u16()? } else { c.u32()? }; + let seg = c.u16()? as u16 as i16 as i64; + i.push(Operand::Imm(seg)); + Operand::UImm(off) + } + // An immediate prints as the bytes encode it, unsigned at its own + // width, except where the encoding sign-extends into a wider + // operand: `add rax, -0x1` really is minus one. + Op::Ib | Op::Ibs => Operand::Imm(c.i8()?), + Op::Ibu => Operand::UImm(c.u8()? as u64), + Op::Iw => Operand::Imm(c.i16()?), + Op::Id => Operand::UImm(c.u32()?), + Op::Iz => { + if p.opsize && !p.w { + Operand::UImm(c.u16()?) + } else if p.w { + Operand::Imm(c.i32()?) + } else { + Operand::UImm(c.u32()?) + } + } + Op::Iv => { + if p.w { + Operand::UImm(c.u64()?) + } else if p.opsize { + Operand::UImm(c.u16()?) + } else { + Operand::UImm(c.u32()?) + } + } + Op::Jb => { + let d = c.i8()?; + Operand::Addr(branch(p, addr, c.at as i64 + d)) + } + Op::Jz => { + let d = if p.opsize { c.i16()? } else { c.i32()? }; + Operand::Addr(branch(p, addr, c.at as i64 + d)) + } + Op::AL => Operand::Reg(gpr(0, Width::W8, p.rex)), + Op::EAX => Operand::Reg(gpr(0, p.width(), p.rex)), + Op::CL => Operand::Reg(gpr(1, Width::W8, p.rex)), + Op::DX => Operand::Reg(gpr(2, Width::W16, p.rex)), + Op::Rb => Operand::Reg(gpr( + (opcode & 7) | if p.b { 8 } else { 0 }, + Width::W8, + p.rex, + )), + Op::Rv => Operand::Reg(gpr( + (opcode & 7) | if p.b { 8 } else { 0 }, + p.width(), + p.rex, + )), + Op::Ob | Op::Ov => Operand::Mem(Mem { + seg: p.seg.map(|r| Reg { + class: RegClass::Seg, + num: r, + width: Width::W16, + }), + base: None, + index: None, + // An absolute offset is as wide as the address size, so it is + // eight bytes only in long mode without `0x67`. + disp: match p.addr_bytes() { + 2 => c.u16()? as i64, + 4 => c.u32()? as i64, + _ => c.u64()? as i64, + }, + mode: AddrMode::Offset, + size: if *kind == Op::Ob { 1 } else { p.opsize_bytes() }, + }), + Op::XMM0 => Operand::Reg(xmm(0)), + Op::One => Operand::Imm(1), + Op::None => continue, + }; + i.push(operand); + } + + let len = c.at; + if len == 0 || len > 15 { + return None; + } + i.len = len as u8; + + i.flow = flow_of(mnemonic, two_byte, opcode, &i); + i.mnemonic = rename(mnemonic, p, &i); + // An absolute-offset move is `movabs` in long mode whatever it moves. + if p.long() && !two_byte && matches!(opcode, 0xa0..=0xa3) { + i.mnemonic = "movabs"; + } + // `aam` and `aad` hide the base-ten immediate they almost always carry. + if matches!(i.mnemonic, "aam" | "aad") && i.operands() == [Operand::Imm(10)] { + i = Insn::new(i.addr, i.len, i.mnemonic, i.flow); + } + // llvm's sixteen-bit forms of `ret`, `retf` and the far branches print + // their word operand unsigned, where the 32-bit forms print it signed. + if !two_byte && p.opsize && matches!(opcode, 0xc2 | 0xca | 0x9a | 0xea) { + if let Some(Operand::Imm(v)) = i.operands().first().copied() { + i.set_operand(0, Operand::UImm(v as u16 as u64)); + } + } + // A 64-bit immediate move reads as a signed number, which is how a + // negative constant written in source appears again in the listing. + if i.mnemonic == "movabs" { + if let Some(Operand::UImm(v)) = i.operands().get(1).copied() { + i.set_operand(1, Operand::Imm(v as i64)); + } + } + if let Some(with_ops) = string_operands(&i, p) { + i = with_ops; + } + // `cmpps` and friends fold their predicate immediate into the mnemonic. + if let Some(folded) = fold_compare_predicate(&i) { + i = folded; + } + if let Some(with_ops) = string_operands(&i, p) { + i = with_ops; + } + // `lock`, `rep` and `repne` modify the whole instruction. `rep` and + // `repne` are only prefixes on the string instructions; elsewhere the + // same bytes select a different opcode entirely. + // llvm names the address size when the operand shows no register to imply + // it, which is the 16-bit absolute form and nothing else. + // llvm names the address size whenever no operand implies it: either there + // is no memory operand at all, or the one there is has no base and no + // index register. An absolute offset is the exception, since its own + // opcode already says how wide it is. + let addr_override = + !moffs && !matches!(i.mnemonic, "jcxz" | "jecxz" | "jrcxz") && names_addr_size(p, &i); + // llvm prints a repeat prefix only on a one-byte opcode. On a two-byte one + // the byte is a mandatory prefix slot, and an opcode that does not use it + // drops it rather than reporting it. + let repeat = i.mnemonic != "pause" && (!two_byte || !p.repeat_is_mandatory); + i.prefix = prefix_text( + if p.lock { + Some("lock") + } else if p.rep && repeat { + Some("rep") + } else if p.repne && repeat { + Some("repne") + } else { + None + }, + addr_override.then_some(addr_prefix_name(p)), + ); + let _ = all; + Some(i) +} + +/// Whether llvm spells the address-size override out in front of the mnemonic. +/// It does so when no operand implies the size: either there is no memory +/// operand at all, or the one there is has no base and no index register and +/// so shows no register whose width would give the answer away. +fn names_addr_size(p: &Prefixes, i: &Insn) -> bool { + p.addrsize + && i.operands() + .iter() + .all(|o| !matches!(o, Operand::Mem(m) if m.base.is_some() || m.index.is_some())) +} + +/// The name of the address size `0x67` selects, which is the other one. +fn addr_prefix_name(p: &Prefixes) -> &'static str { + if p.long() { "addr32" } else { "addr16" } +} + +/// `cmpps xmm0, xmm1, 2` is spelled `cmpleps xmm0, xmm1`: the predicate +/// immediate is part of the name. Predicates past seven keep the raw form. +fn fold_compare_predicate(i: &Insn) -> Option { + let stem = match i.mnemonic { + "cmpps" => "ps", + "cmppd" => "pd", + "cmpss" => "ss", + "cmpsd" if i.operands().len() == 3 => "sd", + _ => return None, + }; + let pred = match i.operands().get(2)? { + Operand::UImm(v) if *v < 8 => *v as usize, + _ => return None, + }; + let name: &'static str = match (stem, table::CMP_PRED[pred]) { + ("ps", p) => concat_pred("ps", p)?, + ("pd", p) => concat_pred("pd", p)?, + ("ss", p) => concat_pred("ss", p)?, + (_, p) => concat_pred("sd", p)?, + }; + let mut out = Insn::new(i.addr, i.len, name, i.flow); + out.prefix = i.prefix; + out.push(*i.operands().first()?); + out.push(*i.operands().get(1)?); + Some(out) +} + +/// Every `cmp` name as a static string, since [`Insn`] holds one. +fn concat_pred(kind: &str, pred: &str) -> Option<&'static str> { + const NAMES: [&str; 32] = [ + "cmpeqps", + "cmpltps", + "cmpleps", + "cmpunordps", + "cmpneqps", + "cmpnltps", + "cmpnleps", + "cmpordps", + "cmpeqpd", + "cmpltpd", + "cmplepd", + "cmpunordpd", + "cmpneqpd", + "cmpnltpd", + "cmpnlepd", + "cmpordpd", + "cmpeqss", + "cmpltss", + "cmpless", + "cmpunordss", + "cmpneqss", + "cmpnltss", + "cmpnless", + "cmpordss", + "cmpeqsd", + "cmpltsd", + "cmplesd", + "cmpunordsd", + "cmpneqsd", + "cmpnltsd", + "cmpnlesd", + "cmpordsd", + ]; + let k = ["ps", "pd", "ss", "sd"].iter().position(|x| *x == kind)?; + let p = table::CMP_PRED.iter().position(|x| *x == pred)?; + NAMES.get(k * 8 + p).copied() +} + +/// `es:[rdi]` and `[rsi]`, the implicit operands of the string instructions. +/// +/// They carry no ModRM, so the table has no operands for them, but every +/// disassembler prints them because they are what the instruction touches. +fn string_operands(i: &Insn, p: &Prefixes) -> Option { + let (name, size) = match i.mnemonic { + m if matches!(m, "insb" | "insw" | "insd" | "outsb" | "outsw" | "outsd") + || m.starts_with("movs") + || m.starts_with("stos") + || m.starts_with("lods") + || m.starts_with("scas") + || m.starts_with("cmps") => + { + let size = match m.as_bytes().last()? { + b'b' => 1, + b'w' => 2, + b'q' => 8, + _ => 4, + }; + (m, size) + } + _ => return None, + }; + if !i.operands().is_empty() { + return None; // an SSE movsd, not the string one + } + + let es = Reg { + class: RegClass::Seg, + num: 0, + width: Width::W16, + }; + let aw = p.addr_width(); + let di = Mem { + seg: Some(es), + base: Some(Reg::gpr(7, aw)), + index: None, + disp: 0, + mode: AddrMode::Offset, + size, + }; + let si = Mem { + // A segment override applies to the source, which is the only string + // operand whose segment is not fixed at `es`. + seg: p.seg.map(|r| Reg { + class: RegClass::Seg, + num: r, + width: Width::W16, + }), + base: Some(Reg::gpr(6, aw)), + index: None, + disp: 0, + mode: AddrMode::Offset, + size, + }; + let acc = Operand::Reg(gpr(0, width_of(size), p.rex)); + + let mut out = Insn::new(i.addr, i.len, i.mnemonic, i.flow); + out.prefix = i.prefix; + match if name.starts_with("ins") { + name + } else { + &name[..4] + } { + "movs" => { + out.push(Operand::Mem(di)); + out.push(Operand::Mem(si)); + } + "stos" => { + out.push(Operand::Mem(di)); + out.push(acc); + } + "lods" => { + out.push(acc); + out.push(Operand::Mem(si)); + } + "scas" => { + out.push(acc); + out.push(Operand::Mem(di)); + } + "cmps" => { + out.push(Operand::Mem(si)); + out.push(Operand::Mem(di)); + } + "insb" | "insw" | "insd" => { + out.push(Operand::Mem(di)); + out.push(Operand::Reg(Reg::gpr(2, Width::W16))); + } + "outs" => { + out.push(Operand::Reg(Reg::gpr(2, Width::W16))); + out.push(Operand::Mem(si)); + } + _ => return None, + } + Some(out) +} + +/// The size in bytes of the ModRM r/m operand for this entry. +fn rm_size_of(entry: &Entry, p: &Prefixes) -> u64 { + for o in &entry.ops { + match o { + Op::Eb => return 1, + Op::Ew => return 2, + Op::Ed => return 4, + Op::Eq => return p.forced_qword(), + Op::Ev | Op::Ewm => return p.opsize_bytes(), + Op::Wx | Op::Ux => return 16, + Op::Qq | Op::Nq => return 8, + _ => {} + } + } + p.opsize_bytes() +} + +/// Control flow for a decoded instruction. +fn flow_of(m: &str, two_byte: bool, opcode: u8, i: &Insn) -> Flow { + let direct = i.operands().first().and_then(|o| match o { + Operand::Addr(a) => Some(*a), + _ => None, + }); + match m { + "call" => match direct { + Some(a) => Flow::Call(a), + None => Flow::IndirectCall, + }, + "jmp" => match direct { + Some(a) => Flow::Branch(a), + None => Flow::IndirectBranch, + }, + "ret" | "lret" | "iret" | "sysret" | "sysexit" => Flow::Return, + "hlt" | "ud2" => Flow::Trap, + "int3" => Flow::Trap, + "int" | "syscall" | "sysenter" => Flow::Syscall, + "loop" | "loope" | "loopne" | "jrcxz" => match direct { + Some(a) => Flow::CondBranch(a), + None => Flow::Next, + }, + _ => { + // The conditional jumps, in both their short and near forms. + let is_jcc = (!two_byte && (0x70..0x80).contains(&opcode)) + || (two_byte && (0x80..0x90).contains(&opcode)); + match (is_jcc, direct) { + (true, Some(a)) => Flow::CondBranch(a), + _ => Flow::Next, + } + } + } +} + +/// Spellings that depend on the prefixes or the operands. +/// A relative branch target. The program counter is as wide as the mode, so a +/// 32-bit branch that runs off the top of the address space wraps there rather +/// than reaching into the 64-bit range. +fn branch(p: &Prefixes, addr: Addr, delta: i64) -> Addr { + let to = addr.wrapping_offset(delta); + if p.long() { + to + } else { + Addr(to.get() & 0xffff_ffff) + } +} + +/// Two prefixes can want printing at once, and [`Insn`] holds one string, so +/// the pairs are spelled out. The tab pair is what llvm puts between them. +fn prefix_text(repeat: Option<&'static str>, addr: Option<&'static str>) -> Option<&'static str> { + Some(match (repeat, addr) { + (None, None) => return None, + (Some(r), None) => r, + (None, Some(a)) => a, + (Some("lock"), Some("addr16")) => "lock\t\taddr16", + (Some("lock"), Some(_)) => "lock\t\taddr32", + (Some("rep"), Some("addr16")) => "rep\t\taddr16", + (Some("rep"), Some(_)) => "rep\t\taddr32", + (Some(_), Some("addr16")) => "repne\t\taddr16", + (Some(_), Some(_)) => "repne\t\taddr32", + }) +} + +fn is_indirect(i: &Insn) -> bool { + matches!(i.operands().first(), Some(Operand::Mem(_))) +} + +fn rename(m: &'static str, p: &Prefixes, i: &Insn) -> &'static str { + // The string instructions take their size from the operand size, and the + // table carries only the doubleword spelling. + let sized = |b: &'static str, w: &'static str, d: &'static str, q: &'static str| match p + .opsize_bytes() + { + 2 => w, + 8 => q, + _ => { + let _ = b; + d + } + }; + let base = match m { + "movsd" if i.operands().is_empty() => sized("movsb", "movsw", "movsd", "movsq"), + "cmpsd" if i.operands().is_empty() => sized("cmpsb", "cmpsw", "cmpsd", "cmpsq"), + "stosd" => sized("stosb", "stosw", "stosd", "stosq"), + "lodsd" => sized("lodsb", "lodsw", "lodsd", "lodsq"), + "scasd" => sized("scasb", "scasw", "scasd", "scasq"), + "insd" => sized("insb", "insw", "insd", "insd"), + "outsd" => sized("outsb", "outsw", "outsd", "outsd"), + "cwde" => match p.opsize_bytes() { + 2 => "cbw", + 8 => "cdqe", + _ => "cwde", + }, + "cdq" => match p.opsize_bytes() { + 2 => "cwd", + 8 => "cqo", + _ => "cdq", + }, + // The flag transfers and the counted loops name their own width, and + // llvm leaves the 16-bit forms unsuffixed. + "iret" => match p.opsize_bytes() { + 2 => "iret", + 8 => "iretq", + _ => "iretd", + }, + // llvm spells the far return `retf`, and suffixes only the 64-bit one. + "lret" => match p.opsize_bytes() { + 8 => "retfq", + _ => "retf", + }, + // The descriptor table instructions take a `d` in 32-bit mode, where + // the operand they load is six bytes rather than ten. + // The descriptor table instructions carry the operand size in their + // name outside long mode, where the table pointer is six bytes rather + // than ten. + "sgdt" | "sidt" | "lgdt" | "lidt" if !p.long() => { + let w = p.opsize_bytes() == 2; + match (m, w) { + ("sgdt", true) => "sgdtw", + ("sgdt", _) => "sgdtd", + ("sidt", true) => "sidtw", + ("sidt", _) => "sidtd", + ("lgdt", true) => "lgdtw", + ("lgdt", _) => "lgdtd", + (_, true) => "lidtw", + _ => "lidtd", + } + } + "pushal" if p.opsize => "pushaw", + "popal" if p.opsize => "popaw", + // 0xf3 0x90 is its own instruction, not a repeated `nop`. + "nop" if p.rep && i.operands().is_empty() => "pause", + "pushf" => match p.opsize_bytes() { + 2 => "pushf", + 8 => "pushfq", + _ => "pushfd", + }, + "popf" => match p.opsize_bytes() { + 2 => "popf", + 8 => "popfq", + _ => "popfd", + }, + "jrcxz" => match p.addr_bytes() { + 2 => "jcxz", + 8 => "jrcxz", + _ => "jecxz", + }, + // 0xff /3 and /5 are the far indirect branches, which llvm spells + // `call` and `jmp` at 32 and 64 bits and `lcall` and `ljmp` at 16. + // The 0x9a and 0xea forms keep the long name at every size, and they + // are the ones whose operand is an immediate rather than memory. + "lcall" if p.opsize_bytes() != 2 && is_indirect(i) => "call", + "ljmp" if p.opsize_bytes() != 2 && is_indirect(i) => "jmp", + // The 64-bit form of movd has its own name. + "movd" if p.w => "movq", + // A 64-bit immediate move has its own name. + "mov" if p.w && matches!(i.operands().get(1), Some(Operand::UImm(_))) && i.len >= 10 => { + "movabs" + } + other => other, + }; + let _ = CC; + base +} + +#[cfg(test)] +mod tests { + use super::*; + + fn dis(bytes: &[u8]) -> String { + let i = decode(bytes, Addr(0x1000)).expect("decode"); + assert_eq!(i.len as usize, bytes.len(), "length mismatch"); + format(&i, Style::default()) + } + + #[test] + fn the_basics() { + assert_eq!(dis(&[0x85, 0xff]), "test\tedi, edi"); + assert_eq!(dis(&[0x89, 0xf9]), "mov\tecx, edi"); + assert_eq!(dis(&[0x31, 0xc0]), "xor\teax, eax"); + assert_eq!(dis(&[0xc3]), "ret"); + } + + #[test] + fn rex_w_widens_to_64_bits() { + assert_eq!(dis(&[0x48, 0x89, 0xf9]), "mov\trcx, rdi"); + assert_eq!(dis(&[0x48, 0x0f, 0xaf, 0xc1]), "imul\trax, rcx"); + } + + #[test] + fn rex_r_and_b_reach_the_high_registers() { + assert_eq!(dis(&[0x4d, 0x89, 0xc1]), "mov\tr9, r8"); + } + + #[test] + fn without_rex_the_high_byte_registers_are_named() { + // 0x88 /r with reg=4 is ah, not spl. + assert_eq!(dis(&[0x88, 0xe0]), "mov\tal, ah"); + // With any REX byte it is spl. + assert_eq!(dis(&[0x40, 0x88, 0xe0]), "mov\tal, spl"); + } + + #[test] + fn memory_operands_print_intel_style() { + assert_eq!(dis(&[0x8d, 0x47, 0xff]), "lea\teax, [rdi - 0x1]"); + assert_eq!( + dis(&[0x8b, 0x04, 0x88]), + "mov\teax, dword ptr [rax + 4*rcx]" + ); + } + + #[test] + fn rip_relative_keeps_the_encoded_displacement() { + // The text says what the bytes say; analysis resolves the target. + let i = decode(&[0x48, 0x8d, 0x05, 0x10, 0x00, 0x00, 0x00], Addr(0x1000)).unwrap(); + assert_eq!(i.len, 7); + assert_eq!(format(&i, Style::default()), "lea\trax, [rip + 0x10]"); + let Operand::Mem(m) = i.operands()[1] else { + panic!("expected a memory operand"); + }; + assert!(m.is_pc_relative()); + // 0x1000 + 7 + 0x10. + assert_eq!(m.pc_target(i.end()), Some(Addr(0x1017))); + } + + #[test] + fn a_call_carries_its_target() { + // e8 rel32 at 0x1000, 5 bytes, displacement 0x10 -> 0x1015. + let i = decode(&[0xe8, 0x10, 0x00, 0x00, 0x00], Addr(0x1000)).unwrap(); + assert_eq!(i.flow, Flow::Call(Addr(0x1015))); + } + + #[test] + fn a_conditional_jump_has_both_successors() { + let i = decode(&[0x78, 0x0f], Addr(0x1000)).unwrap(); + assert_eq!(i.flow, Flow::CondBranch(Addr(0x1011))); + assert!(i.flow.falls_through()); + } + + #[test] + fn groups_pick_their_mnemonic_from_modrm() { + // 83 /5 ib is sub, /0 is add. + assert_eq!(dis(&[0x83, 0xe8, 0x01]), "sub\teax, 0x1"); + assert_eq!(dis(&[0x83, 0xc0, 0x01]), "add\teax, 0x1"); + // f7 /3 is neg, and takes no immediate. + assert_eq!(dis(&[0xf7, 0xd8]), "neg\teax"); + // f7 /0 is test, and does. + assert_eq!(dis(&[0xf7, 0xc0, 0x01, 0x00, 0x00, 0x00]), "test\teax, 0x1"); + } + + #[test] + fn shifts_by_one_are_implicit() { + assert_eq!(dis(&[0x48, 0xd1, 0xe8]), "shr\trax"); + } + + #[test] + fn mandatory_prefixes_select_a_different_instruction() { + assert_eq!(dis(&[0x0f, 0x10, 0xc1]), "movups\txmm0, xmm1"); + assert_eq!(dis(&[0x66, 0x0f, 0x10, 0xc1]), "movupd\txmm0, xmm1"); + assert_eq!(dis(&[0xf3, 0x0f, 0x10, 0xc1]), "movss\txmm0, xmm1"); + assert_eq!(dis(&[0xf2, 0x0f, 0x10, 0xc1]), "movsd\txmm0, xmm1"); + } + + #[test] + fn nothing_decodes_past_fifteen_bytes() { + // A pile of prefixes with no opcode must not produce an instruction. + assert!(decode(&[0x66; 32], Addr(0x1000)).is_none()); + } + + #[test] + fn a_truncated_instruction_is_not_decoded() { + assert!(decode(&[0xe8, 0x10], Addr(0x1000)).is_none()); + assert!(decode(&[], Addr(0x1000)).is_none()); + } + + #[test] + fn no_byte_sequence_panics() { + let mut buf = [0u8; 8]; + for seed in 0u32..20000 { + let mut x = seed.wrapping_mul(2654435761); + for b in buf.iter_mut() { + x = x.wrapping_mul(1103515245).wrapping_add(12345); + *b = (x >> 16) as u8; + } + let _ = decode(&buf, Addr(0x1000)); + } + } +} diff --git a/crates/e5r-arch/src/x86/table.rs b/crates/e5r-arch/src/x86/table.rs new file mode 100644 index 0000000..16b89b8 --- /dev/null +++ b/crates/e5r-arch/src/x86/table.rs @@ -0,0 +1,1192 @@ +//! x86 opcode tables. +//! +//! Operand kinds use the Intel manual's own notation (`Ev`, `Gb`, `Iz`), so an +//! entry can be checked against appendix A line by line. That is the whole +//! reason for the shorthand: a table you cannot diff against the specification +//! is a table nobody can audit. + +/// How one operand is encoded. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(non_camel_case_types)] +pub enum Op { + /// Absent. + None, + /// ModRM r/m, byte. + Eb, + /// ModRM r/m, word. + Ew, + /// ModRM r/m, doubleword. + Ed, + /// ModRM r/m at the operand size. + Ev, + /// ModRM r/m, forced 64-bit. + Eq, + /// ModRM reg, byte. + Gb, + /// ModRM reg, word. + Gw, + /// ModRM reg, doubleword. + Gd, + /// ModRM reg at the operand size. + Gv, + /// Immediate byte, printed signed. llvm prints every byte immediate of an + /// integer instruction as a signed number, so `add al, 0x80` reads back as + /// `add al, -0x80`. + Ib, + /// Immediate byte, printed unsigned: a shift count, a port, an interrupt + /// number, or an SSE selector, none of which is a number line. + Ibu, + /// Immediate byte, sign-extended to the operand size. + Ibs, + /// Immediate word. + Iw, + /// Immediate doubleword. + Id, + /// Immediate, word or doubleword, sign-extended to the operand size. + Iz, + /// Immediate at the full operand size, so 8 bytes under REX.W. + Iv, + /// Relative byte displacement. + Jb, + /// Relative word or doubleword displacement. + Jz, + /// ModRM, memory only. + M, + /// A far pointer in memory. + Mp, + /// The accumulator, byte. + AL, + /// The accumulator at the operand size. + EAX, + /// `cl`, the implicit shift count. + CL, + /// `dx`, the implicit port. + DX, + /// Register in the low three opcode bits, byte. + Rb, + /// Register in the low three opcode bits, at the operand size. + Rv, + /// Absolute offset, byte. + Ob, + /// Absolute offset at the operand size. + Ov, + /// A segment register from ModRM reg. + Sw, + /// A segment register named by bits 3 to 5 of the opcode, as the `push` + /// and `pop` of a segment do. These carry no ModRM byte. + Sr, + /// The literal 1. + One, + /// ModRM reg as an SSE register. + Vx, + /// ModRM r/m as an SSE register or memory. + Wx, + /// ModRM r/m as an SSE register only. + Ux, + /// A control register. + Cd, + /// A debug register. + Dd, + /// ModRM reg as an MMX register. + Pq, + /// ModRM r/m as an MMX register or memory. + Qq, + /// ModRM r/m as an MMX register only. + Nq, + /// A far pointer immediate: offset then segment, printed segment first. + Ap, + /// ModRM r/m: a word in memory, the operand size in a register. Only the + /// segment register store at 0x8c encodes this way. + Ewm, + /// `xmm0`, the implicit mask of the variable blends. + XMM0, + /// ModRM r/m: a byte in memory, the operand size in a register, which is + /// how the SSE4 byte extract and insert print. + Ebm, +} + +/// One opcode's mnemonic and operands. +#[derive(Debug, Clone, Copy)] +pub struct Entry { + /// The mnemonic, or empty when the opcode is not allocated here. + pub m: &'static str, + /// Operands in Intel order: destination first. + pub ops: [Op; 3], + /// Index into [`GROUPS`] when the ModRM reg field selects the instruction. + pub group: u8, + /// Bytes a memory `Wx` operand touches, when it is not the full 16. A + /// scalar `movsd` reads eight bytes and `movss` four, and llvm prints the + /// difference, so the table has to carry it. + pub msize: u8, +} + +/// No instruction at this opcode. +pub const BAD: Entry = Entry { + m: "", + ops: [Op::None, Op::None, Op::None], + group: 0, + msize: 0, +}; + +const fn e(m: &'static str, a: Op, b: Op, c: Op) -> Entry { + Entry { + m, + ops: [a, b, c], + group: 0, + msize: 0, + } +} + +/// Like [`e`], with an explicit memory operand size. +const fn es(m: &'static str, a: Op, b: Op, c: Op, msize: u8) -> Entry { + Entry { + m, + ops: [a, b, c], + group: 0, + msize, + } +} + +const fn g(n: u8, a: Op, b: Op) -> Entry { + Entry { + m: "", + ops: [a, b, Op::None], + group: n, + msize: 0, + } +} + +/// Like [`g`], with an explicit memory operand size. +const fn gs(n: u8, a: Op, b: Op, msize: u8) -> Entry { + Entry { + m: "", + ops: [a, b, Op::None], + group: n, + msize, + } +} + +use Op::*; + +/// Group numbers, matching the Intel manual's numbering where it has one. +/// Group selector for opcode 0x80. +pub const G_80: u8 = 1; +/// Group selector for opcode 0x81. +pub const G_81: u8 = 2; +/// Group selector for opcode 0x83. +pub const G_83: u8 = 3; +/// Group selector for opcode 0xc0. +pub const G_C0: u8 = 4; +/// Group selector for opcode 0xc1. +pub const G_C1: u8 = 5; +/// Group selector for opcode 0xd0. +pub const G_D0: u8 = 6; +/// Group selector for opcode 0xd1. +pub const G_D1: u8 = 7; +/// Group selector for opcode 0xd2. +pub const G_D2: u8 = 8; +/// Group selector for opcode 0xd3. +pub const G_D3: u8 = 9; +/// Group selector for opcode 0xf6. +pub const G_F6: u8 = 10; +/// Group selector for opcode 0xf7. +pub const G_F7: u8 = 11; +/// Group selector for opcode 0xfe. +pub const G_FE: u8 = 12; +/// Group selector for opcode 0xff. +pub const G_FF: u8 = 13; +/// Group selector for opcode 0xc6. +pub const G_C6: u8 = 14; +/// Group selector for opcode 0xc7. +pub const G_C7: u8 = 15; +/// Group selector for opcode 0x8f. +pub const G_8F: u8 = 16; +/// Group selector for opcode 0x0f00. +pub const G_0F00: u8 = 17; +/// Group selector for opcode 0x0f01. +pub const G_0F01: u8 = 18; +/// Group selector for opcode 0x0fba. +pub const G_0FBA: u8 = 19; +/// Group selector for opcode 0x0fae. +pub const G_0FAE: u8 = 20; +/// Group selector for opcode 0x0fc7. +pub const G_0FC7: u8 = 21; +/// Group selector for `0x0f 0xae` with a register operand, which names the +/// fences rather than the state-save instructions its memory forms do. +pub const G_0FAE_REG: u8 = 22; +/// Group selector for opcode 0x82, the 32-bit-only alias of 0x80. +pub const G_82: u8 = 23; +/// Group selector for `0x0f 0x0d`, the AMD prefetch hints. +pub const G_0F0D: u8 = 24; +/// Group selector for `0x0f 0x18`, the prefetch hints. +pub const G_0F18: u8 = 25; +/// Group selector for `0x0f 0x71`, the word shifts. +pub const G_0F71: u8 = 26; +/// Group selector for `0x0f 0x72`, the doubleword shifts. +pub const G_0F72: u8 = 27; +/// Group selector for `0x0f 0x73`, the quadword shifts. +pub const G_0F73: u8 = 28; + +/// The eight instructions a group's ModRM reg field selects between. +pub struct Group(pub [&'static str; 8]); + +/// Group tables, indexed by the constants above minus one. +pub const GROUPS: &[Group] = &[ + // 1: 0x80 + Group(["add", "or", "adc", "sbb", "and", "sub", "xor", "cmp"]), + // 2: 0x81 + Group(["add", "or", "adc", "sbb", "and", "sub", "xor", "cmp"]), + // 3: 0x83 + Group(["add", "or", "adc", "sbb", "and", "sub", "xor", "cmp"]), + // 4: 0xc0 + Group(["rol", "ror", "rcl", "rcr", "shl", "shr", "shl", "sar"]), + // 5: 0xc1 + Group(["rol", "ror", "rcl", "rcr", "shl", "shr", "shl", "sar"]), + // 6: 0xd0 + Group(["rol", "ror", "rcl", "rcr", "shl", "shr", "shl", "sar"]), + // 7: 0xd1 + Group(["rol", "ror", "rcl", "rcr", "shl", "shr", "shl", "sar"]), + // 8: 0xd2 + Group(["rol", "ror", "rcl", "rcr", "shl", "shr", "shl", "sar"]), + // 9: 0xd3 + Group(["rol", "ror", "rcl", "rcr", "shl", "shr", "shl", "sar"]), + // 10: 0xf6 + Group(["test", "test", "not", "neg", "mul", "imul", "div", "idiv"]), + // 11: 0xf7 + Group(["test", "test", "not", "neg", "mul", "imul", "div", "idiv"]), + // 12: 0xfe + Group(["inc", "dec", "", "", "", "", "", ""]), + // 13: 0xff + Group(["inc", "dec", "call", "lcall", "jmp", "ljmp", "push", ""]), + // 14: 0xc6 + Group(["mov", "", "", "", "", "", "", ""]), + // 15: 0xc7 + Group(["mov", "", "", "", "", "", "", ""]), + // 16: 0x8f + Group(["pop", "", "", "", "", "", "", ""]), + // 17: 0x0f 0x00 + Group(["sldt", "str", "lldt", "ltr", "verr", "verw", "", ""]), + // 18: 0x0f 0x01 + Group(["sgdt", "sidt", "lgdt", "lidt", "smsw", "", "lmsw", "invlpg"]), + // 19: 0x0f 0xba + Group(["", "", "", "", "bt", "bts", "btr", "btc"]), + // 20: 0x0f 0xae + Group([ + "fxsave", "fxrstor", "ldmxcsr", "stmxcsr", "xsave", "xrstor", "xsaveopt", "clflush", + ]), + // 21: 0x0f 0xc7 + Group(["", "cmpxchg8b", "", "", "", "", "rdrand", "rdseed"]), + // 22: 0x0f 0xae, register forms + Group(["", "", "", "", "", "lfence", "mfence", "sfence"]), + // 23: 0x82 + Group(["add", "or", "adc", "sbb", "and", "sub", "xor", "cmp"]), + // 24: 0x0f 0x0d + Group(["prefetch", "prefetchw", "prefetchwt1", "", "", "", "", ""]), + // 25: 0x0f 0x18 + Group([ + "prefetchnta", + "prefetcht0", + "prefetcht1", + "prefetcht2", + "", + "", + "", + "", + ]), + // 26: 0x0f 0x71 + Group(["", "", "psrlw", "", "psraw", "", "psllw", ""]), + // 27: 0x0f 0x72 + Group(["", "", "psrld", "", "psrad", "", "pslld", ""]), + // 28: 0x0f 0x73 + Group(["", "", "psrlq", "psrldq", "", "", "psllq", "pslldq"]), +]; + +/// The memory operand size a group's slot touches, where it is not the +/// operand size. Zero is a block with no scalar width, which llvm prints with +/// no size hint at all. `None` means the entry's own size applies. +pub fn group_msize(group: u8, sel: usize) -> Option { + // `Op::None` is in scope here, so the empty answer needs its full name. + let row: [i8; 8] = match group { + // 0x0f 0x00: every slot is a sixteen-bit selector. + G_0F00 => [2; 8], + // 0x0f 0x01: the descriptor tables have no scalar width, the machine + // status word is sixteen bits, and `invlpg` names a byte. + G_0F01 => [0, 0, 0, 0, 2, 2, 2, 1], + // 0x0f 0xae: the state blocks have no width, the control and status + // word are doublewords, and `clflush` names a byte. + G_0FAE => [0, 0, 4, 4, 0, 0, 0, 1], + // 0x0f 0xc7: `cmpxchg8b` touches eight bytes. + G_0FC7 => [-1, 8, -1, -1, -1, -1, -1, -1], + // The prefetch hints all name a byte. + G_0F0D | G_0F18 => [1; 8], + // 0xff /3 and /5 are the far indirect branches, which print no size. + G_FF => [-1, -1, -1, 0, -1, 0, -1, -1], + _ => return Option::None, + }; + match row[sel & 7] { + -1 => Option::None, + n => Some(n as u8), + } +} + +/// The same, for a group's register forms. `sldt` and `str` store into a +/// register at the operand size, while everything else in their group names a +/// sixteen-bit selector whatever the operand size is. +pub fn group_msize_reg(group: u8, sel: usize) -> Option { + match (group, sel) { + (G_0F00, 2..=5) => Some(2), + _ => Option::None, + } +} + +/// Condition names, indexed by the low four bits of a `jcc`, `setcc` or +/// `cmovcc` opcode. +pub const CC: [&str; 16] = [ + "o", "no", "b", "ae", "e", "ne", "be", "a", "s", "ns", "p", "np", "l", "ge", "le", "g", +]; + +/// Mnemonics for `jcc` with a one-byte displacement, `0x70` to `0x7f`. +pub const JCC: [&str; 16] = [ + "jo", "jno", "jb", "jae", "je", "jne", "jbe", "ja", "js", "jns", "jp", "jnp", "jl", "jge", + "jle", "jg", +]; + +/// Mnemonics for `setcc`, `0x0f 0x90` to `0x0f 0x9f`. +pub const SETCC: [&str; 16] = [ + "seto", "setno", "setb", "setae", "sete", "setne", "setbe", "seta", "sets", "setns", "setp", + "setnp", "setl", "setge", "setle", "setg", +]; + +/// Mnemonics for `cmovcc`, `0x0f 0x40` to `0x0f 0x4f`. +pub const CMOVCC: [&str; 16] = [ + "cmovo", "cmovno", "cmovb", "cmovae", "cmove", "cmovne", "cmovbe", "cmova", "cmovs", "cmovns", + "cmovp", "cmovnp", "cmovl", "cmovge", "cmovle", "cmovg", +]; + +/// The one-byte opcode map. +/// +/// Opcodes removed in 64-bit mode (the one-byte `inc`/`dec` block, `pusha`, +/// `arpl`, the BCD instructions) are [`BAD`], because in long mode those bytes +/// are REX prefixes or nothing at all. +pub const ONE_BYTE: [Entry; 256] = { + let mut t = [BAD; 256]; + // 0x00: the eight arithmetic groups, each six encodings. + t[0x00] = e("add", Eb, Gb, None); + t[0x01] = e("add", Ev, Gv, None); + t[0x02] = e("add", Gb, Eb, None); + t[0x03] = e("add", Gv, Ev, None); + t[0x04] = e("add", AL, Ib, None); + t[0x05] = e("add", EAX, Iz, None); + t[0x08] = e("or", Eb, Gb, None); + t[0x09] = e("or", Ev, Gv, None); + t[0x0a] = e("or", Gb, Eb, None); + t[0x0b] = e("or", Gv, Ev, None); + t[0x0c] = e("or", AL, Ib, None); + t[0x0d] = e("or", EAX, Iz, None); + t[0x10] = e("adc", Eb, Gb, None); + t[0x11] = e("adc", Ev, Gv, None); + t[0x12] = e("adc", Gb, Eb, None); + t[0x13] = e("adc", Gv, Ev, None); + t[0x14] = e("adc", AL, Ib, None); + t[0x15] = e("adc", EAX, Iz, None); + t[0x18] = e("sbb", Eb, Gb, None); + t[0x19] = e("sbb", Ev, Gv, None); + t[0x1a] = e("sbb", Gb, Eb, None); + t[0x1b] = e("sbb", Gv, Ev, None); + t[0x1c] = e("sbb", AL, Ib, None); + t[0x1d] = e("sbb", EAX, Iz, None); + t[0x20] = e("and", Eb, Gb, None); + t[0x21] = e("and", Ev, Gv, None); + t[0x22] = e("and", Gb, Eb, None); + t[0x23] = e("and", Gv, Ev, None); + t[0x24] = e("and", AL, Ib, None); + t[0x25] = e("and", EAX, Iz, None); + t[0x28] = e("sub", Eb, Gb, None); + t[0x29] = e("sub", Ev, Gv, None); + t[0x2a] = e("sub", Gb, Eb, None); + t[0x2b] = e("sub", Gv, Ev, None); + t[0x2c] = e("sub", AL, Ib, None); + t[0x2d] = e("sub", EAX, Iz, None); + t[0x30] = e("xor", Eb, Gb, None); + t[0x31] = e("xor", Ev, Gv, None); + t[0x32] = e("xor", Gb, Eb, None); + t[0x33] = e("xor", Gv, Ev, None); + t[0x34] = e("xor", AL, Ib, None); + t[0x35] = e("xor", EAX, Iz, None); + t[0x38] = e("cmp", Eb, Gb, None); + t[0x39] = e("cmp", Ev, Gv, None); + t[0x3a] = e("cmp", Gb, Eb, None); + t[0x3b] = e("cmp", Gv, Ev, None); + t[0x3c] = e("cmp", AL, Ib, None); + t[0x3d] = e("cmp", EAX, Iz, None); + + // 0x50: push and pop of the opcode-embedded register. + let mut i = 0; + while i < 8 { + t[0x50 + i] = e("push", Rv, None, None); + t[0x58 + i] = e("pop", Rv, None, None); + i += 1; + } + + t[0x63] = e("movsxd", Gv, Ed, None); + t[0x68] = e("push", Iz, None, None); + t[0x69] = e("imul", Gv, Ev, Iz); + t[0x6a] = e("push", Ibs, None, None); + t[0x6b] = e("imul", Gv, Ev, Ibs); + t[0x6c] = e("insb", None, None, None); + t[0x6d] = e("insd", None, None, None); + t[0x6e] = e("outsb", None, None, None); + t[0x6f] = e("outsd", None, None, None); + + // 0x70: the short conditional jumps. + let mut i = 0; + while i < 16 { + t[0x70 + i] = e(JCC[i], Jb, None, None); + i += 1; + } + + t[0x80] = g(G_80, Eb, Ib); + t[0x81] = g(G_81, Ev, Iz); + t[0x83] = g(G_83, Ev, Ibs); + t[0x84] = e("test", Eb, Gb, None); + t[0x85] = e("test", Ev, Gv, None); + t[0x86] = e("xchg", Eb, Gb, None); + t[0x87] = e("xchg", Ev, Gv, None); + t[0x88] = e("mov", Eb, Gb, None); + t[0x89] = e("mov", Ev, Gv, None); + t[0x8a] = e("mov", Gb, Eb, None); + t[0x8b] = e("mov", Gv, Ev, None); + t[0x8c] = e("mov", Ewm, Sw, None); + t[0x8d] = e("lea", Gv, M, None); + t[0x8e] = e("mov", Sw, Ewm, None); + t[0x8f] = g(G_8F, Ev, None); + + t[0x90] = e("nop", None, None, None); + let mut i = 1; + while i < 8 { + t[0x90 + i] = e("xchg", EAX, Rv, None); + i += 1; + } + t[0x98] = e("cwde", None, None, None); + t[0x99] = e("cdq", None, None, None); + t[0x9b] = e("wait", None, None, None); + t[0x9c] = e("pushf", None, None, None); + t[0x9d] = e("popf", None, None, None); + t[0x9e] = e("sahf", None, None, None); + t[0x9f] = e("lahf", None, None, None); + + t[0xa0] = e("mov", AL, Ob, None); + t[0xa1] = e("mov", EAX, Ov, None); + t[0xa2] = e("mov", Ob, AL, None); + t[0xa3] = e("mov", Ov, EAX, None); + t[0xa4] = e("movsb", None, None, None); + t[0xa5] = e("movsd", None, None, None); + t[0xa6] = e("cmpsb", None, None, None); + t[0xa7] = e("cmpsd", None, None, None); + t[0xa8] = e("test", AL, Ib, None); + t[0xa9] = e("test", EAX, Iz, None); + t[0xaa] = e("stosb", None, None, None); + t[0xab] = e("stosd", None, None, None); + t[0xac] = e("lodsb", None, None, None); + t[0xad] = e("lodsd", None, None, None); + t[0xae] = e("scasb", None, None, None); + t[0xaf] = e("scasd", None, None, None); + + let mut i = 0; + while i < 8 { + t[0xb0 + i] = e("mov", Rb, Ib, None); + t[0xb8 + i] = e("mov", Rv, Iv, None); + i += 1; + } + + t[0xc0] = g(G_C0, Eb, Ibu); + t[0xc1] = g(G_C1, Ev, Ibu); + t[0xc2] = e("ret", Iw, None, None); + t[0xc3] = e("ret", None, None, None); + t[0xc6] = g(G_C6, Eb, Ib); + t[0xc7] = g(G_C7, Ev, Iz); + t[0xc8] = e("enter", Iw, Ib, None); + t[0xc9] = e("leave", None, None, None); + t[0xca] = e("lret", Iw, None, None); + t[0xcb] = e("lret", None, None, None); + t[0xcc] = e("int3", None, None, None); + t[0xcd] = e("int", Ibu, None, None); + t[0xcf] = e("iret", None, None, None); + + // The shift-by-one forms print with no count, as both objdumps do: the + // opcode itself is the count. + t[0xd0] = g(G_D0, Eb, None); + t[0xd1] = g(G_D1, Ev, None); + t[0xd2] = g(G_D2, Eb, CL); + t[0xd3] = g(G_D3, Ev, CL); + t[0xd7] = e("xlatb", None, None, None); + + t[0xe0] = e("loopne", Jb, None, None); + t[0xe1] = e("loope", Jb, None, None); + t[0xe2] = e("loop", Jb, None, None); + t[0xe3] = e("jrcxz", Jb, None, None); + t[0xe4] = e("in", AL, Ibu, None); + t[0xe5] = e("in", EAX, Ibu, None); + t[0xe6] = e("out", Ibu, AL, None); + t[0xe7] = e("out", Ibu, EAX, None); + t[0xe8] = e("call", Jz, None, None); + t[0xe9] = e("jmp", Jz, None, None); + t[0xeb] = e("jmp", Jb, None, None); + t[0xec] = e("in", AL, DX, None); + t[0xed] = e("in", EAX, DX, None); + t[0xee] = e("out", DX, AL, None); + t[0xef] = e("out", DX, EAX, None); + + t[0xf4] = e("hlt", None, None, None); + t[0xf5] = e("cmc", None, None, None); + t[0xf6] = g(G_F6, Eb, None); + t[0xf7] = g(G_F7, Ev, None); + t[0xf8] = e("clc", None, None, None); + t[0xf9] = e("stc", None, None, None); + t[0xfa] = e("cli", None, None, None); + t[0xfb] = e("sti", None, None, None); + t[0xfc] = e("cld", None, None, None); + t[0xfd] = e("std", None, None, None); + t[0xfe] = g(G_FE, Eb, None); + t[0xff] = g(G_FF, Ev, None); + t +}; + +/// The one-byte map as 32-bit mode sees it. +/// +/// Same table, minus the encodings long mode reassigned and plus the ones it +/// dropped. This is the whole difference that matters at the first byte: 0x40 +/// to 0x4f are `inc` and `dec` here rather than REX, and a decoder that gets +/// that wrong desynchronizes on the most common byte in the map. +pub const ONE_BYTE_32: [Entry; 256] = { + let mut t = ONE_BYTE; + + // The segment push and pop pairs, which long mode dropped. + t[0x06] = e("push", Sr, None, None); + t[0x07] = e("pop", Sr, None, None); + t[0x0e] = e("push", Sr, None, None); + t[0x16] = e("push", Sr, None, None); + t[0x17] = e("pop", Sr, None, None); + t[0x1e] = e("push", Sr, None, None); + t[0x1f] = e("pop", Sr, None, None); + + // The decimal adjust instructions. + t[0x27] = e("daa", None, None, None); + t[0x2f] = e("das", None, None, None); + t[0x37] = e("aaa", None, None, None); + t[0x3f] = e("aas", None, None, None); + + // 0x40 to 0x4f: inc and dec of a register, not a REX prefix. + let mut i = 0; + while i < 8 { + t[0x40 + i] = e("inc", Rv, None, None); + t[0x48 + i] = e("dec", Rv, None, None); + i += 1; + } + + t[0x60] = e("pushal", None, None, None); + t[0x61] = e("popal", None, None, None); + t[0x62] = e("bound", Gv, M, None); + // 0x63 is `arpl`, not `movsxd`, and it is a word operation at any size. + t[0x63] = e("arpl", Ew, Gw, None); + + // 0x82 duplicates 0x80. Long mode reclaimed it; here it is a real alias. + t[0x82] = g(G_82, Eb, Ib); + + t[0x9a] = e("lcall", Ap, None, None); + + t[0xc4] = e("les", Gv, Mp, None); + t[0xc5] = e("lds", Gv, Mp, None); + t[0xce] = e("into", None, None, None); + t[0xd4] = e("aam", Ib, None, None); + t[0xd5] = e("aad", Ib, None, None); + t[0xd6] = e("salc", None, None, None); + t[0xea] = e("ljmp", Ap, None, None); + t +}; + +/// The two-byte map, for opcodes with no mandatory prefix. +pub const TWO_BYTE: [Entry; 256] = { + let mut t = [BAD; 256]; + t[0x00] = g(G_0F00, Ev, None); + t[0x01] = g(G_0F01, M, None); + t[0x05] = e("syscall", None, None, None); + t[0x06] = e("clts", None, None, None); + t[0x07] = e("sysret", None, None, None); + t[0x02] = e("lar", Gv, Ew, None); + t[0x03] = e("lsl", Gv, Ew, None); + t[0x0b] = e("ud2", None, None, None); + t[0x0d] = g(G_0F0D, M, None); + t[0x10] = e("movups", Vx, Wx, None); + t[0x11] = e("movups", Wx, Vx, None); + t[0x12] = es("movlps", Vx, Wx, None, 8); + t[0x13] = es("movlps", Wx, Vx, None, 8); + t[0x14] = e("unpcklps", Vx, Wx, None); + t[0x15] = e("unpckhps", Vx, Wx, None); + t[0x16] = es("movhps", Vx, Wx, None, 8); + t[0x17] = es("movhps", Wx, Vx, None, 8); + t[0x18] = g(G_0F18, M, None); + t[0x1f] = e("nop", Ev, None, None); + t[0x20] = e("mov", Eq, Cd, None); + t[0x21] = e("mov", Eq, Dd, None); + t[0x22] = e("mov", Cd, Eq, None); + t[0x23] = e("mov", Dd, Eq, None); + t[0x28] = e("movaps", Vx, Wx, None); + t[0x29] = e("movaps", Wx, Vx, None); + t[0x2a] = e("cvtpi2ps", Vx, Qq, None); + t[0x2c] = es("cvttps2pi", Pq, Wx, None, 8); + t[0x2d] = es("cvtps2pi", Pq, Wx, None, 8); + t[0x2e] = es("ucomiss", Vx, Wx, None, 4); + t[0x2f] = es("comiss", Vx, Wx, None, 4); + t[0x31] = e("rdtsc", None, None, None); + t[0x34] = e("sysenter", None, None, None); + t[0x35] = e("sysexit", None, None, None); + + let mut i = 0; + while i < 16 { + t[0x40 + i] = e(CMOVCC[i], Gv, Ev, None); + t[0x80 + i] = e(JCC[i], Jz, None, None); + t[0x90 + i] = e(SETCC[i], Eb, None, None); + i += 1; + } + + t[0x51] = e("sqrtps", Vx, Wx, None); + t[0xc2] = e("cmpps", Vx, Wx, Ibu); + t[0xc6] = e("shufps", Vx, Wx, Ibu); + t[0x54] = e("andps", Vx, Wx, None); + t[0x55] = e("andnps", Vx, Wx, None); + t[0x56] = e("orps", Vx, Wx, None); + t[0x57] = e("xorps", Vx, Wx, None); + t[0x58] = e("addps", Vx, Wx, None); + t[0x59] = e("mulps", Vx, Wx, None); + t[0x5a] = es("cvtps2pd", Vx, Wx, None, 8); + t[0x5b] = e("cvtdq2ps", Vx, Wx, None); + t[0x5c] = e("subps", Vx, Wx, None); + t[0x5d] = e("minps", Vx, Wx, None); + t[0x5e] = e("divps", Vx, Wx, None); + t[0x5f] = e("maxps", Vx, Wx, None); + + t[0x6e] = e("movd", Pq, Ev, None); + t[0x6f] = e("movq", Pq, Qq, None); + t[0x7e] = e("movd", Ev, Pq, None); + t[0x7f] = e("movq", Qq, Pq, None); + + t[0xa0] = e("push", Sr, None, None); + t[0xa1] = e("pop", Sr, None, None); + t[0xa2] = e("cpuid", None, None, None); + t[0xa8] = e("push", Sr, None, None); + t[0xa9] = e("pop", Sr, None, None); + t[0xa3] = e("bt", Ev, Gv, None); + t[0xa4] = e("shld", Ev, Gv, Ibu); + t[0xa5] = e("shld", Ev, Gv, CL); + t[0xab] = e("bts", Ev, Gv, None); + t[0xac] = e("shrd", Ev, Gv, Ibu); + t[0xad] = e("shrd", Ev, Gv, CL); + t[0xae] = g(G_0FAE, M, None); + t[0xaf] = e("imul", Gv, Ev, None); + t[0xb0] = e("cmpxchg", Eb, Gb, None); + t[0xb1] = e("cmpxchg", Ev, Gv, None); + t[0xb2] = e("lss", Gv, Mp, None); + t[0xb3] = e("btr", Ev, Gv, None); + t[0xb4] = e("lfs", Gv, Mp, None); + t[0xb5] = e("lgs", Gv, Mp, None); + t[0xb6] = e("movzx", Gv, Eb, None); + t[0xb7] = e("movzx", Gv, Ew, None); + t[0xba] = g(G_0FBA, Ev, Ibu); + t[0xb9] = e("ud1", Gv, Ev, None); + t[0xbb] = e("btc", Ev, Gv, None); + t[0xbc] = e("bsf", Gv, Ev, None); + t[0xbd] = e("bsr", Gv, Ev, None); + t[0xbe] = e("movsx", Gv, Eb, None); + t[0xbf] = e("movsx", Gv, Ew, None); + t[0xc0] = e("xadd", Eb, Gb, None); + t[0xc1] = e("xadd", Ev, Gv, None); + t[0xc3] = e("movnti", Ed, Gd, None); + t[0xc7] = gs(G_0FC7, M, None, 8); + + let mut i = 0; + while i < 8 { + t[0xc8 + i] = e("bswap", Rv, None, None); + i += 1; + } + + // The MMX register file, which has no mandatory prefix: the same opcodes + // with 0x66 are the SSE2 forms in [`TWO_BYTE_66`]. A compiler stopped + // emitting these long ago and a disassembler still meets them. + t[0x0e] = e("femms", None, None, None); + t[0x08] = e("invd", None, None, None); + t[0x09] = e("wbinvd", None, None, None); + t[0x30] = e("wrmsr", None, None, None); + t[0x32] = e("rdmsr", None, None, None); + t[0x33] = e("rdpmc", None, None, None); + t[0x2b] = e("movntps", Wx, Vx, None); + t[0x50] = e("movmskps", Gd, Ux, None); + t[0x52] = e("rsqrtps", Vx, Wx, None); + t[0x53] = e("rcpps", Vx, Wx, None); + t[0x60] = es("punpcklbw", Pq, Qq, None, 4); + t[0x61] = es("punpcklwd", Pq, Qq, None, 4); + t[0x62] = es("punpckldq", Pq, Qq, None, 4); + t[0x63] = es("packsswb", Pq, Qq, None, 8); + t[0x64] = es("pcmpgtb", Pq, Qq, None, 8); + t[0x65] = es("pcmpgtw", Pq, Qq, None, 8); + t[0x66] = es("pcmpgtd", Pq, Qq, None, 8); + t[0x67] = es("packuswb", Pq, Qq, None, 8); + t[0x68] = es("punpckhbw", Pq, Qq, None, 8); + t[0x69] = es("punpckhwd", Pq, Qq, None, 8); + t[0x6a] = es("punpckhdq", Pq, Qq, None, 8); + t[0x6b] = es("packssdw", Pq, Qq, None, 8); + t[0x70] = es("pshufw", Pq, Qq, Ibu, 8); + t[0x71] = g(G_0F71, Nq, Ibu); + t[0x72] = g(G_0F72, Nq, Ibu); + t[0x73] = g(G_0F73, Nq, Ibu); + t[0x74] = es("pcmpeqb", Pq, Qq, None, 8); + t[0x75] = es("pcmpeqw", Pq, Qq, None, 8); + t[0x76] = es("pcmpeqd", Pq, Qq, None, 8); + t[0x77] = e("emms", None, None, None); + t[0xc4] = e("pinsrw", Pq, Ewm, Ibu); + t[0xc5] = e("pextrw", Gd, Nq, Ibu); + t[0xd1] = es("psrlw", Pq, Qq, None, 8); + t[0xd2] = es("psrld", Pq, Qq, None, 8); + t[0xd3] = es("psrlq", Pq, Qq, None, 8); + t[0xd4] = es("paddq", Pq, Qq, None, 8); + t[0xd5] = es("pmullw", Pq, Qq, None, 8); + t[0xd7] = e("pmovmskb", Gd, Nq, None); + t[0xd8] = es("psubusb", Pq, Qq, None, 8); + t[0xd9] = es("psubusw", Pq, Qq, None, 8); + t[0xda] = es("pminub", Pq, Qq, None, 8); + t[0xdb] = es("pand", Pq, Qq, None, 8); + t[0xdc] = es("paddusb", Pq, Qq, None, 8); + t[0xdd] = es("paddusw", Pq, Qq, None, 8); + t[0xde] = es("pmaxub", Pq, Qq, None, 8); + t[0xdf] = es("pandn", Pq, Qq, None, 8); + t[0xe0] = es("pavgb", Pq, Qq, None, 8); + t[0xe1] = es("psraw", Pq, Qq, None, 8); + t[0xe2] = es("psrad", Pq, Qq, None, 8); + t[0xe3] = es("pavgw", Pq, Qq, None, 8); + t[0xe4] = es("pmulhuw", Pq, Qq, None, 8); + t[0xe5] = es("pmulhw", Pq, Qq, None, 8); + t[0xe7] = es("movntq", Qq, Pq, None, 8); + t[0xe8] = es("psubsb", Pq, Qq, None, 8); + t[0xe9] = es("psubsw", Pq, Qq, None, 8); + t[0xea] = es("pminsw", Pq, Qq, None, 8); + t[0xeb] = es("por", Pq, Qq, None, 8); + t[0xec] = es("paddsb", Pq, Qq, None, 8); + t[0xed] = es("paddsw", Pq, Qq, None, 8); + t[0xee] = es("pmaxsw", Pq, Qq, None, 8); + t[0xef] = es("pxor", Pq, Qq, None, 8); + t[0xf1] = es("psllw", Pq, Qq, None, 8); + t[0xf2] = es("pslld", Pq, Qq, None, 8); + t[0xf3] = es("psllq", Pq, Qq, None, 8); + t[0xf4] = es("pmuludq", Pq, Qq, None, 8); + t[0xf5] = es("pmaddwd", Pq, Qq, None, 8); + t[0xf6] = es("psadbw", Pq, Qq, None, 8); + t[0xf7] = e("maskmovq", Pq, Nq, None); + t[0xf8] = es("psubb", Pq, Qq, None, 8); + t[0xf9] = es("psubw", Pq, Qq, None, 8); + t[0xfa] = es("psubd", Pq, Qq, None, 8); + t[0xfb] = es("psubq", Pq, Qq, None, 8); + t[0xfc] = es("paddb", Pq, Qq, None, 8); + t[0xfd] = es("paddw", Pq, Qq, None, 8); + t[0xfe] = es("paddd", Pq, Qq, None, 8); + t +}; + +/// Two-byte opcodes selected by a mandatory `0x66` prefix. +pub const TWO_BYTE_66: [Entry; 256] = { + let mut t = [BAD; 256]; + t[0x10] = e("movupd", Vx, Wx, None); + t[0x11] = e("movupd", Wx, Vx, None); + t[0x12] = es("movlpd", Vx, Wx, None, 8); + t[0x13] = es("movlpd", Wx, Vx, None, 8); + t[0x14] = e("unpcklpd", Vx, Wx, None); + t[0x15] = e("unpckhpd", Vx, Wx, None); + t[0x16] = es("movhpd", Vx, Wx, None, 8); + t[0x17] = es("movhpd", Wx, Vx, None, 8); + t[0x28] = e("movapd", Vx, Wx, None); + t[0x29] = e("movapd", Wx, Vx, None); + t[0x2e] = es("ucomisd", Vx, Wx, None, 8); + t[0x2f] = es("comisd", Vx, Wx, None, 8); + t[0x51] = e("sqrtpd", Vx, Wx, None); + t[0x54] = e("andpd", Vx, Wx, None); + t[0x55] = e("andnpd", Vx, Wx, None); + t[0x56] = e("orpd", Vx, Wx, None); + t[0x57] = e("xorpd", Vx, Wx, None); + t[0x58] = e("addpd", Vx, Wx, None); + t[0x59] = e("mulpd", Vx, Wx, None); + t[0x5a] = e("cvtpd2ps", Vx, Wx, None); + t[0x5b] = e("cvtps2dq", Vx, Wx, None); + t[0x5c] = e("subpd", Vx, Wx, None); + t[0x5d] = e("minpd", Vx, Wx, None); + t[0x5e] = e("divpd", Vx, Wx, None); + t[0x5f] = e("maxpd", Vx, Wx, None); + t[0x60] = e("punpcklbw", Vx, Wx, None); + t[0x61] = e("punpcklwd", Vx, Wx, None); + t[0x62] = e("punpckldq", Vx, Wx, None); + t[0x63] = e("packsswb", Vx, Wx, None); + t[0x64] = e("pcmpgtb", Vx, Wx, None); + t[0x65] = e("pcmpgtw", Vx, Wx, None); + t[0x66] = e("pcmpgtd", Vx, Wx, None); + t[0x67] = e("packuswb", Vx, Wx, None); + t[0x68] = e("punpckhbw", Vx, Wx, None); + t[0x69] = e("punpckhwd", Vx, Wx, None); + t[0x6a] = e("punpckhdq", Vx, Wx, None); + t[0x6b] = e("packssdw", Vx, Wx, None); + t[0x6c] = e("punpcklqdq", Vx, Wx, None); + t[0x6d] = e("punpckhqdq", Vx, Wx, None); + t[0x6e] = e("movd", Vx, Ev, None); + t[0x6f] = e("movdqa", Vx, Wx, None); + t[0x70] = e("pshufd", Vx, Wx, Ibu); + t[0x74] = e("pcmpeqb", Vx, Wx, None); + t[0xc2] = e("cmppd", Vx, Wx, Ibu); + t[0xc5] = e("pextrw", Gd, Ux, Ibu); + t[0xc6] = e("shufpd", Vx, Wx, Ibu); + t[0x2a] = es("cvtpi2pd", Vx, Qq, None, 8); + t[0x2c] = e("cvttpd2pi", Pq, Wx, None); + t[0x2d] = e("cvtpd2pi", Pq, Wx, None); + t[0x7c] = e("haddpd", Vx, Wx, None); + t[0x7d] = e("hsubpd", Vx, Wx, None); + t[0xd0] = e("addsubpd", Vx, Wx, None); + t[0xe6] = e("cvttpd2dq", Vx, Wx, None); + t[0x75] = e("pcmpeqw", Vx, Wx, None); + t[0x76] = e("pcmpeqd", Vx, Wx, None); + t[0x7e] = e("movd", Ev, Vx, None); + t[0x7f] = e("movdqa", Wx, Vx, None); + t[0x2b] = e("movntpd", Wx, Vx, None); + t[0x50] = e("movmskpd", Gd, Ux, None); + t[0x71] = g(G_0F71, Ux, Ibu); + t[0x72] = g(G_0F72, Ux, Ibu); + t[0x73] = g(G_0F73, Ux, Ibu); + t[0xc4] = e("pinsrw", Vx, Ewm, Ibu); + t[0xd1] = e("psrlw", Vx, Wx, None); + t[0xd2] = e("psrld", Vx, Wx, None); + t[0xd3] = e("psrlq", Vx, Wx, None); + t[0xe1] = e("psraw", Vx, Wx, None); + t[0xe2] = e("psrad", Vx, Wx, None); + t[0xe4] = e("pmulhuw", Vx, Wx, None); + t[0xe5] = e("pmulhw", Vx, Wx, None); + t[0xe7] = e("movntdq", Wx, Vx, None); + t[0xf1] = e("psllw", Vx, Wx, None); + t[0xf2] = e("pslld", Vx, Wx, None); + t[0xf3] = e("psllq", Vx, Wx, None); + t[0xf7] = e("maskmovdqu", Vx, Ux, None); + t[0xd4] = e("paddq", Vx, Wx, None); + t[0xd5] = e("pmullw", Vx, Wx, None); + t[0xd6] = es("movq", Wx, Vx, None, 8); + t[0xd7] = e("pmovmskb", Gd, Ux, None); + t[0xd8] = e("psubusb", Vx, Wx, None); + t[0xd9] = e("psubusw", Vx, Wx, None); + t[0xda] = e("pminub", Vx, Wx, None); + t[0xdb] = e("pand", Vx, Wx, None); + t[0xdc] = e("paddusb", Vx, Wx, None); + t[0xdd] = e("paddusw", Vx, Wx, None); + t[0xde] = e("pmaxub", Vx, Wx, None); + t[0xdf] = e("pandn", Vx, Wx, None); + t[0xe0] = e("pavgb", Vx, Wx, None); + t[0xe3] = e("pavgw", Vx, Wx, None); + t[0xe8] = e("psubsb", Vx, Wx, None); + t[0xe9] = e("psubsw", Vx, Wx, None); + t[0xea] = e("pminsw", Vx, Wx, None); + t[0xeb] = e("por", Vx, Wx, None); + t[0xec] = e("paddsb", Vx, Wx, None); + t[0xed] = e("paddsw", Vx, Wx, None); + t[0xee] = e("pmaxsw", Vx, Wx, None); + t[0xef] = e("pxor", Vx, Wx, None); + t[0xf4] = e("pmuludq", Vx, Wx, None); + t[0xf5] = e("pmaddwd", Vx, Wx, None); + t[0xf6] = e("psadbw", Vx, Wx, None); + t[0xf8] = e("psubb", Vx, Wx, None); + t[0xf9] = e("psubw", Vx, Wx, None); + t[0xfa] = e("psubd", Vx, Wx, None); + t[0xfb] = e("psubq", Vx, Wx, None); + t[0xfc] = e("paddb", Vx, Wx, None); + t[0xfd] = e("paddw", Vx, Wx, None); + t[0xfe] = e("paddd", Vx, Wx, None); + t +}; + +/// Two-byte opcodes selected by a mandatory `0xf3` prefix. +pub const TWO_BYTE_F3: [Entry; 256] = { + let mut t = [BAD; 256]; + t[0x10] = es("movss", Vx, Wx, None, 4); + t[0x11] = es("movss", Wx, Vx, None, 4); + t[0x2a] = e("cvtsi2ss", Vx, Ev, None); + t[0x2c] = es("cvttss2si", Gv, Wx, None, 4); + t[0x2d] = es("cvtss2si", Gv, Wx, None, 4); + t[0x51] = es("sqrtss", Vx, Wx, None, 4); + t[0x58] = es("addss", Vx, Wx, None, 4); + t[0x59] = es("mulss", Vx, Wx, None, 4); + t[0x5a] = es("cvtss2sd", Vx, Wx, None, 4); + t[0x5b] = e("cvttps2dq", Vx, Wx, None); + t[0x5c] = es("subss", Vx, Wx, None, 4); + t[0x5d] = es("minss", Vx, Wx, None, 4); + t[0x5e] = es("divss", Vx, Wx, None, 4); + t[0x5f] = es("maxss", Vx, Wx, None, 4); + t[0x6f] = e("movdqu", Vx, Wx, None); + t[0xc2] = es("cmpss", Vx, Wx, Ibu, 4); + t[0x70] = e("pshufhw", Vx, Wx, Ibu); + t[0x7e] = es("movq", Vx, Wx, None, 8); + t[0x7f] = e("movdqu", Wx, Vx, None); + t[0xb8] = e("popcnt", Gv, Ev, None); + t[0xbc] = e("tzcnt", Gv, Ev, None); + t[0xbd] = e("lzcnt", Gv, Ev, None); + t[0xe6] = es("cvtdq2pd", Vx, Wx, None, 8); + t[0x09] = e("wbnoinvd", None, None, None); + t[0x12] = e("movsldup", Vx, Wx, None); + t[0x16] = e("movshdup", Vx, Wx, None); + t[0x2b] = es("movntss", Wx, Vx, None, 4); + t[0x52] = es("rsqrtss", Vx, Wx, None, 4); + t[0x53] = es("rcpss", Vx, Wx, None, 4); + t +}; + +/// Two-byte opcodes selected by a mandatory `0xf2` prefix. +pub const TWO_BYTE_F2: [Entry; 256] = { + let mut t = [BAD; 256]; + t[0x10] = es("movsd", Vx, Wx, None, 8); + t[0x11] = es("movsd", Wx, Vx, None, 8); + t[0x12] = es("movddup", Vx, Wx, None, 8); + t[0x2a] = e("cvtsi2sd", Vx, Ev, None); + t[0x2c] = es("cvttsd2si", Gv, Wx, None, 8); + t[0x2d] = es("cvtsd2si", Gv, Wx, None, 8); + t[0x51] = es("sqrtsd", Vx, Wx, None, 8); + t[0x58] = es("addsd", Vx, Wx, None, 8); + t[0x59] = es("mulsd", Vx, Wx, None, 8); + t[0x5a] = es("cvtsd2ss", Vx, Wx, None, 8); + t[0x5c] = es("subsd", Vx, Wx, None, 8); + t[0x5d] = es("minsd", Vx, Wx, None, 8); + t[0x5e] = es("divsd", Vx, Wx, None, 8); + t[0x5f] = es("maxsd", Vx, Wx, None, 8); + t[0x70] = e("pshuflw", Vx, Wx, Ibu); + t[0xc2] = es("cmpsd", Vx, Wx, Ibu, 8); + t[0xe6] = e("cvtpd2dq", Vx, Wx, None); + t[0x2b] = es("movntsd", Wx, Vx, None, 8); + t[0x7c] = e("haddps", Vx, Wx, None); + t[0x7d] = e("hsubps", Vx, Wx, None); + t[0xd0] = e("addsubps", Vx, Wx, None); + t[0xf0] = e("lddqu", Vx, Wx, None); + t +}; + +/// The `0x0f 0x38` map with a mandatory `0x66`, which is where SSSE3 and SSE4 +/// put the instructions compilers reach for. +pub const THREE_BYTE_38_66: [Entry; 256] = { + let mut t = [BAD; 256]; + t[0x00] = e("pshufb", Vx, Wx, None); + t[0x01] = e("phaddw", Vx, Wx, None); + t[0x02] = e("phaddd", Vx, Wx, None); + t[0x04] = e("pmaddubsw", Vx, Wx, None); + t[0x05] = e("phsubw", Vx, Wx, None); + t[0x06] = e("phsubd", Vx, Wx, None); + t[0x08] = e("psignb", Vx, Wx, None); + t[0x09] = e("psignw", Vx, Wx, None); + t[0x0a] = e("psignd", Vx, Wx, None); + t[0x0b] = e("pmulhrsw", Vx, Wx, None); + t[0x10] = e("pblendvb", Vx, Wx, XMM0); + t[0x14] = e("blendvps", Vx, Wx, XMM0); + t[0x15] = e("blendvpd", Vx, Wx, XMM0); + t[0x17] = e("ptest", Vx, Wx, None); + t[0x1c] = e("pabsb", Vx, Wx, None); + t[0x1d] = e("pabsw", Vx, Wx, None); + t[0x1e] = e("pabsd", Vx, Wx, None); + t[0x20] = es("pmovsxbw", Vx, Wx, None, 8); + t[0x21] = es("pmovsxbd", Vx, Wx, None, 4); + t[0x23] = es("pmovsxwd", Vx, Wx, None, 8); + t[0x25] = es("pmovsxdq", Vx, Wx, None, 8); + t[0x28] = e("pmuldq", Vx, Wx, None); + t[0x29] = e("pcmpeqq", Vx, Wx, None); + t[0x2b] = e("packusdw", Vx, Wx, None); + t[0x30] = es("pmovzxbw", Vx, Wx, None, 8); + t[0x31] = es("pmovzxbd", Vx, Wx, None, 4); + t[0x33] = es("pmovzxwd", Vx, Wx, None, 8); + t[0x35] = es("pmovzxdq", Vx, Wx, None, 8); + t[0x37] = e("pcmpgtq", Vx, Wx, None); + t[0x38] = e("pminsb", Vx, Wx, None); + t[0x39] = e("pminsd", Vx, Wx, None); + t[0x3a] = e("pminuw", Vx, Wx, None); + t[0x3b] = e("pminud", Vx, Wx, None); + t[0x3c] = e("pmaxsb", Vx, Wx, None); + t[0x3d] = e("pmaxsd", Vx, Wx, None); + t[0x3e] = e("pmaxuw", Vx, Wx, None); + t[0x3f] = e("pmaxud", Vx, Wx, None); + t[0x40] = e("pmulld", Vx, Wx, None); + t[0x41] = e("phminposuw", Vx, Wx, None); + t +}; + +/// The `0x0f 0x38` map with no prefix, which is the MMX half of SSSE3. +pub const THREE_BYTE_38: [Entry; 256] = { + let mut t = [BAD; 256]; + t[0x00] = es("pshufb", Pq, Qq, None, 8); + t[0x01] = es("phaddw", Pq, Qq, None, 8); + t[0x02] = es("phaddd", Pq, Qq, None, 8); + t[0x03] = es("phaddsw", Pq, Qq, None, 8); + t[0x04] = es("pmaddubsw", Pq, Qq, None, 8); + t[0x05] = es("phsubw", Pq, Qq, None, 8); + t[0x06] = es("phsubd", Pq, Qq, None, 8); + t[0x07] = es("phsubsw", Pq, Qq, None, 8); + t[0x08] = es("psignb", Pq, Qq, None, 8); + t[0x09] = es("psignw", Pq, Qq, None, 8); + t[0x0a] = es("psignd", Pq, Qq, None, 8); + t[0x0b] = es("pmulhrsw", Pq, Qq, None, 8); + t[0x1c] = es("pabsb", Pq, Qq, None, 8); + t[0x1d] = es("pabsw", Pq, Qq, None, 8); + t[0x1e] = es("pabsd", Pq, Qq, None, 8); + t +}; + +/// The `0x0f 0x3a` map with no prefix, which holds one MMX instruction. +pub const THREE_BYTE_3A: [Entry; 256] = { + let mut t = [BAD; 256]; + t[0x0f] = es("palignr", Pq, Qq, Ibu, 8); + t +}; + +/// The `0x0f 0x3a` map with a mandatory `0x66`. +pub const THREE_BYTE_3A_66: [Entry; 256] = { + let mut t = [BAD; 256]; + t[0x0b] = es("roundsd", Vx, Wx, Ibu, 8); + t[0x0a] = es("roundss", Vx, Wx, Ibu, 4); + t[0x08] = e("roundps", Vx, Wx, Ibu); + t[0x09] = e("roundpd", Vx, Wx, Ibu); + t[0x0e] = e("pblendw", Vx, Wx, Ibu); + t[0x0f] = e("palignr", Vx, Wx, Ibu); + t[0x14] = e("pextrb", Ebm, Vx, Ibu); + t[0x15] = e("pextrw", Ewm, Vx, Ibu); + t[0x16] = e("pextrd", Ev, Vx, Ibu); + t[0x17] = es("extractps", Ed, Vx, Ibu, 4); + t[0x20] = e("pinsrb", Vx, Ebm, Ibu); + t[0x21] = es("insertps", Vx, Wx, Ibu, 4); + t[0x22] = e("pinsrd", Vx, Ev, Ibu); + t[0x60] = e("pcmpestrm", Vx, Wx, Ibu); + t[0x61] = e("pcmpestri", Vx, Wx, Ibu); + t[0x62] = e("pcmpistrm", Vx, Wx, Ibu); + t[0x63] = e("pcmpistri", Vx, Wx, Ibu); + t +}; + +/// The comparison predicate that the immediate of `cmpps` and friends selects. +pub const CMP_PRED: [&str; 8] = ["eq", "lt", "le", "unord", "neq", "nlt", "nle", "ord"]; + +/// The memory forms of the x87 escapes, indexed by opcode minus 0xd8 and then +/// by the ModRM reg field, as the mnemonic and the size in bytes it touches. +/// A size of zero is the environment and state blocks, which llvm prints with +/// no size hint because they have no scalar width. +pub const X87_MEM: [[(&str, u8); 8]; 8] = [ + // 0xd8: single precision arithmetic. + [ + ("fadd", 4), + ("fmul", 4), + ("fcom", 4), + ("fcomp", 4), + ("fsub", 4), + ("fsubr", 4), + ("fdiv", 4), + ("fdivr", 4), + ], + // 0xd9: single precision load and store, and the control word. + [ + ("fld", 4), + ("", 0), + ("fst", 4), + ("fstp", 4), + ("fldenv", 0), + ("fldcw", 2), + ("fnstenv", 0), + ("fnstcw", 2), + ], + // 0xda: doubleword integer arithmetic. + [ + ("fiadd", 4), + ("fimul", 4), + ("ficom", 4), + ("ficomp", 4), + ("fisub", 4), + ("fisubr", 4), + ("fidiv", 4), + ("fidivr", 4), + ], + // 0xdb: doubleword integer transfers, and the 80-bit load and store. + [ + ("fild", 4), + ("fisttp", 4), + ("fist", 4), + ("fistp", 4), + ("", 0), + ("fld", 10), + ("", 0), + ("fstp", 10), + ], + // 0xdc: double precision arithmetic. + [ + ("fadd", 8), + ("fmul", 8), + ("fcom", 8), + ("fcomp", 8), + ("fsub", 8), + ("fsubr", 8), + ("fdiv", 8), + ("fdivr", 8), + ], + // 0xdd: double precision transfers, and the whole machine state. + [ + ("fld", 8), + ("fisttp", 8), + ("fst", 8), + ("fstp", 8), + ("frstor", 0), + ("", 0), + ("fnsave", 0), + ("fnstsw", 2), + ], + // 0xde: word integer arithmetic. + [ + ("fiadd", 2), + ("fimul", 2), + ("ficom", 2), + ("ficomp", 2), + ("fisub", 2), + ("fisubr", 2), + ("fidiv", 2), + ("fidivr", 2), + ], + // 0xdf: word integer transfers, the packed decimal pair, and int64. + [ + ("fild", 2), + ("fisttp", 2), + ("fist", 2), + ("fistp", 2), + ("fbld", 10), + ("fild", 8), + ("fbstp", 10), + ("fistp", 8), + ], +]; + +/// `0xd9` with a ModRM byte of 0xe0 or above: no operands, and no pattern +/// either, so this is a plain list indexed by the low five bits. +pub const X87_D9_E0: [&str; 32] = [ + "fchs", "fabs", "", "", "ftst", "fxam", "", "", "fld1", "fldl2t", "fldl2e", "fldpi", "fldlg2", + "fldln2", "fldz", "", "f2xm1", "fyl2x", "fptan", "fpatan", "fxtract", "fprem1", "fdecstp", + "fincstp", "fprem", "fyl2xp1", "fsqrt", "fsincos", "frndint", "fscale", "fsin", "fcos", +]; + +/// `0xd8` register forms, by ModRM reg. `fcom` and `fcomp` take one operand; +/// the rest take `st` and a stack register. +pub const X87_D8_REG: [&str; 8] = [ + "fadd", "fmul", "fcom", "fcomp", "fsub", "fsubr", "fdiv", "fdivr", +]; + +/// `0xdc` register forms, by ModRM reg. The subtract and divide pairs are +/// swapped against the memory forms, which is the architecture's own quirk. +pub const X87_DC_REG: [&str; 8] = ["fadd", "fmul", "", "", "fsubr", "fsub", "fdivr", "fdiv"]; + +/// `0xdd` register forms, by ModRM reg. Each takes one stack register. +pub const X87_DD_REG: [&str; 8] = ["ffree", "", "fst", "fstp", "fucom", "fucomp", "", ""]; + +/// `0xde` register forms, by ModRM reg, with the same swap as `0xdc`. +pub const X87_DE_REG: [&str; 8] = [ + "faddp", "fmulp", "", "", "fsubrp", "fsubp", "fdivrp", "fdivp", +]; + +/// The conditional moves at `0xda` and `0xdb`, by the ModRM reg field. +pub const X87_FCMOV: [&str; 4] = ["b", "e", "be", "u"]; diff --git a/crates/e5r-arch/src/x86/text.rs b/crates/e5r-arch/src/x86/text.rs new file mode 100644 index 0000000..a614202 --- /dev/null +++ b/crates/e5r-arch/src/x86/text.rs @@ -0,0 +1,189 @@ +//! x86 text output in Intel syntax, matching llvm-objdump. +//! +//! Intel rather than AT&T because llvm-objdump's Intel mode is unambiguous +//! about operand order and needs no size suffixes, which makes the G3 parity +//! gate a string comparison. + +use std::fmt::Write; + +use crate::insn::{Insn, Mem, Operand, Reg, RegClass, Width}; + +/// Formatting options. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Style { + /// Reserved for a future AT&T mode. Intel is the only syntax today. + pub att: bool, +} + +const GPR64: [&str; 16] = [ + "rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", "r8", "r9", "r10", "r11", "r12", "r13", + "r14", "r15", +]; +const GPR32: [&str; 16] = [ + "eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi", "r8d", "r9d", "r10d", "r11d", "r12d", + "r13d", "r14d", "r15d", +]; +const GPR16: [&str; 16] = [ + "ax", "cx", "dx", "bx", "sp", "bp", "si", "di", "r8w", "r9w", "r10w", "r11w", "r12w", "r13w", + "r14w", "r15w", +]; +const GPR8: [&str; 16] = [ + "al", "cl", "dl", "bl", "spl", "bpl", "sil", "dil", "r8b", "r9b", "r10b", "r11b", "r12b", + "r13b", "r14b", "r15b", +]; +const GPR8H: [&str; 4] = ["ah", "ch", "dh", "bh"]; +const SEG: [&str; 6] = ["es", "cs", "ss", "ds", "fs", "gs"]; + +/// Render a register. +fn reg(out: &mut String, r: Reg) { + let n = (r.num & 15) as usize; + match r.class { + RegClass::Gpr => out.push_str(match r.width { + Width::W8 => GPR8[n], + Width::W16 => GPR16[n], + Width::W32 => GPR32[n], + _ => GPR64[n], + }), + RegClass::GprHigh => out.push_str(GPR8H[(r.num & 3) as usize]), + RegClass::Vec => { + let _ = write!(out, "xmm{n}"); + } + RegClass::Seg => out.push_str(SEG[(r.num as usize).min(5)]), + // `0x67` shrinks the program counter a displacement is taken from, + // and the width says which one this is. + RegClass::Pc => out.push_str(match r.width { + Width::W32 => "eip", + Width::W16 => "ip", + _ => "rip", + }), + RegClass::Sys => { + let _ = write!(out, "cr{n}"); + } + RegClass::Sp => out.push_str("rsp"), + // The x86 zero register is the phantom SIB index llvm prints when the + // index field is empty but the scale still has to be shown. + RegClass::Zr => out.push_str(match r.width { + Width::W64 => "riz", + _ => "eiz", + }), + RegClass::Flags => out.push_str("eflags"), + } +} + +/// The `dword ptr` style prefix llvm prints when the size is not implied by a +/// register operand. +fn size_hint(size: u64) -> &'static str { + match size { + 1 => "byte ptr ", + 2 => "word ptr ", + 4 => "dword ptr ", + 8 => "qword ptr ", + 10 => "tbyte ptr ", + 16 => "xmmword ptr ", + _ => "", + } +} + +fn mem(out: &mut String, m: Mem, _needs_size: bool) { + // llvm prints the size on every real memory access. A size of zero means + // the operand is an address, not an access, which is `lea`. + out.push_str(size_hint(m.size)); + if let Some(s) = m.seg { + reg(out, s); + out.push(':'); + } + out.push('['); + let mut wrote = false; + // RIP-relative displacements are resolved at decode time, so the number + // shown is the address, not an offset. + if let Some(b) = m.base { + reg(out, b); + wrote = true; + } + if let Some((ix, _, scale)) = m.index { + if wrote { + out.push_str(" + "); + } + // The scale is printed whenever it is not implied: a scale of one is + // silent next to a base register and explicit without one. + if scale > 0 || m.base.is_none() { + let _ = write!(out, "{}*", 1u32 << scale); + } + reg(out, ix); + wrote = true; + } + if m.disp != 0 || !wrote { + if !wrote { + // An absolute address prints signed, the same as a displacement: + // llvm spells the top of the 32-bit space `-0x80000000`. + if m.disp < 0 { + let _ = write!(out, "-{:#x}", m.disp.unsigned_abs()); + } else { + let _ = write!(out, "{:#x}", m.disp); + } + } else if m.disp > 0 { + let _ = write!(out, " + {:#x}", m.disp); + } else { + let _ = write!(out, " - {:#x}", m.disp.unsigned_abs()); + } + } + out.push(']'); +} + +/// Render one operand. `needs_size` is set when no register operand implies it. +fn operand(out: &mut String, op: &Operand, needs_size: bool) { + match op { + Operand::Reg(r) => reg(out, *r), + Operand::Imm(v) => { + if *v < 0 { + let _ = write!(out, "-{:#x}", v.unsigned_abs()); + } else { + let _ = write!(out, "{v:#x}"); + } + } + Operand::UImm(v) => { + let _ = write!(out, "{v:#x}"); + } + Operand::Count(v) => { + let _ = write!(out, "{v:#x}"); + } + Operand::Addr(a) => { + let _ = write!(out, "{:#x}", a.get()); + } + Operand::Mem(m) => mem(out, *m, needs_size), + Operand::Name(n) => out.push_str(n), + Operand::Cond(_) | Operand::Shifted(..) | Operand::Extended(..) | Operand::ShiftOp(..) => {} + Operand::Sys(v) => { + let _ = write!(out, "{v:#x}"); + } + Operand::FpImm(b) => { + let _ = write!(out, "{}", f64::from_bits(*b)); + } + // AArch64 lane operands, which x86 never produces. + Operand::Vector(..) | Operand::VectorLane(..) | Operand::VectorList(..) => {} + } +} + +/// Render a whole instruction as `mnemonic\toperands`. +pub fn format(i: &Insn, _style: Style) -> String { + format_with_prefix(i, i.prefix) +} + +/// Render with a repeat prefix, which llvm prints as its own token followed by +/// two tabs. +pub fn format_with_prefix(i: &Insn, prefix: Option<&str>) -> String { + let mut out = String::with_capacity(32); + if let Some(p) = prefix { + out.push_str(p); + out.push_str("\t\t"); + } + out.push_str(i.mnemonic); + // A memory operand needs an explicit size only when no register operand + // gives it away. + let has_reg = i.operands().iter().any(|o| matches!(o, Operand::Reg(_))); + for (n, op) in i.operands().iter().enumerate() { + out.push_str(if n == 0 { "\t" } else { ", " }); + operand(&mut out, op, !has_reg); + } + out +} diff --git a/crates/e5r-arch/tests/a64_logical_imm.rs b/crates/e5r-arch/tests/a64_logical_imm.rs new file mode 100644 index 0000000..c22b353 --- /dev/null +++ b/crates/e5r-arch/tests/a64_logical_imm.rs @@ -0,0 +1,175 @@ +//! The logical-immediate encoding space, swept and checked against llvm. +//! +//! One instruction group, every encoding of it. `orr Rd, ZR, #imm` is the one +//! that carries an alias decision -- it prints as `mov` only when no `movz` or +//! `movn` could spell the same constant -- and that decision is a three-branch +//! arithmetic predicate over `N`, `immr` and `imms` with no natural test case. +//! The corpus gate could not see it: a compiler emits the constants a program +//! happens to use, and the branch that was wrong covers constants with their +//! zeros in the top halfword, which the fixtures never asked for. +//! +//! So this walks the field space instead of a corpus, which is the only way +//! the count of encodings checked is a property of the architecture rather +//! than of whatever was compiled. `llvm-mc -disassemble` is the oracle. + +use std::io::Write; +use std::process::{Command, Stdio}; + +use e5r_arch::aarch64; +use e5r_core::Addr; + +fn llvm_mc() -> Option<&'static str> { + ["llvm-mc-18", "llvm-mc"] + .into_iter() + .find(|c| Command::new(c).arg("--version").output().is_ok()) +} + +/// Every logical-immediate encoding, for both operations that take one. +/// +/// `sf`, `opc`, `N`, `immr` and `imms` are the whole encoding; `Rn` and `Rd` +/// are held at `ZR` and `x0` because the alias turns on the first and the +/// second names no behaviour. Invalid `(N, imms)` pairs are swept too -- what +/// llvm rejects, this must reject. +fn candidates() -> Vec { + let mut out = Vec::new(); + for sf in [0u32, 1] { + // ORR, which aliases to MOV, and AND, which does not: the second is + // the control, so a disagreement that shows up in both is not about + // the alias. + for opc in [0b01u32, 0b00] { + for n in 0..2u32 { + for immr in 0..64u32 { + for imms in 0..64u32 { + out.push( + (sf << 31) + | (opc << 29) + | (0b100100 << 23) + | (n << 22) + | (immr << 16) + | (imms << 10) + | (31 << 5), + ); + } + } + } + } + } + out +} + +/// What llvm makes of each word, in order. `None` where it decodes nothing. +fn llvm_says(mc: &str, words: &[u32]) -> Vec> { + let mut input = String::new(); + for w in words { + for b in w.to_le_bytes() { + input.push_str(&format!("0x{b:02x} ")); + } + input.push('\n'); + } + let mut child = Command::new(mc) + .args(["-triple=aarch64", "-disassemble"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("llvm-mc did not start"); + child + .stdin + .take() + .unwrap() + .write_all(input.as_bytes()) + .unwrap(); + let out = child.wait_with_output().unwrap(); + let text = String::from_utf8_lossy(&out.stdout); + let err = String::from_utf8_lossy(&out.stderr); + + // One line of output per line of input, except that a word llvm cannot + // decode produces a diagnostic on stderr and nothing on stdout. So the + // undecodable ones are counted off stderr by their line number, and the + // decoded ones consumed in order. + let mut bad = std::collections::BTreeSet::new(); + for line in err.lines() { + if let Some(n) = line.split(':').nth(1).and_then(|n| n.parse::().ok()) { + bad.insert(n - 1); + } + } + let mut decoded = text + .lines() + .map(str::trim) + .filter(|l| !l.is_empty() && !l.starts_with('.')); + (0..words.len()) + .map(|i| { + if bad.contains(&i) { + None + } else { + // `udf` is how llvm-mc spells a word it could not decode, so it + // is a decline here rather than an instruction. + decoded + .next() + .and_then(|l| l.split_whitespace().next()) + .filter(|m| *m != "udf") + .map(str::to_string) + } + }) + .collect() +} + +/// Our mnemonic for a word, which is what the alias decision moves. +fn ours(w: u32) -> Option { + let insn = aarch64::decode(&w.to_le_bytes(), Addr(0x1000))?; + Some(insn.mnemonic.to_string()) +} + +#[test] +fn every_logical_immediate_agrees_with_llvm() { + let Some(mc) = llvm_mc() else { + // Not a silent pass: the harness says what it could not check, and the + // CI job installs llvm precisely so this never prints. + panic!("no llvm-mc on PATH; this gate cannot run and must not report a pass"); + }; + let words = candidates(); + let theirs = llvm_says(mc, &words); + assert_eq!(theirs.len(), words.len()); + + let mut wrong = Vec::new(); + let mut ungated = 0; + for (w, want) in words.iter().zip(&theirs) { + match (ours(*w), want) { + // Both decline. The encoding is not one. + (None, None) => {} + // llvm declines and we do not, or the reverse: a decoder that + // invents an instruction is the worse of the two, and a decoder + // that misses one is a gap. Both are wrong here, because this + // space is small enough to be complete. + (Some(got), None) => wrong.push((*w, got, "".to_string())), + (None, Some(want)) => wrong.push((*w, "".to_string(), want.clone())), + // Mnemonics, which is the whole question here: `mov` or `orr` is + // the alias decision, and the operands of this group are checked + // instruction for instruction by `objdump_parity`. + (Some(got), Some(want)) => { + if got != *want { + wrong.push((*w, got, want.clone())); + } else { + ungated += 1; + } + } + } + } + if !wrong.is_empty() { + let shown: Vec = wrong + .iter() + .take(10) + .map(|(w, got, want)| format!(" {w:#010x} ours {got:<28} llvm {want}")) + .collect(); + panic!( + "{} of {} logical-immediate encodings disagree with llvm-mc:\n{}", + wrong.len(), + words.len(), + shown.join("\n") + ); + } + assert!( + ungated > 4000, + "only {ungated} encodings decoded at all: the sweep is not reaching the group" + ); +} diff --git a/crates/e5r-arch/tests/arm_parity.rs b/crates/e5r-arch/tests/arm_parity.rs new file mode 100644 index 0000000..5fe117a --- /dev/null +++ b/crates/e5r-arch/tests/arm_parity.rs @@ -0,0 +1,396 @@ +//! G3 for ARM: the A32 and T32 decoders must agree with llvm-objdump. +//! +//! The oracle is `llvm-objdump-18 -d` over cross-compiled objects, the same +//! source built twice so the two instruction sets are measured against each +//! other's coverage as well as against the tool. Thumb is walked in address +//! order rather than sampled, because an instruction inside an IT block is +//! conditional and only the instructions before it say so. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_arch::arm::{self, ItState, Mode, Thumb}; +use e5r_core::Addr; + +/// Coverage floor over the corpus, which is at 100% today. What the decoders +/// decline outside it is the Advanced SIMD register file, the parallel +/// arithmetic and saturating packing instructions, and the encodings the +/// architecture calls unpredictable; `arm_parity_report` lists whatever the +/// corpus is currently missing. Raise this as it grows; never lower it. +const MIN_COVERAGE: f64 = 0.999; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// The first llvm-objdump on this machine. Version 18 is what the ARM +/// spellings here were written against. +fn objdump() -> Option<&'static str> { + ["llvm-objdump-18", "llvm-objdump"] + .into_iter() + .find(|&c| Command::new(c).arg("--version").output().is_ok()) +} + +/// One disassembled line, or the start of a function. +enum Line { + Insn { + addr: u64, + bytes: Vec, + text: String, + }, + Label, +} + +/// Parse `llvm-objdump -d`. ARM prints one eight-digit word per instruction +/// and Thumb one or two four-digit halfwords, both already byte-swapped. +fn disassemble(tool: &str, path: &Path) -> Vec { + let Ok(out) = Command::new(tool).arg("-d").arg(path).output() else { + return Vec::new(); + }; + let text = String::from_utf8_lossy(&out.stdout); + let mut lines = Vec::new(); + for l in text.lines() { + if l.ends_with(">:") { + lines.push(Line::Label); + continue; + } + let Some((left, rest)) = l.split_once(": ") else { + continue; + }; + let Ok(addr) = u64::from_str_radix(left.trim(), 16) else { + continue; + }; + let Some((raw, body)) = rest.split_once('\t') else { + continue; + }; + let mut bytes = Vec::new(); + let mut ok = true; + for t in raw.split_whitespace() { + match u32::from_str_radix(t, 16) { + Ok(v) if t.len() == 8 => bytes.extend_from_slice(&v.to_le_bytes()), + Ok(v) if t.len() == 4 => bytes.extend_from_slice(&(v as u16).to_le_bytes()), + _ => ok = false, + } + } + if !ok || bytes.is_empty() { + continue; + } + // Drop the symbolic suffix and the immediate comment llvm appends. + let cut = body.find(" <").into_iter().chain(body.find(" @")).min(); + let body = match cut { + Some(i) => &body[..i], + None => body, + }; + lines.push(Line::Insn { + addr, + bytes, + text: body.trim_end().to_string(), + }); + } + lines +} + +#[derive(Default)] +struct Tally { + matched: usize, + undecoded: usize, + wrong: Vec, + missing: Vec, +} + +impl Tally { + fn merge(&mut self, other: Tally) { + self.matched += other.matched; + self.undecoded += other.undecoded; + self.wrong.extend(other.wrong); + self.missing.extend(other.missing); + } + + fn total(&self) -> usize { + self.matched + self.undecoded + self.wrong.len() + } + + fn coverage(&self) -> f64 { + if self.total() == 0 { + return 1.0; + } + (self.matched + self.wrong.len()) as f64 / self.total() as f64 + } +} + +fn check(tool: &str, path: &Path, mode: Mode) -> Tally { + let mut t = Tally::default(); + let mut thumb = Thumb::new(); + for l in disassemble(tool, path) { + let (addr, bytes, want) = match l { + // An IT block never spans a function, so the state resets here. + Line::Label => { + thumb.resume(ItState::default()); + continue; + } + Line::Insn { addr, bytes, text } => (addr, bytes, text), + }; + if want.is_empty() || want.starts_with("") || want.starts_with(".word") { + continue; + } + let decoded = match mode { + Mode::A32 => arm::decode(&bytes, Addr(addr)), + Mode::T32 => { + let before = thumb.state(); + let i = thumb.decode(&bytes, Addr(addr)); + if i.is_none() { + thumb.resume(before.advance()); + } + i + } + }; + match decoded { + None => { + t.undecoded += 1; + t.missing + .push(want.split('\t').next().unwrap_or("?").to_string()); + } + Some(i) if i.len as usize != bytes.len() => t.wrong.push(format!( + "{addr:x}: {bytes:02x?} length {} != llvm's {}", + i.len, + bytes.len() + )), + Some(i) => { + let got = arm::format(&i); + if got == want { + t.matched += 1; + } else { + t.wrong.push(format!( + "{addr:x}: {bytes:02x?} llvm {want:?} != ours {got:?}" + )); + } + } + } + } + t +} + +/// Every ARM fixture, paired with the instruction set it was built for. +fn fixtures() -> Vec<(PathBuf, Mode)> { + let Some(dir) = corpus() else { + return Vec::new(); + }; + let mut out = Vec::new(); + let Ok(rd) = std::fs::read_dir(dir) else { + return out; + }; + for e in rd.flatten() { + let p = e.path(); + let name = p + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + if name.contains(".arm.") { + out.push((p, Mode::A32)); + } else if name.contains(".thumb.") { + out.push((p, Mode::T32)); + } + } + out.sort_by(|a, b| a.0.cmp(&b.0)); + out +} + +#[test] +fn arm_matches_llvm_objdump() { + let (Some(tool), files) = (objdump(), fixtures()) else { + return; + }; + if files.is_empty() { + return; + } + let mut all = Tally::default(); + let mut named = Vec::new(); + for (p, mode) in &files { + let t = check(tool, p, *mode); + let name = p.file_name().unwrap().to_string_lossy().into_owned(); + named.extend(t.wrong.iter().map(|w| format!("{name}: {w}"))); + all.merge(t); + } + if all.total() < 100 { + return; // nothing useful to compare + } + + // A wrong answer is a bug, and there is no allowance for any. + if !named.is_empty() { + let mut by_kind: BTreeMap = BTreeMap::new(); + for f in &named { + let kind = f + .split("llvm \"") + .nth(1) + .and_then(|s| s.split("\\t").next()) + .and_then(|s| s.split('"').next()) + .unwrap_or("length") + .to_string(); + *by_kind.entry(kind).or_default() += 1; + } + let summary: Vec = by_kind.iter().map(|(k, n)| format!("{k} x{n}")).collect(); + panic!( + "{} of {} ARM instructions decode differently\nby mnemonic: {}\nfirst 20:\n{}", + named.len(), + all.total(), + summary.join(", "), + named + .iter() + .take(20) + .cloned() + .collect::>() + .join("\n") + ); + } + + // A gap is not a bug, but its floor only moves up. + assert!( + all.coverage() >= MIN_COVERAGE, + "decoded {:.3}% of {} ARM instructions, floor is {:.1}% ({} undecoded)", + all.coverage() * 100.0, + all.total(), + MIN_COVERAGE * 100.0, + all.undecoded + ); + println!( + "arm parity: {} instructions, {} matched, {} undecoded, {:.3}% coverage", + all.total(), + all.matched, + all.undecoded, + all.coverage() * 100.0 + ); +} + +#[test] +fn a32_decoding_is_a_pure_function_of_the_word() { + for w in [0xe0813081u32, 0xe12fff1e, 0xea000004, 0xe92d4800] { + assert_eq!( + arm::decode_word(w, Addr(0x1000)), + arm::decode_word(w, Addr(0x1000)) + ); + } +} + +#[test] +fn no_encoding_panics() { + // A wide stride still crosses every top-level group many times, which is + // what a full sweep of the word space would cost too much to do here. + let mut w: u32 = 0; + loop { + let _ = arm::decode_word(w, Addr(0x1000)); + let b = w.to_le_bytes(); + let _ = arm::decode_thumb(&b, Addr(0x1000), ItState::default()); + let Some(next) = w.checked_add(0x3ff1) else { + break; + }; + w = next; + } +} + +#[test] +fn it_blocks_predicate_the_instructions_after_them() { + // `itt mi` then two instructions that print with the `mi` suffix, and a + // third that does not. + let mut t = Thumb::new(); + let bytes = [0x44u8, 0xbf, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20]; + let mut out = Vec::new(); + let mut at = 0u64; + while at < 8 { + let i = t.decode(&bytes[at as usize..], Addr(at)).expect("decode"); + out.push(arm::format(&i)); + at += i.len as u64; + } + assert_eq!( + out, + [ + "itt\tmi", + "movmi\tr0, #0x0", + "movmi\tr0, #0x0", + "movs\tr0, #0x0" + ] + ); +} + +/// A work list rather than a pass or fail: the disagreement and gap classes, +/// biggest first. Ignored by default. +#[test] +#[ignore] +fn arm_parity_report() { + let (Some(tool), files) = (objdump(), fixtures()) else { + return; + }; + let mut wrong: BTreeMap = BTreeMap::new(); + let mut missing: BTreeMap = BTreeMap::new(); + let mut example: BTreeMap = BTreeMap::new(); + let mut all = Tally::default(); + for (p, mode) in &files { + let t = check(tool, p, *mode); + for b in &t.wrong { + let key = b + .split("llvm \"") + .nth(1) + .and_then(|s| s.split("\\t").next()) + .and_then(|s| s.split('"').next()) + .unwrap_or("length") + .to_string(); + *wrong.entry(key.clone()).or_default() += 1; + example.entry(key).or_insert_with(|| b.clone()); + } + for m in &t.missing { + *missing.entry(m.clone()).or_default() += 1; + } + all.merge(t); + } + let mut v: Vec<_> = wrong.into_iter().collect(); + v.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); + println!( + "{} instructions, {} wrong, {} undecoded, {:.3}% coverage", + all.total(), + all.wrong.len(), + all.undecoded, + all.coverage() * 100.0 + ); + for (k, n) in v.iter().take(40) { + println!("{n:>6} {k:<16} {}", example[k]); + } + let mut mv: Vec<_> = missing.into_iter().collect(); + mv.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); + println!("--- undecoded ---"); + for (k, n) in mv.iter().take(40) { + println!("{n:>6} {k}"); + } +} + +/// The barriers, which share their encoding space with the branches. +/// +/// `dsb`, `dmb` and `isb` sit in the rows of the branch-and-miscellaneous +/// space where the field a conditional branch reads as its condition is 1110 +/// or 1111 and is not one. A dispatch that tests the wrong bits sends them to +/// the branch decoder, which rejects them, and every function containing one +/// then stops at it: firmware is full of them and a Cortex-M image lost its +/// whole reset path to this. +#[test] +fn the_thumb_barriers_decode() { + for (bytes, want) in [ + ([0xbfu8, 0xf3, 0x4f, 0x8f], "dsb"), + ([0xbf, 0xf3, 0x5f, 0x8f], "dmb"), + ([0xbf, 0xf3, 0x6f, 0x8f], "isb"), + ] { + let (i, _) = arm::decode_thumb(&bytes, Addr(0x1000), ItState::default()) + .unwrap_or_else(|| panic!("{want} does not decode")); + assert_eq!(i.mnemonic, want); + assert_eq!(i.len, 4); + } + // And a conditional branch in the same space still decodes as one, which + // is the half that would pass if the barriers simply took everything. + let (i, _) = arm::decode_thumb( + &[0x00u8, 0xf0, 0x02, 0x80], + Addr(0x1000), + ItState::default(), + ) + .expect("a wide conditional branch decodes"); + assert!(i.mnemonic.starts_with('b'), "{}", i.mnemonic); +} diff --git a/crates/e5r-arch/tests/arm_sweep.rs b/crates/e5r-arch/tests/arm_sweep.rs new file mode 100644 index 0000000..69f9d4f --- /dev/null +++ b/crates/e5r-arch/tests/arm_sweep.rs @@ -0,0 +1,375 @@ +//! G3 for ARM: the decoder against llvm-objdump over a swept encoding space +//! rather than only over compiled code. +//! +//! A compiler emits a narrow slice of an instruction set. The fixtures this +//! repository builds are C compiled at four optimization levels, so they reach +//! the arithmetic, the loads and the branches and nothing else: no barriers, no +//! system register moves, no DSP instructions, no coprocessor forms. Every one +//! of those is in real code -- firmware is full of the first two -- and a +//! decoder measured only against fixtures says nothing about them. +//! +//! So the corpus here is generated rather than compiled. Both instruction sets +//! are swept structurally, group by group, with the fields that select a form +//! varied inside each: what is covered is the shape of the encoding space, not +//! a sample of one program's instructions. +//! +//! Each candidate sits at the start of its own eight-byte slot padded with +//! `nop`, so a candidate that turns out to be two bytes rather than four still +//! leaves the next one on a boundary and the two disassemblers cannot +//! desynchronize. + +use std::collections::BTreeMap; +use std::process::Command; + +use e5r_arch::arm::{self, ItState, Mode}; +use e5r_core::Addr; + +/// Bytes per candidate: twice the longest instruction either set has. +const SLOT: usize = 8; + +/// The share of what llvm decodes that this decoder also decodes. +/// +/// A ratchet: it only rises. What is left is listed by mnemonic by +/// `arm_sweep_report`, which is the work list. +const MIN_A32: f64 = 0.87; +const MIN_T32: f64 = 0.96; + +fn tool(names: &[&'static str]) -> Option<&'static str> { + names + .iter() + .copied() + .find(|c| Command::new(c).arg("--version").output().is_ok()) +} + +/// Every A32 candidate. +/// +/// The architecture splits the word by bits 27:25 and then by the fields each +/// group uses, so the sweep walks those fields rather than the whole 2^32: +/// every top-level group, every operation within it, and a few values of each +/// register and immediate field, which is what selects between forms. +fn a32_candidates() -> Vec { + let mut out = Vec::new(); + // Condition `al` for most, plus one conditional pass so the condition + // field is exercised without multiplying the whole sweep by sixteen. + for cond in [0xeu32, 0x0, 0xb] { + let c = cond << 28; + // Data processing, immediate and register forms, with and without S. + for op in 0..16u32 { + for s in 0..2u32 { + for imm in [0u32, 1] { + for rn in [0u32, 5, 13, 15] { + for rd in [0u32, 5, 15] { + out.push( + c | imm << 25 | op << 21 | s << 20 | rn << 16 | rd << 12 | 0x12, + ); + } + } + } + } + } + // Loads and stores, every P/U/B/W/L combination. + for bits in 0..64u32 { + for rn in [0u32, 13, 15] { + out.push(c | 0x4 << 24 | bits << 20 | rn << 16 | 0x3004); + out.push(c | 0x6 << 24 | bits << 20 | rn << 16 | 0x3004); + } + } + // Load and store multiple. + for bits in 0..32u32 { + out.push(c | 0x8 << 24 | bits << 20 | 13 << 16 | 0x4030); + } + // Multiplies and the synchronization primitives, which share a space. + for op in 0..32u32 { + out.push(c | op << 20 | 0x9 << 4 | 0x0302_0001); + } + // The halfword and signed loads, and the extra load/store space. + for op in 0..32u32 { + for kind in [0xbu32, 0xd, 0xf] { + out.push(c | op << 20 | kind << 4 | 0x0030_0001); + } + } + // Coprocessor: `cdp`, `mcr`/`mrc`, `stc`/`ldc`, which firmware and + // anything with a floating point unit both use. + for op in 0..16u32 { + out.push(c | 0xe << 24 | op << 20 | 0x3105); + out.push(c | 0xe << 24 | op << 20 | 0x3115); + out.push(c | 0xc << 24 | op << 20 | 0x3105); + out.push(c | 0xd << 24 | op << 20 | 0x3105); + } + // The miscellaneous space: `bx`, `clz`, `msr`, `mrs`, the saturating + // arithmetic and the breakpoints. + for op in 0..16u32 { + for kind in 0..8u32 { + out.push(c | 0x1 << 24 | op << 20 | kind << 4 | 0x0f0f); + } + } + // Media: the packing, saturating, reversing and DSP forms. + for op1 in 0..32u32 { + for op2 in 0..8u32 { + out.push(c | 0x3 << 25 | op1 << 20 | op2 << 5 | 0x1 << 4 | 0x0203_0f04); + } + } + } + // The unconditional space, which is a second instruction space and not a + // condition: barriers, `pli`, `blx` and the SIMD loads. + for op in 0..64u32 { + out.push(0xf000_0000 | op << 20 | 0x0f00_0050); + out.push(0xf000_0000 | op << 20 | 0x0507_f000); + } + out.sort_unstable(); + out.dedup(); + out +} + +/// Every T32 candidate, as one or two halfwords. +fn t32_candidates() -> Vec> { + let mut out: Vec> = Vec::new(); + // The sixteen-bit space, swept whole: it is only 65,536 encodings and it + // is where the common instructions live. + for w in (0u32..=0xffff).step_by(7) { + out.push((w as u16).to_le_bytes().to_vec()); + } + // The thirty-two bit space, by group. The first halfword's bits 12:11 + // select the group and 10:4 the operation within it. + for hi_op in 0..128u32 { + for group in [0b01u32, 0b10, 0b11] { + for rn in [0u32, 3, 13, 15] { + let hw1 = 0xe000 | group << 11 | hi_op << 4 | rn; + for lo in [0x0000u32, 0x8000, 0xf000, 0x3104, 0x2f01] { + let mut b = (hw1 as u16).to_le_bytes().to_vec(); + b.extend_from_slice(&(lo as u16).to_le_bytes()); + out.push(b); + } + } + } + } + // The system and barrier rows specifically, which the stride above can + // step over: these are what firmware is made of. + for op in 0..16u32 { + for rn in [0u32, 3, 8, 15] { + let hw1 = 0xf380 | rn; + let hw2 = 0x8800 | op << 4 | op; + let mut b = (hw1 as u16).to_le_bytes().to_vec(); + b.extend_from_slice(&(hw2 as u16).to_le_bytes()); + out.push(b); + let mut b = 0xf3bfu16.to_le_bytes().to_vec(); + b.extend_from_slice(&((0x8f00 | op << 4 | op) as u16).to_le_bytes()); + out.push(b); + } + } + // The DSP and coprocessor rows, for the same reason. + for op in 0..64u32 { + for op2 in 0..8u32 { + let mut b = ((0xfa00 | op << 4 | 2) as u16).to_le_bytes().to_vec(); + b.extend_from_slice(&((0xf000 | op2 << 4 | 12) as u16).to_le_bytes()); + out.push(b); + } + let mut b = ((0xec00 | op) as u16).to_le_bytes().to_vec(); + b.extend_from_slice(&0x3105u16.to_le_bytes()); + out.push(b); + let mut b = ((0xee00 | op) as u16).to_le_bytes().to_vec(); + b.extend_from_slice(&0x3115u16.to_le_bytes()); + out.push(b); + } + out.sort(); + out.dedup(); + out +} + +/// What llvm makes of each slot, by slot index. +/// +/// `None` where llvm itself declines, which is most of a swept space and is +/// not a gap in this decoder. +fn oracle(bytes: &[u8], thumb: bool) -> Option> { + let cc = tool(&["clang-18", "clang"])?; + let objdump = tool(&["llvm-objdump-18", "llvm-objdump"])?; + let dir = std::env::temp_dir().join(format!( + "e5r-armsweep-{}-{}", + if thumb { "t32" } else { "a32" }, + std::process::id() + )); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("s.s"); + let obj = dir.join("s.o"); + let mut text = String::from(if thumb { + ".text\n.thumb\n.syntax unified\n" + } else { + ".text\n.arm\n" + }); + for chunk in bytes.chunks(16) { + text.push_str(".byte "); + for (n, b) in chunk.iter().enumerate() { + if n > 0 { + text.push(','); + } + text.push_str(&format!("{b:#04x}")); + } + text.push('\n'); + } + std::fs::write(&src, text).ok()?; + let built = Command::new(cc) + .args([ + "--target=arm-unknown-linux-gnueabi", + "-march=armv7-a", + "-c", + "-o", + ]) + .arg(&obj) + .arg(&src) + .output() + .ok()?; + if !built.status.success() { + return None; + } + let triple = if thumb { "thumbv7em" } else { "armv7" }; + let out = Command::new(objdump) + .args(["-d", &format!("--triple={triple}")]) + .arg(&obj) + .output() + .ok()?; + let listing = String::from_utf8_lossy(&out.stdout); + let mut by_slot = BTreeMap::new(); + for line in listing.lines() { + let Some((head, rest)) = line.split_once(':') else { + continue; + }; + let Ok(addr) = usize::from_str_radix(head.trim(), 16) else { + continue; + }; + // Only what starts a slot is a candidate; the rest is padding, or the + // tail of a candidate llvm read as longer than this decoder did. + if addr % SLOT != 0 { + continue; + } + let Some((_, asm)) = rest.split_once('\t') else { + continue; + }; + let asm = asm.trim(); + if asm.is_empty() || asm.starts_with("") || asm.starts_with(".word") { + continue; + } + by_slot.insert(addr / SLOT, asm.to_string()); + } + let _ = std::fs::remove_dir_all(&dir); + Some(by_slot) +} + +/// Lay candidates out one per slot, padded with `nop`. +fn image(candidates: &[Vec], thumb: bool) -> Vec { + // `nop` is `mov r0, r0` in A32 and `bf00` in Thumb; either is one + // instruction that changes nothing and keeps the padding readable. + let pad: [u8; 4] = if thumb { + [0x00, 0xbf, 0x00, 0xbf] + } else { + [0x00, 0xf0, 0x20, 0xe3] + }; + let mut out = Vec::with_capacity(candidates.len() * SLOT); + for c in candidates { + out.extend_from_slice(c); + while out.len() % SLOT != 0 { + let at = out.len() % 4; + out.push(pad[at]); + } + } + out +} + +/// Compare the two decoders over a swept space. +/// +/// Returns how many llvm decoded, how many this decoder also decoded, and what +/// it declined by mnemonic. +fn measure( + candidates: Vec>, + thumb: bool, +) -> Option<(usize, usize, BTreeMap)> { + let bytes = image(&candidates, thumb); + let want = oracle(&bytes, thumb)?; + let mut ours = 0usize; + let mut missing: BTreeMap = BTreeMap::new(); + for (slot, asm) in &want { + let at = slot * SLOT; + let window = &bytes[at..(at + SLOT).min(bytes.len())]; + let decoded = if thumb { + arm::decode_in(Mode::T32, window, Addr(at as u64), ItState::default()).map(|(i, _)| i) + } else { + arm::decode(window, Addr(at as u64)) + }; + match decoded { + Some(_) => ours += 1, + None => { + let m = asm.split_whitespace().next().unwrap_or("?").to_string(); + *missing.entry(m).or_default() += 1; + } + } + } + Some((want.len(), ours, missing)) +} + +fn check(thumb: bool, floor: f64, what: &str) { + let candidates = if thumb { + t32_candidates() + } else { + a32_candidates() + .into_iter() + .map(|w| w.to_le_bytes().to_vec()) + .collect() + }; + let Some((total, ours, missing)) = measure(candidates, thumb) else { + return; // no assembler or no objdump here + }; + assert!(total > 2_000, "{what}: only {total} encodings to measure"); + let rate = ours as f64 / total as f64; + let mut worst: Vec<_> = missing.iter().collect(); + worst.sort_by_key(|(_, n)| std::cmp::Reverse(**n)); + let summary: Vec = worst + .iter() + .take(12) + .map(|(m, n)| format!("{m} x{n}")) + .collect(); + assert!( + rate >= floor, + "{what}: decoded {ours} of {total} ({:.2}%), floor {:.2}%\nbiggest gaps: {}", + rate * 100.0, + floor * 100.0, + summary.join(", ") + ); + eprintln!( + "{what} sweep: {ours} of {total} llvm-decoded encodings, {:.2}%", + rate * 100.0 + ); +} + +#[test] +fn a32_decodes_what_llvm_decodes() { + check(false, MIN_A32, "A32"); +} + +#[test] +fn t32_decodes_what_llvm_decodes() { + check(true, MIN_T32, "T32"); +} + +/// A work list, not a gate. Ignored by default. +#[test] +#[ignore = "a report of what the sweep declines, for choosing what to add next"] +fn arm_sweep_report() { + for (thumb, what) in [(false, "A32"), (true, "T32")] { + let candidates = if thumb { + t32_candidates() + } else { + a32_candidates() + .into_iter() + .map(|w| w.to_le_bytes().to_vec()) + .collect() + }; + let Some((total, ours, missing)) = measure(candidates, thumb) else { + return; + }; + eprintln!("--- {what} --- {ours} of {total}"); + let mut worst: Vec<_> = missing.iter().collect(); + worst.sort_by_key(|(_, n)| std::cmp::Reverse(**n)); + for (m, n) in worst.iter().take(30) { + eprintln!(" {n:6} {m}"); + } + } +} diff --git a/crates/e5r-arch/tests/fuzz.rs b/crates/e5r-arch/tests/fuzz.rs new file mode 100644 index 0000000..d40da40 --- /dev/null +++ b/crates/e5r-arch/tests/fuzz.rs @@ -0,0 +1,117 @@ +//! Mutation fuzzing of the decoders. +//! +//! A decoder's contract is narrower than a loader's but no less absolute: given +//! any bytes it returns an instruction or nothing, never a panic, and the +//! length it reports is never longer than the bytes it was given. + +use std::time::{Duration, Instant}; + +use e5r_arch::{aarch64, x86}; +use e5r_core::Addr; + +const BUDGET: Duration = Duration::from_millis(1200); + +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545_f491_4f6c_dd1d) + } +} + +#[test] +fn aarch64_decodes_anything_without_panicking() { + let mut rng = Rng(0xa64_0001); + let start = Instant::now(); + let mut n = 0u64; + while start.elapsed() < BUDGET { + let w = rng.next() as u32; + if let Some(i) = aarch64::decode_word(w, Addr(0x40_0000)) { + // Fixed-width architecture: anything else is a decoder bug. + assert_eq!(i.len, 4, "{w:08x} decoded as {} bytes", i.len); + // Formatting must not panic either; it is the other half of the + // decoder's surface. + let _ = aarch64::format(&i, aarch64::Style { objdump: true }); + } + n += 1; + } + assert!(n > 10_000, "only {n} words in {BUDGET:?}"); + println!("aarch64 fuzz: {n} words, no panics"); +} + +#[test] +fn x86_decodes_anything_without_panicking() { + let mut rng = Rng(0x86_0002); + let start = Instant::now(); + let mut n = 0u64; + let mut buf = [0u8; 16]; + while start.elapsed() < BUDGET { + for chunk in buf.chunks_mut(8) { + chunk.copy_from_slice(&rng.next().to_le_bytes()[..chunk.len()]); + } + // Every prefix length, so the walk through the prefix loop is covered. + let len = 1 + (rng.next() % 16) as usize; + if let Some(i) = x86::decode(&buf[..len], Addr(0x40_0000)) { + assert!( + i.len as usize <= len, + "{:02x?} decoded {} bytes from {len}", + &buf[..len], + i.len + ); + assert!( + i.len > 0 && i.len <= 15, + "{:02x?} length {}", + &buf[..len], + i.len + ); + let _ = x86::format(&i, x86::Style::default()); + } + n += 1; + } + assert!(n > 10_000, "only {n} cases in {BUDGET:?}"); + println!("x86 fuzz: {n} cases, no panics"); +} + +#[test] +fn x86_prefix_storms_terminate() { + // A run of prefixes with no opcode is the shape that makes a naive prefix + // loop run off the end or loop forever. + for prefix in [0x66u8, 0x67, 0xf0, 0xf2, 0xf3, 0x2e, 0x64, 0x40, 0x48] { + for len in 1..64usize { + let buf = vec![prefix; len]; + let started = Instant::now(); + let _ = x86::decode(&buf, Addr(0x1000)); + assert!( + started.elapsed() < Duration::from_millis(50), + "{len} copies of {prefix:#x} took too long" + ); + } + } +} + +#[test] +fn every_aarch64_top_level_group_is_reached() { + // A stride that crosses each of the sixteen top-level groups many times, + // so a change that accidentally drops one shows up here. + let mut seen = [false; 16]; + let mut w: u32 = 0; + loop { + if aarch64::decode_word(w, Addr(0x1000)).is_some() { + seen[((w >> 25) & 0xf) as usize] = true; + } + let Some(next) = w.checked_add(0x1_0001) else { + break; + }; + w = next; + } + let reached = seen.iter().filter(|x| **x).count(); + assert!( + reached >= 8, + "only {reached} of 16 top-level groups decode anything" + ); +} diff --git a/crates/e5r-arch/tests/objdump_parity.rs b/crates/e5r-arch/tests/objdump_parity.rs new file mode 100644 index 0000000..b8ad741 --- /dev/null +++ b/crates/e5r-arch/tests/objdump_parity.rs @@ -0,0 +1,467 @@ +//! G3: the AArch64 decoder must agree with objdump, instruction for +//! instruction, over the whole fixture corpus. +//! +//! The oracle is `objdump -d`, so the expectations are not our own decoder's +//! opinion. A disagreement is either a bug here or a deliberate divergence with +//! a written reason in `DIVERGENCES`; there is no third category. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_arch::aarch64; +use e5r_core::Addr; + +/// Deliberate divergences from objdump, each with its reason. +/// +/// Relocated branches are the only one. In a relocatable object a `bl` whose +/// encoded offset is zero carries an R_AARCH64_CALL26 relocation, and objdump +/// prints the relocated target while we print what the bytes say. Applying +/// relocations is the loader's job, not the decoder's, so the decoder is right +/// and the comparison is unfair rather than failing. +#[allow(dead_code)] +const DIVERGENCES: &[(&str, &str)] = &[( + "relocated branch", + "objdump resolves R_AARCH64_CALL26; the decoder reports the encoded offset", +)]; + +/// True when this is the relocated-branch case described above. +fn is_relocated_branch(path: &Path, insn: &e5r_arch::Insn) -> bool { + let object_file = path.extension().is_some_and(|e| e == "o"); + object_file && insn.flow.target() == Some(insn.addr) +} + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// One objdump line: address, encoded word, and the text after the encoding. +struct Line { + addr: u64, + word: u32, + text: String, +} + +/// The binutils that knows AArch64. +/// +/// Prefixed name first: `objdump` is built for its own host's targets, so on +/// an x86-64 machine the plain one reads an AArch64 file and prints nothing. +/// That is not a missing oracle, it is an oracle that answers "no +/// instructions" to every question -- which this file then reported as an +/// empty corpus. +fn binutils(name: &str) -> String { + let prefixed = format!("aarch64-linux-gnu-{name}"); + if Command::new(&prefixed).arg("--version").output().is_ok() { + prefixed + } else { + name.to_string() + } +} + +/// Parse `objdump -d` output, keeping only fully decoded 4-byte instructions. +fn objdump(path: &Path) -> Vec { + let out = Command::new(binutils("objdump")) + .args(["-d", "--show-raw-insn"]) + .arg(path) + .output() + .expect("objdump"); + let text = String::from_utf8_lossy(&out.stdout); + let mut lines = Vec::new(); + for l in text.lines() { + // " 4006e8:\t910043ff \tadd\tsp, sp, #0x10" + let Some((left, rest)) = l.split_once(":\t") else { + continue; + }; + let Ok(addr) = u64::from_str_radix(left.trim(), 16) else { + continue; + }; + let mut parts = rest.splitn(2, '\t'); + let Some(hex) = parts.next() else { continue }; + let hex = hex.trim(); + if hex.len() != 8 { + continue; + } + let Ok(word) = u32::from_str_radix(hex, 16) else { + continue; + }; + let Some(body) = parts.next() else { + // No text at all: objdump could not decode it either. + continue; + }; + // Drop the symbolic suffix and objdump's alias comments. + let body = body.split("//").next().unwrap_or(body); + let body = match body.find(" <") { + Some(i) => &body[..i], + None => body, + }; + lines.push(Line { + addr, + word, + text: unprefixed(body.trim_end()), + }); + } + lines +} + +/// Strip the `0x` objdump puts on a branch target it has no symbol for. +/// +/// With a symbol it prints `b.lt 4002a4 `, and without one +/// `b.lt 0x11080`, so the same instruction is spelled two ways depending on +/// whether the binary was stripped. Only the last operand, and only when the +/// whole of it is an address. +fn unprefixed(text: &str) -> String { + let cut = text.rfind(['\t', ' ']).map(|i| i + 1).unwrap_or(0); + let last = &text[cut..]; + match last.strip_prefix("0x") { + Some(hex) if !hex.is_empty() && hex.bytes().all(|b| b.is_ascii_hexdigit()) => { + format!("{}{hex}", &text[..cut]) + } + _ => text.to_string(), + } +} + +/// Coverage floor over the fixtures, which are built from sources in this +/// repository and are the same instructions on any machine. What remains is +/// the single-structure SIMD loads and stores, the by-element multiplies, the +/// memory-tagging instructions, and the Scalable Vector Extension, which is a +/// separate architecture's worth of encodings. Raise this as the gap closes; +/// never lower it. +/// +/// Held over the fixtures alone on purpose. Counting this machine's libc, +/// libstdc++ and libcrypto in the same fraction makes the number a fact about +/// which libraries are installed: they are hundreds of thousands of ordinary +/// instructions that dilute every gap, and a runner without them measured +/// 99.67% where this machine measured 99.87% with nothing different about the +/// decoder. They are still compared -- a wrong answer in libc is a wrong +/// answer -- and their coverage is reported beside the gated one. +const MIN_COVERAGE: f64 = 0.996; + +/// What comparing one file found. +#[derive(Clone, Default)] +struct Tally { + /// Decoded, and the text matches objdump. + matched: usize, + /// Not decoded at all. A gap, not a wrong answer. + undecoded: usize, + /// Decoded differently from objdump. These are bugs. + wrong: Vec, + /// Mnemonics objdump decoded and we did not. + missing: Vec, +} + +impl Tally { + fn merge(&mut self, other: Tally) { + self.matched += other.matched; + self.undecoded += other.undecoded; + self.wrong.extend(other.wrong); + self.missing.extend(other.missing); + } + + fn total(&self) -> usize { + self.matched + self.undecoded + self.wrong.len() + } + + fn coverage(&self) -> f64 { + if self.total() == 0 { + return 1.0; + } + (self.matched + self.wrong.len()) as f64 / self.total() as f64 + } +} + +/// Compare every instruction in one file. +fn check(path: &Path) -> Tally { + let mut t = Tally::default(); + for l in objdump(path) { + // objdump spells undefined encodings in ways we do not try to match. + if l.text.starts_with(".inst") || l.text.starts_with(".word") || l.text.is_empty() { + continue; + } + let want = l.text.as_str(); + let Some(insn) = aarch64::decode_word(l.word, Addr(l.addr)) else { + t.undecoded += 1; + t.missing + .push(l.text.split('\t').next().unwrap_or("?").to_string()); + continue; + }; + let got = aarch64::format(&insn, aarch64::Style { objdump: true }); + if got == want || is_relocated_branch(path, &insn) { + t.matched += 1; + } else { + t.wrong.push(format!( + "{:x}: {:08x} objdump {:?} != ours {:?}", + l.addr, l.word, want, got + )); + } + } + t +} + +/// System binaries, which cost nothing to add and are tens of thousands of +/// instructions of real compiler output apiece. +fn system_binaries() -> Vec { + let mut out = Vec::new(); + for p in [ + "/bin/ls", + "/bin/bash", + "/usr/bin/objdump", + "/lib/aarch64-linux-gnu/libc.so.6", + "/usr/lib/aarch64-linux-gnu/libc.so.6", + "/usr/lib/aarch64-linux-gnu/libstdc++.so.6", + "/usr/lib/aarch64-linux-gnu/libcrypto.so.3", + ] { + let p = PathBuf::from(p); + if p.is_file() { + out.push(p); + } + } + out +} + +#[test] +fn aarch64_matches_objdump_on_every_fixture() { + let Some(dir) = corpus() else { return }; + let mut all = Tally::default(); + let mut ours = Tally::default(); + let mut files = 0; + + let built: Vec = std::fs::read_dir(&dir) + .unwrap() + .filter_map(|e| { + let p = e.ok()?.path(); + let n = p.file_name()?.to_string_lossy().into_owned(); + (n.contains("a64") || n.contains("hello")).then_some(p) + }) + .collect(); + let from_here = built.len(); + + let mut named: Vec = Vec::new(); + for (i, p) in built.into_iter().chain(system_binaries()).enumerate() { + let name = p.file_name().unwrap().to_string_lossy().into_owned(); + let t = check(&p); + if t.total() == 0 { + continue; + } + files += 1; + named.extend(t.wrong.iter().map(|b| format!("{name}: {b}"))); + if i < from_here { + ours.merge(t.clone()); + } + all.merge(t); + } + + assert!(files > 0, "no aarch64 fixtures found in {}", dir.display()); + assert!( + all.total() > 100, + "only {} instructions compared", + all.total() + ); + + // A wrong answer is a bug, and there is no allowance for any. + if !named.is_empty() { + let mut by_kind: BTreeMap = BTreeMap::new(); + for f in &named { + let kind = f + .split("objdump \"") + .nth(1) + .and_then(|s| s.split(['\t', '"']).next()) + .unwrap_or("?") + .to_string(); + *by_kind.entry(kind).or_default() += 1; + } + let summary: Vec = by_kind.iter().map(|(k, n)| format!("{k} x{n}")).collect(); + panic!( + "{} of {} instructions decode differently from objdump\nby mnemonic: {}\nfirst 20:\n{}", + named.len(), + all.total(), + summary.join(", "), + named + .iter() + .take(20) + .cloned() + .collect::>() + .join("\n") + ); + } + + // A gap is not a bug, but its floor only moves up -- and it is held over + // the half of the corpus this repository builds. + assert!( + ours.total() > 100, + "only {} instructions came from fixtures", + ours.total() + ); + assert!( + ours.coverage() >= MIN_COVERAGE, + "decoded {:.3}% of {} fixture instructions, floor is {:.1}% ({} undecoded)", + ours.coverage() * 100.0, + ours.total(), + MIN_COVERAGE * 100.0, + ours.undecoded + ); + println!( + "objdump parity, fixtures: {} instructions, {} matched, {} undecoded, \ + {:.3}% coverage", + ours.total(), + ours.matched, + ours.undecoded, + ours.coverage() * 100.0 + ); + println!( + "objdump parity, with this machine's libraries: {} instructions, \ + {} matched, {} undecoded, {:.3}% coverage", + all.total(), + all.matched, + all.undecoded, + all.coverage() * 100.0 + ); +} + +#[test] +fn decoding_is_a_pure_function_of_the_word() { + // Same word, same address, same answer, every time. + for w in [0x910043ffu32, 0xd65f03c0, 0x14000008, 0xb9400000] { + let a = aarch64::decode_word(w, Addr(0x1000)); + let b = aarch64::decode_word(w, Addr(0x1000)); + assert_eq!(a, b); + } +} + +#[test] +fn no_encoding_panics() { + // The whole 32-bit space is too slow for a unit test; a wide stride over it + // still crosses every top-level group many times. + let mut w: u32 = 0; + loop { + let _ = aarch64::decode_word(w, Addr(0x1000)); + let Some(next) = w.checked_add(0x3ff1) else { + break; + }; + w = next; + } +} + +/// A work list rather than a pass/fail: prints the disagreement classes, +/// biggest first, so the next decoder gap is obvious. Ignored by default. +#[test] +#[ignore] +fn parity_report() { + let Some(dir) = corpus() else { return }; + let mut by_mnemonic: BTreeMap = BTreeMap::new(); + let mut examples: BTreeMap = BTreeMap::new(); + let files = std::fs::read_dir(&dir) + .unwrap() + .filter_map(|e| { + let p = e.ok()?.path(); + let n = p.file_name()?.to_string_lossy().into_owned(); + (n.contains("a64") || n.contains("hello")).then_some(p) + }) + .chain(system_binaries()); + for p in files { + for b in check(&p).wrong { + // "addr: word objdump "mnem\t..." != ours "..."" or "we decoded nothing" + let key = b + .split("objdump ") + .nth(1) + .and_then(|s| s.strip_prefix('"')) + .and_then(|s| s.split(['\t', '"']).next()) + .unwrap_or("?") + .to_string(); + *by_mnemonic.entry(key.clone()).or_default() += 1; + examples.entry(key).or_insert(b); + } + } + let mut v: Vec<_> = by_mnemonic.into_iter().collect(); + v.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); + for (k, n) in v.iter().take(40) { + println!("{n:>7} {k:<12} {}", examples[k]); + } + let mut missing: BTreeMap = BTreeMap::new(); + for p in std::fs::read_dir(&dir) + .unwrap() + .filter_map(|e| { + let p = e.ok()?.path(); + let n = p.file_name()?.to_string_lossy().into_owned(); + (n.contains("a64") || n.contains("hello")).then_some(p) + }) + .chain(system_binaries()) + { + for m in check(&p).missing { + *missing.entry(m).or_default() += 1; + } + } + let mut mv: Vec<_> = missing.into_iter().collect(); + mv.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); + println!("--- undecoded ---"); + for (k, n) in mv.iter().take(40) { + println!("{n:>7} {k}"); + } +} + +/// The system register table says the same thing the assembler and the +/// disassembler do, over every encoding an `MRS` can name. +/// +/// The table is data, and data written by hand goes wrong quietly: the +/// encodings for `midr_el1`, `cntfrq_el0` and six others were wrong for as +/// long as no fixture happened to read them. This checks all 32768 of them. +#[test] +fn sysreg_names_match_binutils() { + let dir = std::env::temp_dir().join(format!("e5r-sysreg-{}", std::process::id())); + if std::fs::create_dir_all(&dir).is_err() { + return; + } + let mut asm = String::new(); + let mut encodings = Vec::new(); + for op0 in 2..4u32 { + for op1 in 0..8u32 { + for crn in 0..16u32 { + for crm in 0..16u32 { + for op2 in 0..8u32 { + asm.push_str(&format!("mrs x0, s{op0}_{op1}_c{crn}_c{crm}_{op2}\n")); + encodings.push(op0 << 14 | op1 << 11 | crn << 7 | crm << 3 | op2); + } + } + } + } + } + let src = dir.join("sys.s"); + let obj = dir.join("sys.o"); + if std::fs::write(&src, &asm).is_err() { + return; + } + let assembled = Command::new(binutils("as")) + .arg("-o") + .arg(&obj) + .arg(&src) + .status(); + if !matches!(assembled, Ok(s) if s.success()) { + return; // no aarch64 assembler here + } + let lines = objdump(&obj); + assert_eq!(lines.len(), encodings.len(), "objdump lost instructions"); + + let mut wrong = Vec::new(); + for (line, enc) in lines.iter().zip(&encodings) { + let theirs = line.text.rsplit("x0, ").next().unwrap_or("").trim(); + let ours = aarch64::sysreg_name(*enc); + // A name binutils does not have prints as the generic spelling, which + // is what it prints too. + if ours != theirs { + wrong.push(format!("{enc:#06x}: objdump {theirs:?} != ours {ours:?}")); + } + } + let _ = std::fs::remove_dir_all(&dir); + assert!( + wrong.is_empty(), + "{} of {} system register names disagree\nfirst 20:\n{}", + wrong.len(), + encodings.len(), + wrong + .iter() + .take(20) + .cloned() + .collect::>() + .join("\n") + ); +} diff --git a/crates/e5r-arch/tests/x86_32_parity.rs b/crates/e5r-arch/tests/x86_32_parity.rs new file mode 100644 index 0000000..2465c84 --- /dev/null +++ b/crates/e5r-arch/tests/x86_32_parity.rs @@ -0,0 +1,655 @@ +//! G3 for i386: the 32-bit decoder must agree with llvm-objdump in Intel +//! syntax, over a swept encoding space rather than only over compiled code. +//! +//! A compiler emits a few hundred distinct encodings and never reaches the +//! corners where 32-bit mode differs from long mode, which is exactly where a +//! 64-bit decoder retargeted at 32 bits goes wrong. So the corpus here is +//! generated: every opcode crossed with every ModRM form and every prefix +//! combination that changes the answer, assembled with `llvm-mc` and read back +//! with `llvm-objdump`. Each candidate sits at the start of its own sixteen +//! byte slot padded with `nop`, so an instruction that turns out longer than +//! the bytes that seeded it still ends inside its slot and the next candidate +//! still starts on a boundary. Fifteen bytes is the architecture's own limit, +//! which is what makes sixteen enough. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_arch::x86; +use e5r_core::Addr; + +/// Coverage floor over the swept corpus. The sweep measures 99.71%, and what +/// it declines is listed by mnemonic by `x86_32_parity_report`: none of it is +/// a 32-bit form, it is the extensions the shared table has never held. Raise +/// this as those fill in; it is a ratchet and never goes down. +const MIN_SWEEP_COVERAGE: f64 = 0.995; + +/// Coverage floor over the compiled i386 fixtures, which contain only what a +/// compiler emits and so should be near total. +const MIN_FIXTURE_COVERAGE: f64 = 0.999; + +/// Bytes per candidate. One more than the longest instruction the +/// architecture allows, so a candidate can never run into the next one. +const SLOT: usize = 16; + +fn tool(names: &[&'static str]) -> Option<&'static str> { + names + .iter() + .copied() + .find(|c| Command::new(c).arg("--version").output().is_ok()) +} + +fn objdump() -> Option<&'static str> { + tool(&["llvm-objdump-18", "llvm-objdump-15", "llvm-objdump"]) +} + +fn assembler() -> Option<&'static str> { + tool(&["llvm-mc-18", "llvm-mc-15", "llvm-mc"]) +} + +struct Line { + addr: u64, + bytes: Vec, + text: String, +} + +/// Parse `llvm-objdump -d --x86-asm-syntax=intel`. +fn parse(text: &str) -> Vec { + let mut lines = Vec::new(); + for l in text.lines() { + let Some((left, rest)) = l.split_once(": ") else { + continue; + }; + let Ok(addr) = u64::from_str_radix(left.trim(), 16) else { + continue; + }; + let Some((raw, body)) = rest.split_once('\t') else { + continue; + }; + let bytes: Option> = raw + .split_whitespace() + .map(|h| u8::from_str_radix(h, 16).ok()) + .collect(); + let Some(bytes) = bytes else { continue }; + if bytes.is_empty() { + continue; + } + // Drop the symbolic suffix llvm appends to branch targets. + let body = match body.find(" #").or_else(|| body.find(" <")) { + Some(i) => &body[..i], + None => body, + }; + lines.push(Line { + addr, + bytes, + text: body.trim_end().to_string(), + }); + } + lines +} + +fn disassemble_file(tool: &str, path: &Path) -> Vec { + let out = Command::new(tool) + .args(["-d", "--x86-asm-syntax=intel"]) + .arg(path) + .output(); + let Ok(out) = out else { return Vec::new() }; + parse(&String::from_utf8_lossy(&out.stdout)) +} + +/// Assemble `slots` into an i386 object and disassemble it, returning one +/// [`Line`] per slot start. The object goes through a temporary file because +/// llvm-objdump reads a file, not a pipe. +fn oracle(mc: &str, dump: &str, slots: &[[u8; SLOT]]) -> Option> { + let dir = std::env::temp_dir().join(format!("e5r-x32-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("sweep.s"); + let obj = dir.join("sweep.o"); + + let mut text = String::with_capacity(slots.len() * 96); + text.push_str(".text\n"); + for s in slots { + text.push_str(".byte "); + for (n, b) in s.iter().enumerate() { + if n > 0 { + text.push(','); + } + text.push_str(&format!("0x{b:02x}")); + } + text.push('\n'); + } + std::fs::write(&src, text).ok()?; + + let st = Command::new(mc) + .args(["--triple=i386", "--filetype=obj", "-o"]) + .arg(&obj) + .arg(&src) + .output() + .ok()?; + if !st.status.success() { + return None; + } + let lines = disassemble_file(dump, &obj); + let _ = std::fs::remove_file(&src); + let _ = std::fs::remove_file(&obj); + // Only the line that starts a slot is a candidate; the rest is padding. + Some( + lines + .into_iter() + .filter(|l| l.addr as usize % SLOT == 0) + .collect(), + ) +} + +/// A candidate, padded out to a full slot with `nop`. +fn slot(bytes: &[u8]) -> [u8; SLOT] { + let mut s = [0x90u8; SLOT]; + let n = bytes.len().min(SLOT); + s[..n].copy_from_slice(&bytes[..n]); + s +} + +/// A deterministic generator, so a failure reproduces exactly. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + // SplitMix64, which needs no state beyond the counter. + self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z ^ (z >> 31) + } +} + +/// The prefix strings worth crossing every opcode with. Each changes the +/// answer in 32-bit mode: operand size, address size, a mandatory prefix, a +/// segment override, or a lock. +const PREFIXES: [&[u8]; 8] = [ + &[], + &[0x66], + &[0x67], + &[0xf2], + &[0xf3], + &[0x64], + &[0x66, 0x67], + &[0xf0], +]; + +/// ModRM bytes that reach every addressing form: each mod, the SIB escape, +/// the absolute form, and a register. +const MODRM: [u8; 12] = [ + 0x00, 0x01, 0x04, 0x05, 0x06, 0x40, 0x44, 0x47, 0x80, 0x84, 0xc0, 0xd1, +]; + +/// Every candidate the sweep tries. +fn candidates() -> Vec<[u8; SLOT]> { + let mut out: Vec<[u8; SLOT]> = Vec::new(); + let tail = [0x21u8, 0x43, 0x65, 0x87, 0x09, 0xab, 0xcd, 0xef, 0x11, 0x22]; + + // Every one-byte and two-byte opcode, crossed with every prefix and every + // ModRM shape. This is the part that finds a mode bug rather than a table + // typo: it walks the whole first byte, including the block 32-bit mode + // spends on inc and dec. + for pre in PREFIXES { + for op in 0u16..=0xff { + for m in MODRM { + for lead in [None, Some(0x0fu8)] { + let mut v = pre.to_vec(); + if let Some(l) = lead { + v.push(l); + } + v.push(op as u8); + v.push(m); + // A SIB byte where the ModRM asked for one, then enough + // bytes for any displacement or immediate. + v.push(0x8d); + v.extend_from_slice(&tail); + out.push(slot(&v)); + } + } + } + } + + // The three-byte maps. + for op2 in [0x38u8, 0x3a] { + for op3 in 0u16..=0xff { + for m in [0x00u8, 0x05, 0x44, 0xc1] { + for pre in [&[0x66u8][..], &[][..]] { + let mut v = pre.to_vec(); + v.extend_from_slice(&[0x0f, op2, op3 as u8, m, 0x8d]); + v.extend_from_slice(&tail); + out.push(slot(&v)); + } + } + } + } + + // The x87 escapes in full: the ModRM byte is an opcode extension above + // 0xc0, so every one of the 256 values is a different instruction. + for op in 0xd8u8..=0xdf { + for m in 0u16..=0xff { + let mut v = vec![op, m as u8, 0x8d]; + v.extend_from_slice(&tail); + out.push(slot(&v)); + let mut v = vec![0x67u8, op, m as u8, 0x8d]; + v.extend_from_slice(&tail); + out.push(slot(&v)); + } + } + + // Random bytes, which reach prefix pile-ups and operand combinations no + // structured sweep thought of. + let mut r = Rng(0x1234_5678_9abc_def0); + for _ in 0..200_000 { + let mut s = [0x90u8; SLOT]; + let n = 1 + (r.next() % 8) as usize; + for b in s.iter_mut().take(n) { + *b = (r.next() >> 13) as u8; + } + out.push(s); + } + + out +} + +#[derive(Default)] +struct Tally { + matched: usize, + undecoded: usize, + /// Decoded where llvm declined, which is not counted as a disagreement + /// because llvm declines a few real encodings, but is reported. + extra: usize, + /// Those counted by mnemonic, so the report can show whether they are + /// encodings llvm cannot reach or ones this decoder should not accept. + extra_by_name: BTreeMap, + /// Slots where llvm printed a bare prefix as its own one-byte line rather + /// than attaching it to the instruction after it. That is a presentation + /// choice, not a decode, and this decoder makes the other one. + split_prefix: usize, + wrong: Vec, + missing: BTreeMap, +} + +impl Tally { + fn merge(&mut self, o: Tally) { + self.matched += o.matched; + self.undecoded += o.undecoded; + self.extra += o.extra; + for (k, (n, ex)) in o.extra_by_name { + let e = self.extra_by_name.entry(k).or_insert((0, ex)); + e.0 += n; + } + self.split_prefix += o.split_prefix; + self.wrong.extend(o.wrong); + for (k, v) in o.missing { + *self.missing.entry(k).or_default() += v; + } + } + + fn total(&self) -> usize { + self.matched + self.undecoded + self.wrong.len() + } + + fn coverage(&self) -> f64 { + if self.total() == 0 { + return 1.0; + } + (self.matched + self.wrong.len()) as f64 / self.total() as f64 + } +} + +/// The mnemonic of an oracle line, which is the first field that is not a +/// prefix: llvm puts any prefix it spells out in front of the mnemonic, so +/// taking the first field would file every `addr16` line under one name. +fn mnemonic_of(text: &str) -> String { + text.split('\t') + .filter(|f| !f.is_empty()) + .find(|f| !is_bare_prefix(f)) + .unwrap_or("?") + .to_string() +} + +/// Whether an oracle line is nothing but prefix bytes. llvm spells such a run +/// out word by word, so every field of the line is one of the prefix names. +fn is_bare_prefix(text: &str) -> bool { + let mut any = false; + for field in text.split('\t').filter(|f| !f.is_empty()) { + any = true; + if !matches!( + field, + "lock" + | "xacquire" + | "xrelease" + | "addr16" + | "addr32" + | "data16" + | "data32" + | "rep" + | "repne" + | "cs" + | "ds" + | "es" + | "fs" + | "gs" + | "ss" + ) { + return false; + } + } + any +} + +/// Compare one batch of oracle lines against the decoder. `window` says how +/// many bytes the decoder may read, which for a slot is the whole slot. +fn compare(lines: &[Line], bytes_of: impl Fn(&Line) -> Vec) -> Tally { + let mut t = Tally::default(); + for l in lines { + let input = bytes_of(l); + let unknown = l.text.starts_with("") || l.text.is_empty(); + // llvm-objdump prints a run of prefixes that reaches no opcode as a + // line of its own, with no instruction on it at all. This decoder + // attaches prefixes to the instruction they precede and so never + // produces one, which is a different rendering of the same bytes + // rather than a different reading of them, so the slot is skipped. + if is_bare_prefix(&l.text) { + t.split_prefix += 1; + continue; + } + let decoded = x86::decode32(&input, Addr(l.addr)); + if unknown { + if let Some(i) = decoded { + t.extra += 1; + let got = x86::format(&i, x86::Style::default()); + let name = mnemonic_of(&got); + let ex = format!( + "{:02x?} -> {got}", + &input[..input.len().min(i.len as usize)] + ); + let e = t.extra_by_name.entry(name).or_insert((0, ex)); + e.0 += 1; + } + continue; + } + match decoded { + None => { + t.undecoded += 1; + *t.missing.entry(mnemonic_of(&l.text)).or_default() += 1; + } + Some(i) if i.len as usize != l.bytes.len() => t.wrong.push(format!( + "{:x}: {:02x?} llvm {:?} length {} != llvm's {}", + l.addr, + &input[..input.len().min(8)], + l.text, + i.len, + l.bytes.len() + )), + Some(i) => { + let got = x86::format(&i, x86::Style::default()); + if got == l.text { + t.matched += 1; + } else { + t.wrong.push(format!( + "{:x}: {:02x?} llvm {:?} != ours {:?}", + l.addr, + &input[..input.len().min(8)], + l.text, + got + )); + } + } + } + } + t +} + +/// Run the whole sweep, in batches so the assembler input stays a sane size. +fn sweep(mc: &str, dump: &str) -> Option { + let all = candidates(); + let mut t = Tally::default(); + for chunk in all.chunks(40_000) { + let lines = oracle(mc, dump, chunk)?; + let by_slot = |l: &Line| { + let n = l.addr as usize / SLOT; + chunk[n].to_vec() + }; + t.merge(compare(&lines, by_slot)); + } + Some(t) +} + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// The compiled i386 fixtures, which `scripts/build-fixtures.sh` names `x32`. +fn fixtures() -> Vec { + let Some(dir) = corpus() else { + return Vec::new(); + }; + let mut v: Vec = std::fs::read_dir(dir) + .map(|rd| { + rd.filter_map(|e| { + let p = e.ok()?.path(); + let n = p.file_name()?.to_string_lossy().into_owned(); + n.contains("x32").then_some(p) + }) + .collect() + }) + .unwrap_or_default(); + v.sort(); + v +} + +fn describe(t: &Tally, what: &str) -> String { + format!( + "i386 {what}: {} instructions, {} matched, {} wrong, {} undecoded, {:.3}% coverage ({} decoded where llvm declined)", + t.total(), + t.matched, + t.wrong.len(), + t.undecoded, + t.coverage() * 100.0, + t.extra, + ) + &format!(", {} bare prefix lines skipped", t.split_prefix) +} + +fn fail_if_wrong(t: &Tally, what: &str) { + if t.wrong.is_empty() { + return; + } + let mut by_kind: BTreeMap<&str, usize> = BTreeMap::new(); + for w in &t.wrong { + let kind = w + .split("llvm \"") + .nth(1) + .and_then(|s| s.split(['\\', '"']).next()) + .unwrap_or("length"); + *by_kind.entry(kind).or_default() += 1; + } + let summary: Vec = by_kind.iter().map(|(k, n)| format!("{k} x{n}")).collect(); + panic!( + "{} of {} i386 {what} instructions decode differently\nby mnemonic: {}\nfirst 20:\n{}", + t.wrong.len(), + t.total(), + summary.join(", "), + t.wrong + .iter() + .take(20) + .cloned() + .collect::>() + .join("\n") + ); +} + +#[test] +fn i386_sweep_matches_llvm() { + let (Some(mc), Some(dump)) = (assembler(), objdump()) else { + return; + }; + let Some(t) = sweep(mc, dump) else { + return; + }; + if t.total() < 1000 { + return; // the oracle did not run + } + fail_if_wrong(&t, "swept"); + assert!( + t.coverage() >= MIN_SWEEP_COVERAGE, + "{} (floor is {:.1}%)", + describe(&t, "sweep"), + MIN_SWEEP_COVERAGE * 100.0 + ); + println!("{}", describe(&t, "sweep")); +} + +#[test] +fn i386_fixtures_match_llvm() { + let (Some(dump), files) = (objdump(), fixtures()) else { + return; + }; + if files.is_empty() { + return; + } + let mut all = Tally::default(); + for p in &files { + let lines = disassemble_file(dump, p); + all.merge(compare(&lines, |l| l.bytes.clone())); + } + if all.total() < 100 { + return; + } + fail_if_wrong(&all, "compiled"); + assert!( + all.coverage() >= MIN_FIXTURE_COVERAGE, + "{} (floor is {:.1}%)", + describe(&all, "fixtures"), + MIN_FIXTURE_COVERAGE * 100.0 + ); + println!("{}", describe(&all, "fixtures")); +} + +#[test] +fn the_inc_block_is_not_a_rex_prefix() { + // The single most common way a 32-bit decoder written as a 64-bit one goes + // wrong: 0x40 eats the next byte and everything after it desynchronizes. + let i = x86::decode32(&[0x40, 0x89, 0xc8], Addr(0)).unwrap(); + assert_eq!(i.len, 1); + assert_eq!(x86::format(&i, x86::Style::default()), "inc\teax"); + let i = x86::decode32(&[0x4f], Addr(0)).unwrap(); + assert_eq!(x86::format(&i, x86::Style::default()), "dec\tedi"); + // The same bytes in long mode are a prefix on the instruction after them. + let i = x86::decode(&[0x48, 0x89, 0xc8], Addr(0)).unwrap(); + assert_eq!(i.len, 3); + assert_eq!(x86::format(&i, x86::Style::default()), "mov\trax, rcx"); +} + +#[test] +fn absolute_addressing_is_not_pc_relative() { + let i = x86::decode32(&[0x8b, 0x05, 0x78, 0x56, 0x34, 0x12], Addr(0x1000)).unwrap(); + assert_eq!( + x86::format(&i, x86::Style::default()), + "mov\teax, dword ptr [0x12345678]" + ); + let Some(e5r_arch::Operand::Mem(m)) = i.operands().get(1).copied() else { + panic!("expected a memory operand"); + }; + assert!(!m.is_pc_relative()); +} + +#[test] +fn the_mode_only_instructions_decode_in_their_own_mode() { + let d32 = |b: &[u8]| x86::decode32(b, Addr(0)).map(|i| x86::format(&i, x86::Style::default())); + let d64 = |b: &[u8]| x86::decode(b, Addr(0)).map(|i| x86::format(&i, x86::Style::default())); + assert_eq!(d32(&[0x60]).as_deref(), Some("pushal")); + assert_eq!(d32(&[0x61]).as_deref(), Some("popal")); + assert_eq!(d32(&[0x27]).as_deref(), Some("daa")); + assert_eq!(d32(&[0xce]).as_deref(), Some("into")); + assert_eq!( + d32(&[0x62, 0x0a]).as_deref(), + Some("bound\tecx, dword ptr [edx]") + ); + assert_eq!(d32(&[0x63, 0xc8]).as_deref(), Some("arpl\tax, cx")); + assert_eq!(d32(&[0xc4, 0x08]).as_deref(), Some("les\tecx, [eax]")); + // 0x63 is `movsxd` in long mode and `arpl` here, off the same byte. + assert_eq!( + d64(&[0x48, 0x63, 0xc8]).as_deref(), + Some("movsxd\trcx, eax") + ); + // None of the 32-bit-only opcodes decode in long mode. + for b in [0x60u8, 0x61, 0x27, 0xce, 0x9a, 0xea, 0xd4, 0xd5] { + assert!(d64(&[b, 0x0a, 0, 0, 0, 0, 0]).is_none(), "{b:#x}"); + } +} + +#[test] +fn sixteen_bit_addressing_survives_under_0x67() { + let d = |b: &[u8]| x86::decode32(b, Addr(0)).map(|i| x86::format(&i, x86::Style::default())); + assert_eq!( + d(&[0x67, 0x8b, 0x00]).as_deref(), + Some("mov\teax, dword ptr [bx + si]") + ); + assert_eq!( + d(&[0x67, 0x8b, 0x41, 0x10]).as_deref(), + Some("mov\teax, dword ptr [bx + di + 0x10]") + ); + assert_eq!( + d(&[0x67, 0x8b, 0x06, 0x34, 0x12]).as_deref(), + Some("addr16\t\tmov\teax, dword ptr [0x1234]") + ); +} + +#[test] +fn no_byte_sequence_panics_in_32_bit_mode() { + let mut buf = [0u8; 16]; + let mut r = Rng(99); + for _ in 0..200_000 { + for b in buf.iter_mut() { + *b = (r.next() >> 21) as u8; + } + let _ = x86::decode32(&buf, Addr(0x1000)); + } +} + +/// A work list rather than a pass or fail: what the sweep still declines and +/// what it still gets wrong, biggest class first. Ignored by default. +#[test] +#[ignore] +fn x86_32_parity_report() { + let (Some(mc), Some(dump)) = (assembler(), objdump()) else { + return; + }; + let Some(t) = sweep(mc, dump) else { return }; + println!("{}", describe(&t, "sweep")); + let mut wrong: BTreeMap = BTreeMap::new(); + for w in &t.wrong { + let key = w + .split("llvm \"") + .nth(1) + .and_then(|s| s.split(['\\', '"']).next()) + .unwrap_or("length") + .to_string(); + let e = wrong.entry(key).or_insert((0, w.clone())); + e.0 += 1; + } + let mut v: Vec<_> = wrong.into_iter().collect(); + v.sort_by_key(|(_, (n, _))| std::cmp::Reverse(*n)); + println!("--- disagreements ---"); + for (k, (n, ex)) in v.iter().take(40) { + println!("{n:>7} {k:<16} {ex}"); + } + let mut x: Vec<_> = t.extra_by_name.iter().collect(); + x.sort_by_key(|(_, (n, _))| std::cmp::Reverse(*n)); + println!("--- decoded where llvm declined ---"); + for (k, (n, ex)) in x.iter().take(30) { + println!("{n:>7} {k:<16} {ex}"); + } + let mut m: Vec<_> = t.missing.iter().collect(); + m.sort_by_key(|(_, n)| std::cmp::Reverse(**n)); + println!("--- undecoded ---"); + for (k, n) in m.iter().take(60) { + println!("{n:>7} {k}"); + } +} diff --git a/crates/e5r-arch/tests/x86_parity.rs b/crates/e5r-arch/tests/x86_parity.rs new file mode 100644 index 0000000..416ddea --- /dev/null +++ b/crates/e5r-arch/tests/x86_parity.rs @@ -0,0 +1,251 @@ +//! G3 for x86-64: the decoder must agree with llvm-objdump in Intel syntax. +//! +//! The host here is aarch64, so the oracle is `llvm-objdump --x86-asm-syntax=intel` +//! over cross-compiled objects. Skipped when neither is available. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_arch::x86; +use e5r_core::Addr; + +/// Coverage floor over the corpus. Raise it as the tables fill in. +const MIN_COVERAGE: f64 = 0.999; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// The first llvm-objdump on this machine that can do Intel syntax. +fn objdump() -> Option<&'static str> { + ["llvm-objdump-18", "llvm-objdump-15", "llvm-objdump"] + .into_iter() + .find(|&c| Command::new(c).arg("--version").output().is_ok()) + .map(|v| v as _) +} + +struct Line { + addr: u64, + bytes: Vec, + text: String, +} + +fn disassemble(tool: &str, path: &Path) -> Vec { + let out = Command::new(tool) + .args(["-d", "--x86-asm-syntax=intel"]) + .arg(path) + .output(); + let Ok(out) = out else { return Vec::new() }; + let text = String::from_utf8_lossy(&out.stdout); + let mut lines = Vec::new(); + for l in text.lines() { + // " 0: 85 ff \ttest\tedi, edi" + let Some((left, rest)) = l.split_once(": ") else { + continue; + }; + let Ok(addr) = u64::from_str_radix(left.trim(), 16) else { + continue; + }; + let Some((raw, body)) = rest.split_once('\t') else { + continue; + }; + let bytes: Option> = raw + .split_whitespace() + .map(|h| u8::from_str_radix(h, 16).ok()) + .collect(); + let Some(bytes) = bytes else { continue }; + if bytes.is_empty() { + continue; + } + // Drop the symbolic suffix llvm appends to branch targets. + let body = match body.find(" #").or_else(|| body.find(" <")) { + Some(i) => &body[..i], + None => body, + }; + lines.push(Line { + addr, + bytes, + text: body.trim_end().to_string(), + }); + } + lines +} + +#[derive(Default)] +struct Tally { + matched: usize, + undecoded: usize, + wrong: Vec, +} + +impl Tally { + fn total(&self) -> usize { + self.matched + self.undecoded + self.wrong.len() + } + fn coverage(&self) -> f64 { + if self.total() == 0 { + return 1.0; + } + (self.matched + self.wrong.len()) as f64 / self.total() as f64 + } +} + +fn check(tool: &str, path: &Path) -> Tally { + let mut t = Tally::default(); + for l in disassemble(tool, path) { + if l.text.starts_with("") || l.text.is_empty() { + continue; + } + match x86::decode(&l.bytes, Addr(l.addr)) { + None => t.undecoded += 1, + Some(i) if i.len as usize != l.bytes.len() => t.wrong.push(format!( + "{:x}: {:02x?} length {} != llvm's {}", + l.addr, + l.bytes, + i.len, + l.bytes.len() + )), + Some(i) => { + let got = x86::format(&i, x86::Style::default()); + if got == l.text { + t.matched += 1; + } else { + t.wrong.push(format!( + "{:x}: {:02x?} llvm {:?} != ours {:?}", + l.addr, l.bytes, l.text, got + )); + } + } + } + } + t +} + +fn fixtures() -> Vec { + let Some(dir) = corpus() else { + return Vec::new(); + }; + std::fs::read_dir(dir) + .map(|rd| { + rd.filter_map(|e| { + let p = e.ok()?.path(); + let n = p.file_name()?.to_string_lossy().into_owned(); + n.contains("x64").then_some(p) + }) + .collect() + }) + .unwrap_or_default() +} + +#[test] +fn x86_64_matches_llvm_objdump() { + let (Some(tool), files) = (objdump(), fixtures()) else { + return; + }; + if files.is_empty() { + return; + } + let mut all = Tally::default(); + let mut named = Vec::new(); + for p in &files { + let t = check(tool, p); + let name = p.file_name().unwrap().to_string_lossy().into_owned(); + named.extend(t.wrong.iter().map(|w| format!("{name}: {w}"))); + all.matched += t.matched; + all.undecoded += t.undecoded; + all.wrong.extend(t.wrong); + } + if all.total() < 20 { + return; // nothing useful to compare + } + + if !named.is_empty() { + let mut by_kind: BTreeMap = BTreeMap::new(); + for f in &named { + let kind = f + .split("llvm \"") + .nth(1) + .and_then(|s| s.split(['\t', '"']).next()) + .unwrap_or("length") + .to_string(); + *by_kind.entry(kind).or_default() += 1; + } + let summary: Vec = by_kind.iter().map(|(k, n)| format!("{k} x{n}")).collect(); + panic!( + "{} of {} x86 instructions decode differently\nby mnemonic: {}\nfirst 20:\n{}", + named.len(), + all.total(), + summary.join(", "), + named + .iter() + .take(20) + .cloned() + .collect::>() + .join("\n") + ); + } + assert!( + all.coverage() >= MIN_COVERAGE, + "decoded {:.2}% of {} x86 instructions, floor is {:.0}% ({} undecoded)", + all.coverage() * 100.0, + all.total(), + MIN_COVERAGE * 100.0, + all.undecoded + ); + println!( + "x86 parity: {} instructions, {} matched, {} undecoded, {:.2}% coverage", + all.total(), + all.matched, + all.undecoded, + all.coverage() * 100.0 + ); +} + +/// A work list, like the AArch64 one. Ignored by default. +#[test] +#[ignore] +fn x86_parity_report() { + let (Some(tool), files) = (objdump(), fixtures()) else { + return; + }; + let mut by_kind: BTreeMap = BTreeMap::new(); + let mut example: BTreeMap = BTreeMap::new(); + let mut undecoded = 0; + let mut total = 0; + for p in &files { + for l in disassemble(tool, p) { + if l.text.is_empty() { + continue; + } + total += 1; + let key = l.text.split('\t').next().unwrap_or("?").to_string(); + match x86::decode(&l.bytes, Addr(l.addr)) { + None => { + undecoded += 1; + *by_kind.entry(format!("UNDECODED {key}")).or_default() += 1; + example + .entry(format!("UNDECODED {key}")) + .or_insert(format!("{:02x?} {}", l.bytes, l.text)); + } + Some(i) => { + let got = x86::format(&i, x86::Style::default()); + if got != l.text || i.len as usize != l.bytes.len() { + *by_kind.entry(key.clone()).or_default() += 1; + example.entry(key).or_insert(format!( + "{:02x?} llvm {:?} ours {:?}", + l.bytes, l.text, got + )); + } + } + } + } + } + let mut v: Vec<_> = by_kind.into_iter().collect(); + v.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); + println!("{total} instructions, {undecoded} undecoded"); + for (k, n) in v.iter().take(40) { + println!("{n:>6} {k:<24} {}", example[k]); + } +} diff --git a/crates/e5r-asm/Cargo.toml b/crates/e5r-asm/Cargo.toml new file mode 100644 index 0000000..18a5ef7 --- /dev/null +++ b/crates/e5r-asm/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "e5r-asm" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +description = "Instruction encoders for x86-64 and AArch64, the inverse of the decoders in e5r-arch." + +[dependencies] +e5r-core.workspace = true +e5r-arch.workspace = true diff --git a/crates/e5r-asm/src/aarch64.rs b/crates/e5r-asm/src/aarch64.rs new file mode 100644 index 0000000..4b26321 --- /dev/null +++ b/crates/e5r-asm/src/aarch64.rs @@ -0,0 +1,2034 @@ +//! AArch64 encoding: one [`Insn`] to one 32-bit word. +//! +//! Written against the architecture reference manual's field layouts, and +//! checked against the decoder in `e5r-arch`, which is the inverse function. +//! Where the decoder resolves an alias (`asr` for the right `sbfm`), this +//! lowers the same alias back to the same encoding, so the round trip is the +//! identity on bytes rather than only on meaning. + +// Encoding literals are grouped the way the manual draws them. +#![allow(clippy::unusual_byte_groupings)] + +use e5r_arch::insn::{AddrMode, Extend, Insn, Operand, Reg, RegClass, Shift, Width}; +use e5r_core::Addr; + +use crate::Encoded; +use crate::error::AsmError; + +/// Every mnemonic this encoder accepts, as the static strings [`Insn`] holds. +const MNEMONICS: &[&str] = &[ + "adc", + "adcs", + "add", + "adds", + "adr", + "adrp", + "and", + "ands", + "asr", + "asrv", + "autia1716", + "autiasp", + "autiaz", + "autib1716", + "autibsp", + "autibz", + "b", + "b.al", + "b.cc", + "b.cs", + "b.eq", + "b.ge", + "b.gt", + "b.hi", + "b.hs", + "b.le", + "b.lo", + "b.ls", + "b.lt", + "b.mi", + "b.ne", + "b.nv", + "b.pl", + "b.vc", + "b.vs", + "bfi", + "bfm", + "bfxil", + "bic", + "bics", + "bl", + "blr", + "br", + "brk", + "bti", + "cbnz", + "cbz", + "ccmn", + "ccmp", + "cinc", + "cinv", + "cls", + "clz", + "cmn", + "cmp", + "cneg", + "csel", + "cset", + "csetm", + "csinc", + "csinv", + "csneg", + "dmb", + "dsb", + "eon", + "eor", + "eret", + "extr", + "hint", + "hlt", + "hvc", + "isb", + "ldnp", + "ldp", + "ldpsw", + "ldr", + "ldrb", + "ldrh", + "ldrsb", + "ldrsh", + "ldrsw", + "ldur", + "ldurb", + "ldurh", + "ldursb", + "ldursh", + "ldursw", + "lsl", + "lslv", + "lsr", + "lsrv", + "madd", + "mneg", + "mov", + "movk", + "movn", + "movz", + "msub", + "mul", + "mvn", + "neg", + "negs", + "ngc", + "ngcs", + "nop", + "orn", + "orr", + "pacia1716", + "paciasp", + "paciaz", + "pacib1716", + "pacibsp", + "pacibz", + "prfm", + "prfum", + "rbit", + "ret", + "rev", + "rev16", + "rev32", + "ror", + "rorv", + "sbc", + "sbcs", + "sbfiz", + "sbfm", + "sbfx", + "sdiv", + "sev", + "sevl", + "smaddl", + "smc", + "smnegl", + "smsubl", + "smulh", + "smull", + "stnp", + "stp", + "str", + "strb", + "strh", + "stur", + "sturb", + "sturh", + "sub", + "subs", + "svc", + "sxtb", + "sxth", + "sxtw", + "tbnz", + "tbz", + "tst", + "ubfiz", + "ubfm", + "ubfx", + "udf", + "udiv", + "umaddl", + "umnegl", + "umsubl", + "umulh", + "umull", + "uxtb", + "uxth", + "wfe", + "wfi", + "xpaclri", + "yield", +]; + +/// Resolve a mnemonic to the static string an [`Insn`] can hold. +pub(crate) fn intern(s: &str) -> Option<&'static str> { + MNEMONICS.binary_search(&s).ok().map(|n| MNEMONICS[n]) +} + +/// Names that appear as bare words in an operand position. +const NAMES: &[&str] = &[ + "c", + "ish", + "ishld", + "ishst", + "j", + "jc", + "ld", + "nsh", + "nshld", + "nshst", + "osh", + "oshld", + "oshst", + "pldl1keep", + "pldl1strm", + "pldl2keep", + "pldl2strm", + "pldl3keep", + "pldl3strm", + "pldslckeep", + "pldslcstrm", + "plil1keep", + "plil1strm", + "plil2keep", + "plil2strm", + "plil3keep", + "plil3strm", + "plislckeep", + "plislcstrm", + "pstl1keep", + "pstl1strm", + "pstl2keep", + "pstl2strm", + "pstl3keep", + "pstl3strm", + "pstslckeep", + "pstslcstrm", + "st", + "sy", +]; + +pub(crate) fn name(s: &str) -> Option<&'static str> { + NAMES.binary_search(&s).ok().map(|n| NAMES[n]) +} + +/// Barrier option value for a name, `CRm` in the encoding. +fn barrier_crm(s: &str) -> Option { + Some(match s { + "oshld" => 1, + "oshst" => 2, + "osh" => 3, + "nshld" => 5, + "nshst" => 6, + "nsh" => 7, + "ishld" => 9, + "ishst" => 10, + "ish" => 11, + "ld" => 13, + "st" => 14, + "sy" => 15, + _ => return None, + }) +} + +/// The prefetch operation number a name stands for. +fn prefetch_num(s: &str) -> Option { + let (kind, rest) = if let Some(r) = s.strip_prefix("pld") { + (0u32, r) + } else if let Some(r) = s.strip_prefix("pli") { + (1, r) + } else { + (2, s.strip_prefix("pst")?) + }; + let (target, rest) = if let Some(r) = rest.strip_prefix("l1") { + (0u32, r) + } else if let Some(r) = rest.strip_prefix("l2") { + (1, r) + } else if let Some(r) = rest.strip_prefix("l3") { + (2, r) + } else { + (3, rest.strip_prefix("slc")?) + }; + let policy = match rest { + "keep" => 0u32, + "strm" => 1, + _ => return None, + }; + Some((kind << 3) | (target << 1) | policy) +} + +/// Parse a register name. +pub(crate) fn register(s: &str) -> Option { + match s { + "sp" => { + return Some(Reg { + class: RegClass::Sp, + num: 31, + width: Width::W64, + }); + } + "wsp" => { + return Some(Reg { + class: RegClass::Sp, + num: 31, + width: Width::W32, + }); + } + "xzr" => { + return Some(Reg { + class: RegClass::Zr, + num: 31, + width: Width::W64, + }); + } + "wzr" => { + return Some(Reg { + class: RegClass::Zr, + num: 31, + width: Width::W32, + }); + } + _ => {} + } + let (head, rest) = s.split_at_checked(1)?; + let num: u32 = if rest.len() <= 2 && !rest.is_empty() { + rest.parse().ok()? + } else { + return None; + }; + let width = match head { + "x" => Width::W64, + "w" => Width::W32, + "b" => Width::W8, + "h" => Width::W16, + "s" => Width::W32, + "d" => Width::W64, + "q" => Width::W128, + _ => return None, + }; + if matches!(head, "x" | "w") { + if num > 30 { + return None; + } + Some(Reg::gpr(num as u8, width)) + } else { + if num > 31 { + return None; + } + Some(Reg::vec(num as u8, width)) + } +} + +/// The condition code a name stands for. +pub(crate) fn condition(s: &str) -> Option { + Some(match s { + "eq" => 0, + "ne" => 1, + "cs" | "hs" => 2, + "cc" | "lo" => 3, + "mi" => 4, + "pl" => 5, + "vs" => 6, + "vc" => 7, + "hi" => 8, + "ls" => 9, + "ge" => 10, + "lt" => 11, + "gt" => 12, + "le" => 13, + "al" => 14, + "nv" => 15, + _ => return None, + }) +} + +// ------------------------------------------------------------------ helpers + +fn form(i: &Insn, detail: &'static str) -> AsmError { + AsmError::UnsupportedForm { + mnemonic: i.mnemonic.to_string(), + detail, + } +} + +/// The register number, with 31 standing for both `sp` and the zero register. +fn num(i: &Insn, r: Reg) -> Result { + match r.class { + RegClass::Gpr => Ok(r.num as u32 & 31), + RegClass::Sp | RegClass::Zr => Ok(31), + _ => Err(form(i, "expected a general purpose register")), + } +} + +fn gpr_op(i: &Insn, n: usize) -> Result { + match i.operands().get(n) { + Some(Operand::Reg(r)) if matches!(r.class, RegClass::Gpr | RegClass::Sp | RegClass::Zr) => { + Ok(*r) + } + _ => Err(form(i, "expected a general purpose register operand")), + } +} + +fn sf_of(r: Reg) -> Result { + match r.width { + Width::W64 => Ok(1), + Width::W32 => Ok(0), + _ => Err(AsmError::UnsupportedForm { + mnemonic: String::new(), + detail: "a general purpose operand is 32 or 64 bits", + }), + } +} + +/// An immediate, however the parser or the decoder chose to spell it. +fn imm_of(op: &Operand) -> Option { + match op { + Operand::Imm(v) | Operand::Count(v) => Some(*v), + Operand::UImm(v) => Some(*v as i64), + _ => None, + } +} + +fn uimm_of(op: &Operand) -> Option { + match op { + Operand::Imm(v) | Operand::Count(v) => Some(*v as u64), + Operand::UImm(v) => Some(*v), + _ => None, + } +} + +fn addr_of(op: &Operand) -> Option { + match op { + Operand::Addr(a) => Some(*a), + Operand::Imm(v) | Operand::Count(v) => Some(Addr(*v as u64)), + Operand::UImm(v) => Some(Addr(*v)), + _ => None, + } +} + +fn fits(value: i64, bits: u32, what: &'static str) -> Result { + let low = -(1i64 << (bits - 1)); + let high = (1i64 << (bits - 1)) - 1; + if value < low || value > high { + return Err(AsmError::Range { + what, + value, + low, + high, + }); + } + Ok((value as u64 & ((1u64 << bits) - 1)) as u32) +} + +fn unsigned_fits(value: i64, bits: u32, what: &'static str) -> Result { + let high = (1i64 << bits) - 1; + if !(0..=high).contains(&value) { + return Err(AsmError::Range { + what, + value, + low: 0, + high, + }); + } + Ok(value as u32) +} + +/// A branch displacement in bytes, checked for range and alignment. +fn displacement(i: &Insn, to: Addr, bits: u32) -> Result { + let delta = to.get().wrapping_sub(i.addr.get()) as i64; + let high = ((1i64 << (bits - 1)) - 1) * 4; + let low = -(1i64 << (bits - 1)) * 4; + if delta % 4 != 0 { + return Err(AsmError::Unaligned { + what: "branch target", + value: delta, + align: 4, + }); + } + if delta < low || delta > high { + return Err(AsmError::BranchRange { + mnemonic: i.mnemonic.to_string(), + from: i.addr.get(), + to: to.get(), + low, + high, + }); + } + Ok(((delta >> 2) as u64 & ((1u64 << bits) - 1)) as u32) +} + +// -------------------------------------------------------- logical immediates + +fn mask_bits(n: u32) -> u64 { + if n >= 64 { u64::MAX } else { (1u64 << n) - 1 } +} + +/// The inverse of the architecture's `DecodeBitMasks`: `N:immr:imms` for a +/// value, or `None` when no rotated run of ones produces it. +pub(crate) fn encode_bitmask(value: u64, sf: bool) -> Option<(u32, u32, u32)> { + if !sf && value >> 32 != 0 { + return None; + } + // Work at 64 bits with the 32-bit case replicated, which is what the + // architecture does, so one search covers both. + let val = if sf { + value + } else { + (value & 0xffff_ffff) | (value << 32) + }; + if val == 0 || val == u64::MAX { + return None; + } + // The element size is the smallest power of two the value repeats at. + let mut size = 64u32; + while size > 2 { + let half = size / 2; + let m = mask_bits(half); + let mut repeats = true; + let mut pos = 0; + while pos < 64 { + if ((val >> pos) & m) != (val & m) { + repeats = false; + break; + } + pos += half; + } + if !repeats { + break; + } + size = half; + } + let elem = val & mask_bits(size); + let ones = elem.count_ones(); + if ones == 0 || ones == size { + return None; + } + // The set bits have to be contiguous once rotated left by `immr`. + let target = mask_bits(ones); + let mut immr = None; + for r in 0..size { + let rotated = ((elem << r) | (elem >> (size - r).min(63))) & mask_bits(size); + let rotated = if r == 0 { elem } else { rotated }; + if rotated == target { + immr = Some(r); + break; + } + } + let immr = immr?; + let n = u32::from(size == 64); + let imms = (!((size << 1) - 1) & 0x3f) | (ones - 1); + Some((n, immr, imms)) +} + +// ------------------------------------------------------------------- encode + +/// Encode one instruction. +pub fn encode(i: &Insn) -> Result { + let w = word(i)?; + Ok(Encoded::from_u32_le(w)) +} + +fn word(i: &Insn) -> Result { + match i.mnemonic { + "add" | "adds" | "sub" | "subs" => add_sub(i), + "cmp" | "cmn" => cmp(i), + "neg" | "negs" => neg(i), + "and" | "ands" | "orr" | "eor" | "bic" | "bics" | "orn" | "eon" => logical(i), + "tst" => tst(i), + "mvn" => mvn(i), + "mov" => mov(i), + "movz" | "movn" | "movk" => move_wide(i), + "adr" | "adrp" => pc_rel(i), + "b" | "bl" => branch_imm(i), + m if m.starts_with("b.") => b_cond(i), + "cbz" | "cbnz" => cmp_branch(i), + "tbz" | "tbnz" => test_branch(i), + "br" | "blr" | "ret" => branch_reg(i), + "eret" => Ok(0xd69f_03e0), + "svc" | "hvc" | "smc" | "brk" | "hlt" => exception(i), + "nop" | "yield" | "wfe" | "wfi" | "sev" | "sevl" | "hint" | "xpaclri" | "pacia1716" + | "pacib1716" | "autia1716" | "autib1716" | "paciaz" | "paciasp" | "pacibz" | "pacibsp" + | "autiaz" | "autiasp" | "autibz" | "autibsp" => hint(i), + "bti" => bti(i), + "dmb" | "dsb" | "isb" => barrier(i), + "ldr" | "str" | "ldrb" | "strb" | "ldrh" | "strh" | "ldrsb" | "ldrsh" | "ldrsw" + | "ldur" | "stur" | "ldurb" | "sturb" | "ldurh" | "sturh" | "ldursb" | "ldursh" + | "ldursw" | "prfm" | "prfum" => load_store(i), + "ldp" | "stp" | "ldnp" | "stnp" | "ldpsw" => load_store_pair(i), + "lsl" | "lsr" | "asr" | "ror" => shift(i), + "lslv" | "lsrv" | "asrv" | "rorv" | "udiv" | "sdiv" => dp2(i), + "ubfm" | "sbfm" | "bfm" => bfm_raw(i), + "ubfx" | "sbfx" | "bfxil" | "ubfiz" | "sbfiz" | "bfi" => bitfield(i), + "sxtb" | "sxth" | "sxtw" | "uxtb" | "uxth" => extend_op(i), + "mul" | "madd" | "msub" | "mneg" | "smull" | "umull" | "smaddl" | "umaddl" | "smsubl" + | "umsubl" | "smnegl" | "umnegl" | "smulh" | "umulh" => multiply(i), + "csel" | "csinc" | "csinv" | "csneg" => csel(i), + "cset" | "csetm" => cset(i), + "cinc" | "cinv" | "cneg" => cinc(i), + "ccmp" | "ccmn" => ccmp(i), + "adc" | "adcs" | "sbc" | "sbcs" => adc(i), + "ngc" | "ngcs" => ngc(i), + "extr" => extr(i), + "rbit" | "rev" | "rev16" | "rev32" | "clz" | "cls" => dp1(i), + "udf" => udf(i), + _ => Err(form(i, "no encoder for this mnemonic")), + } +} + +fn add_sub(i: &Insn) -> Result { + let sub = u32::from(i.mnemonic.starts_with("sub")); + let s = u32::from(i.mnemonic.ends_with('s')); + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let sf = sf_of(rd).map_err(|_| form(i, "a destination is 32 or 64 bits"))?; + let third = i.operands().get(2).ok_or(form(i, "needs three operands"))?; + encode_add_sub( + i, + sf, + sub, + s, + num(i, rd)?, + num(i, rn)?, + third, + 3, + is_sp(rd) || is_sp(rn), + ) +} + +fn is_sp(r: Reg) -> bool { + r.class == RegClass::Sp +} + +/// The shared tail of add, sub, cmp, cmn and neg. +/// +/// Long because the callers have already decided every field between them: +/// gathering them into a struct would move the argument list rather than +/// shorten it. +#[allow(clippy::too_many_arguments)] +fn encode_add_sub( + i: &Insn, + sf: u32, + sub: u32, + s: u32, + rd: u32, + rn: u32, + third: &Operand, + next: usize, + sp_involved: bool, +) -> Result { + match third { + Operand::Imm(_) | Operand::UImm(_) | Operand::Count(_) => { + let v = imm_of(third).unwrap_or(0); + let mut sh = 0u32; + let mut imm = v; + if let Some(qualifier) = i.operands().get(next) { + match qualifier { + Operand::ShiftOp(Shift::Lsl, 12) => sh = 1, + Operand::ShiftOp(Shift::Lsl, 0) => sh = 0, + _ => return Err(form(i, "an add or subtract immediate shifts by 12 only")), + } + } else if !(0..=0xfff).contains(&v) + && v & 0xfff == 0 + && (0..=0xfff).contains(&(v >> 12)) + { + // A wide multiple of 4096 is what `lsl #12` is for. + sh = 1; + imm = v >> 12; + } + let imm12 = unsigned_fits(imm, 12, "add or subtract immediate")?; + Ok((sf << 31) + | (sub << 30) + | (s << 29) + | (0b100010 << 23) + | (sh << 22) + | (imm12 << 10) + | (rn << 5) + | rd) + } + Operand::Reg(rm) if sp_involved => { + let option = if sf == 1 { 0b011 } else { 0b010 }; + Ok(extended(sf, sub, s, option, 0, num(i, *rm)?, rn, rd)) + } + Operand::Reg(rm) => { + let rm = num(i, *rm)?; + Ok( + (sf << 31) + | (sub << 30) + | (s << 29) + | (0b01011 << 24) + | (rm << 16) + | (rn << 5) + | rd, + ) + } + Operand::Shifted(rm, kind, amount) => { + let shift = match kind { + Shift::Lsl => 0, + Shift::Lsr => 1, + Shift::Asr => 2, + _ => return Err(form(i, "an add or subtract shifts by lsl, lsr or asr")), + }; + let limit = if sf == 1 { 63 } else { 31 }; + if *amount as i64 > limit { + return Err(AsmError::Range { + what: "shift amount", + value: *amount as i64, + low: 0, + high: limit, + }); + } + Ok((sf << 31) + | (sub << 30) + | (s << 29) + | (0b01011 << 24) + | (shift << 22) + | (num(i, *rm)? << 16) + | ((*amount as u32) << 10) + | (rn << 5) + | rd) + } + Operand::Extended(rm, ext, amount) => { + let option = match ext { + Extend::Uxtb => 0, + Extend::Uxth => 1, + Extend::Uxtw => 2, + Extend::Uxtx => 3, + Extend::Sxtb => 4, + Extend::Sxth => 5, + Extend::Sxtw => 6, + Extend::Sxtx => 7, + // `lsl` on an extended operand is the no-op extension. + Extend::Lsl | Extend::LslZero => { + if sf == 1 { + 3 + } else { + 2 + } + } + }; + if *amount > 4 { + return Err(AsmError::Range { + what: "extend shift", + value: *amount as i64, + low: 0, + high: 4, + }); + } + Ok(extended( + sf, + sub, + s, + option, + *amount as u32, + num(i, *rm)?, + rn, + rd, + )) + } + _ => Err(form(i, "expected a register or an immediate")), + } +} + +#[allow(clippy::too_many_arguments)] +fn extended(sf: u32, sub: u32, s: u32, option: u32, amount: u32, rm: u32, rn: u32, rd: u32) -> u32 { + (sf << 31) + | (sub << 30) + | (s << 29) + | (0b01011 << 24) + | (1 << 21) + | (rm << 16) + | (option << 13) + | (amount << 10) + | (rn << 5) + | rd +} + +fn cmp(i: &Insn) -> Result { + let sub = u32::from(i.mnemonic == "cmp"); + let rn = gpr_op(i, 0)?; + let sf = sf_of(rn).map_err(|_| form(i, "a compare is 32 or 64 bits"))?; + let second = i.operands().get(1).ok_or(form(i, "needs two operands"))?; + encode_add_sub(i, sf, sub, 1, 31, num(i, rn)?, second, 2, is_sp(rn)) +} + +fn neg(i: &Insn) -> Result { + let s = u32::from(i.mnemonic.ends_with('s')); + let rd = gpr_op(i, 0)?; + let sf = sf_of(rd).map_err(|_| form(i, "a negate is 32 or 64 bits"))?; + let second = i.operands().get(1).ok_or(form(i, "needs two operands"))?; + encode_add_sub(i, sf, 1, s, num(i, rd)?, 31, second, 2, false) +} + +fn logical_bits(m: &str) -> (u32, u32) { + match m { + "and" => (0b00, 0), + "bic" => (0b00, 1), + "orr" => (0b01, 0), + "orn" => (0b01, 1), + "eor" => (0b10, 0), + "eon" => (0b10, 1), + "ands" => (0b11, 0), + _ => (0b11, 1), + } +} + +fn logical(i: &Insn) -> Result { + let (opc, n) = logical_bits(i.mnemonic); + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let sf = sf_of(rd).map_err(|_| form(i, "a logical operation is 32 or 64 bits"))?; + let third = i.operands().get(2).ok_or(form(i, "needs three operands"))?; + encode_logical(i, sf, opc, n, num(i, rd)?, num(i, rn)?, third) +} + +fn encode_logical( + i: &Insn, + sf: u32, + opc: u32, + n: u32, + rd: u32, + rn: u32, + third: &Operand, +) -> Result { + if let Some(v) = uimm_of(third) { + if n == 1 { + return Err(form(i, "the inverted logical operations take no immediate")); + } + let (bn, immr, imms) = encode_bitmask(v, sf == 1).ok_or_else(|| AsmError::NoEncoding { + mnemonic: i.mnemonic.to_string(), + value: v, + what: "logical immediate: a rotated run of ones in a repeated element", + })?; + return Ok((sf << 31) + | (opc << 29) + | (0b100100 << 23) + | (bn << 22) + | (immr << 16) + | (imms << 10) + | (rn << 5) + | rd); + } + let (rm, shift, amount) = match third { + Operand::Reg(r) => (*r, 0u32, 0u32), + Operand::Shifted(r, kind, amount) => ( + *r, + match kind { + Shift::Lsl => 0, + Shift::Lsr => 1, + Shift::Asr => 2, + Shift::Ror => 3, + Shift::Msl => return Err(form(i, "msl is a SIMD shift")), + }, + *amount as u32, + ), + _ => return Err(form(i, "expected a register or an immediate")), + }; + let limit = if sf == 1 { 63 } else { 31 }; + if amount as i64 > limit { + return Err(AsmError::Range { + what: "shift amount", + value: amount as i64, + low: 0, + high: limit, + }); + } + Ok((sf << 31) + | (opc << 29) + | (0b01010 << 24) + | (shift << 22) + | (n << 21) + | (num(i, rm)? << 16) + | (amount << 10) + | (rn << 5) + | rd) +} + +fn tst(i: &Insn) -> Result { + let rn = gpr_op(i, 0)?; + let sf = sf_of(rn).map_err(|_| form(i, "a test is 32 or 64 bits"))?; + let second = i.operands().get(1).ok_or(form(i, "needs two operands"))?; + encode_logical(i, sf, 0b11, 0, 31, num(i, rn)?, second) +} + +fn mvn(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let sf = sf_of(rd).map_err(|_| form(i, "a move-not is 32 or 64 bits"))?; + let second = i.operands().get(1).ok_or(form(i, "needs two operands"))?; + encode_logical(i, sf, 0b01, 1, num(i, rd)?, 31, second) +} + +fn mov(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let sf = sf_of(rd).map_err(|_| form(i, "a move is 32 or 64 bits"))?; + let second = *i.operands().get(1).ok_or(form(i, "needs two operands"))?; + if let Operand::Reg(rm) = second { + if is_sp(rd) || is_sp(rm) { + // Moving to or from the stack pointer is `add Rd, Rn, #0`. + return Ok((sf << 31) | (0b100010 << 23) | (num(i, rm)? << 5) | num(i, rd)?); + } + return encode_logical(i, sf, 0b01, 0, num(i, rd)?, 31, &second); + } + let v = uimm_of(&second).ok_or(form(i, "expected a register or an immediate"))?; + let width = if sf == 1 { 64 } else { 32 }; + let v = if sf == 1 { v } else { v & 0xffff_ffff }; + let rd_n = num(i, rd)?; + // MOVZ first, then MOVN, then the logical immediate: the same order the + // decoder's alias rules assume, so the round trip keeps the bytes. + for hw in 0..(width / 16) { + let shift = hw * 16; + if v & !(0xffffu64 << shift) == 0 { + let imm16 = ((v >> shift) & 0xffff) as u32; + return Ok((sf << 31) + | (0b10 << 29) + | (0b100101 << 23) + | (hw << 21) + | (imm16 << 5) + | rd_n); + } + } + let inv = if sf == 1 { !v } else { (!v) & 0xffff_ffff }; + for hw in 0..(width / 16) { + let shift = hw * 16; + if inv & !(0xffffu64 << shift) == 0 { + let imm16 = ((inv >> shift) & 0xffff) as u32; + if imm16 == 0 && hw != 0 { + continue; + } + return Ok((sf << 31) | (0b100101 << 23) | (hw << 21) | (imm16 << 5) | rd_n); + } + } + if let Some((bn, immr, imms)) = encode_bitmask(v, sf == 1) { + return Ok((sf << 31) + | (0b01 << 29) + | (0b100100 << 23) + | (bn << 22) + | (immr << 16) + | (imms << 10) + | (31 << 5) + | rd_n); + } + Err(AsmError::NoEncoding { + mnemonic: i.mnemonic.to_string(), + value: v, + what: "value one move-wide or logical immediate can build; \ + build it with movz and movk", + }) +} + +fn move_wide(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let sf = sf_of(rd).map_err(|_| form(i, "a move-wide is 32 or 64 bits"))?; + let v = imm_of(i.operands().get(1).ok_or(form(i, "needs an immediate"))?) + .ok_or(form(i, "expected an immediate"))?; + let imm16 = unsigned_fits(v, 16, "move-wide immediate")?; + let mut hw = 0u32; + if let Some(Operand::ShiftOp(Shift::Lsl, amount)) = i.operands().get(2) { + if amount % 16 != 0 { + return Err(form(i, "a move-wide shifts by 0, 16, 32 or 48")); + } + hw = *amount as u32 / 16; + } + if hw >= if sf == 1 { 4 } else { 2 } { + return Err(AsmError::Range { + what: "move-wide shift", + value: (hw * 16) as i64, + low: 0, + high: if sf == 1 { 48 } else { 16 }, + }); + } + let opc = match i.mnemonic { + "movn" => 0b00, + "movz" => 0b10, + _ => 0b11, + }; + Ok((sf << 31) | (opc << 29) | (0b100101 << 23) | (hw << 21) | (imm16 << 5) | num(i, rd)?) +} + +fn pc_rel(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let target = addr_of(i.operands().get(1).ok_or(form(i, "needs a target"))?) + .ok_or(form(i, "expected an address"))?; + let page = i.mnemonic == "adrp"; + let delta = if page { + let from = i.addr.get() & !0xfff; + if target.get() & 0xfff != 0 { + return Err(AsmError::Unaligned { + what: "adrp target", + value: target.get() as i64, + align: 4096, + }); + } + (target.get().wrapping_sub(from) as i64) >> 12 + } else { + target.get().wrapping_sub(i.addr.get()) as i64 + }; + if !(-(1i64 << 20)..(1i64 << 20)).contains(&delta) { + let scale = if page { 4096 } else { 1 }; + return Err(AsmError::BranchRange { + mnemonic: i.mnemonic.to_string(), + from: i.addr.get(), + to: target.get(), + low: -(1i64 << 20) * scale, + high: ((1i64 << 20) - 1) * scale, + }); + } + let imm = delta as u32 & 0x1f_ffff; + Ok((u32::from(page) << 31) + | ((imm & 3) << 29) + | (0b10000 << 24) + | ((imm >> 2) << 5) + | num(i, rd)?) +} + +fn branch_imm(i: &Insn) -> Result { + let target = addr_of(i.operands().first().ok_or(form(i, "needs a target"))?) + .ok_or(form(i, "expected an address"))?; + let imm = displacement(i, target, 26)?; + Ok((u32::from(i.mnemonic == "bl") << 31) | (0b00101 << 26) | imm) +} + +fn b_cond(i: &Insn) -> Result { + let cond = condition(&i.mnemonic[2..]).ok_or(form(i, "unknown condition"))? as u32; + let target = addr_of(i.operands().first().ok_or(form(i, "needs a target"))?) + .ok_or(form(i, "expected an address"))?; + let imm = displacement(i, target, 19)?; + Ok((0b0101010 << 25) | (imm << 5) | cond) +} + +fn cmp_branch(i: &Insn) -> Result { + let rt = gpr_op(i, 0)?; + let sf = sf_of(rt).map_err(|_| form(i, "a compare-and-branch is 32 or 64 bits"))?; + let target = addr_of(i.operands().get(1).ok_or(form(i, "needs a target"))?) + .ok_or(form(i, "expected an address"))?; + let imm = displacement(i, target, 19)?; + Ok((sf << 31) + | (0b011010 << 25) + | (u32::from(i.mnemonic == "cbnz") << 24) + | (imm << 5) + | num(i, rt)?) +} + +fn test_branch(i: &Insn) -> Result { + let rt = gpr_op(i, 0)?; + let bit = imm_of(i.operands().get(1).ok_or(form(i, "needs a bit number"))?) + .ok_or(form(i, "expected a bit number"))?; + let bit = unsigned_fits(bit, 6, "bit number")?; + let target = addr_of(i.operands().get(2).ok_or(form(i, "needs a target"))?) + .ok_or(form(i, "expected an address"))?; + let imm = displacement(i, target, 14)?; + Ok(((bit >> 5) << 31) + | (0b011011 << 25) + | (u32::from(i.mnemonic == "tbnz") << 24) + | ((bit & 31) << 19) + | (imm << 5) + | num(i, rt)?) +} + +fn branch_reg(i: &Insn) -> Result { + let (opc, default) = match i.mnemonic { + "br" => (0b0000u32, None), + "blr" => (0b0001, None), + _ => (0b0010, Some(30u32)), + }; + let rn = match i.operands().first() { + Some(Operand::Reg(r)) => num(i, *r)?, + None => default.ok_or(form(i, "needs a register"))?, + _ => return Err(form(i, "expected a register")), + }; + Ok((0b1101011 << 25) | (opc << 21) | (0b11111 << 16) | (rn << 5)) +} + +fn exception(i: &Insn) -> Result { + let (opc, ll) = match i.mnemonic { + "svc" => (0b000u32, 0b01u32), + "hvc" => (0b000, 0b10), + "smc" => (0b000, 0b11), + "brk" => (0b001, 0b00), + _ => (0b010, 0b00), + }; + let v = match i.operands().first() { + Some(op) => imm_of(op).ok_or(form(i, "expected an immediate"))?, + None => 0, + }; + let imm16 = unsigned_fits(v, 16, "exception immediate")?; + Ok((0b1101_0100 << 24) | (opc << 21) | (imm16 << 5) | ll) +} + +fn hint(i: &Insn) -> Result { + let hint = match i.mnemonic { + "nop" => 0u32, + "yield" => 1, + "wfe" => 2, + "wfi" => 3, + "sev" => 4, + "sevl" => 5, + // The pointer-authentication hints. They open and close nearly every + // function a modern toolchain emits, so a patch that pastes a prologue + // back needs them even though nothing here authenticates anything. + "xpaclri" => 7, + "pacia1716" => 8, + "pacib1716" => 10, + "autia1716" => 12, + "autib1716" => 14, + "paciaz" => 24, + "paciasp" => 25, + "pacibz" => 26, + "pacibsp" => 27, + "autiaz" => 28, + "autiasp" => 29, + "autibz" => 30, + "autibsp" => 31, + _ => { + let v = imm_of(i.operands().first().ok_or(form(i, "needs a hint number"))?) + .ok_or(form(i, "expected an immediate"))?; + unsigned_fits(v, 7, "hint number")? + } + }; + Ok(0xd503_201f | ((hint >> 3) << 8) | ((hint & 7) << 5)) +} + +fn bti(i: &Insn) -> Result { + let op2 = match i.operands().first() { + None => 0b100u32, + Some(Operand::Name("c")) => 0b010, + Some(Operand::Name("j")) => 0b100, + Some(Operand::Name("jc")) => 0b110, + _ => return Err(form(i, "bti takes c, j or jc")), + }; + Ok(0xd503_201f | (0b0100 << 8) | (op2 << 5)) +} + +fn barrier(i: &Insn) -> Result { + let op2 = match i.mnemonic { + "dsb" => 0b100u32, + "dmb" => 0b101, + _ => 0b110, + }; + let crm = match i.operands().first() { + None => 15, + Some(Operand::Name(n)) => barrier_crm(n).ok_or(form(i, "unknown barrier option"))?, + Some(op) => { + let v = imm_of(op).ok_or(form(i, "expected a barrier option"))?; + unsigned_fits(v, 4, "barrier option")? + } + }; + Ok(0xd503_3000 | (crm << 8) | (op2 << 5) | 0b11111) +} + +fn udf(i: &Insn) -> Result { + let v = match i.operands().first() { + Some(op) => imm_of(op).ok_or(form(i, "expected an immediate"))?, + None => 0, + }; + unsigned_fits(v, 16, "udf immediate") +} + +// --------------------------------------------------------- loads and stores + +/// `size`, `opc`, the SIMD bit, and the bytes one access transfers. +struct LsForm { + size: u32, + opc: u32, + v: u32, + bytes: u64, +} + +/// The unscaled mnemonics are the scaled ones with a different immediate +/// field, so they share every other decision. +fn unscaled_base(m: &str) -> Option<&'static str> { + Some(match m { + "ldur" => "ldr", + "stur" => "str", + "ldurb" => "ldrb", + "sturb" => "strb", + "ldurh" => "ldrh", + "sturh" => "strh", + "ldursb" => "ldrsb", + "ldursh" => "ldrsh", + "ldursw" => "ldrsw", + "prfum" => "prfm", + _ => return None, + }) +} + +fn ls_form(i: &Insn, m: &str, rt: Option) -> Result { + let load = !m.starts_with("st"); + if m == "prfm" { + return Ok(LsForm { + size: 0b11, + opc: 0b10, + v: 0, + bytes: 8, + }); + } + let rt = rt.ok_or(form(i, "needs a register operand"))?; + if rt.class == RegClass::Vec { + let (size, wide, bytes) = match rt.width { + Width::W8 => (0b00u32, false, 1u64), + Width::W16 => (0b01, false, 2), + Width::W32 => (0b10, false, 4), + Width::W64 => (0b11, false, 8), + Width::W128 => (0b00, true, 16), + }; + let opc = (u32::from(wide) << 1) | u32::from(load); + return Ok(LsForm { + size, + opc, + v: 1, + bytes, + }); + } + let wide = rt.width == Width::W64; + Ok(match m { + "strb" => LsForm { + size: 0, + opc: 0b00, + v: 0, + bytes: 1, + }, + "ldrb" => LsForm { + size: 0, + opc: 0b01, + v: 0, + bytes: 1, + }, + "ldrsb" => LsForm { + size: 0, + opc: if wide { 0b10 } else { 0b11 }, + v: 0, + bytes: 1, + }, + "strh" => LsForm { + size: 1, + opc: 0b00, + v: 0, + bytes: 2, + }, + "ldrh" => LsForm { + size: 1, + opc: 0b01, + v: 0, + bytes: 2, + }, + "ldrsh" => LsForm { + size: 1, + opc: if wide { 0b10 } else { 0b11 }, + v: 0, + bytes: 2, + }, + "ldrsw" => LsForm { + size: 0b10, + opc: 0b10, + v: 0, + bytes: 4, + }, + "str" => LsForm { + size: if wide { 0b11 } else { 0b10 }, + opc: 0b00, + v: 0, + bytes: if wide { 8 } else { 4 }, + }, + "ldr" => LsForm { + size: if wide { 0b11 } else { 0b10 }, + opc: 0b01, + v: 0, + bytes: if wide { 8 } else { 4 }, + }, + _ => return Err(form(i, "no encoder for this load or store")), + }) +} + +fn load_store(i: &Insn) -> Result { + let forced_unscaled = unscaled_base(i.mnemonic); + let m = forced_unscaled.unwrap_or(i.mnemonic); + let first = i.operands().first().ok_or(form(i, "needs operands"))?; + let (rt_reg, rt_num) = match first { + Operand::Reg(r) => (Some(*r), num_any(i, *r)?), + Operand::Name(n) => ( + None, + prefetch_num(n).ok_or(form(i, "unknown prefetch operation"))?, + ), + op => { + // `prfm #0x18, [x0]` for the unnamed operations. + let v = imm_of(op).ok_or(form(i, "expected a register"))?; + (None, unsigned_fits(v, 5, "prefetch operation")?) + } + }; + let f = ls_form(i, m, rt_reg)?; + let second = i.operands().get(1).ok_or(form(i, "needs an address"))?; + + if let Some(target) = match second { + Operand::Addr(_) => addr_of(second), + _ => None, + } { + // Load register (literal). + if forced_unscaled.is_some() { + return Err(form(i, "an unscaled load has no literal form")); + } + let (opc, v) = match (m, rt_reg.map(|r| (r.class, r.width))) { + ("prfm", _) => (0b11u32, 0u32), + ("ldrsw", _) => (0b10, 0), + ("ldr", Some((RegClass::Vec, Width::W32))) => (0b00, 1), + ("ldr", Some((RegClass::Vec, Width::W64))) => (0b01, 1), + ("ldr", Some((RegClass::Vec, Width::W128))) => (0b10, 1), + ("ldr", Some((_, Width::W32))) => (0b00, 0), + ("ldr", Some((_, Width::W64))) => (0b01, 0), + _ => return Err(form(i, "this mnemonic has no pc-relative form")), + }; + let imm = displacement(i, target, 19)?; + return Ok((opc << 30) | (0b011 << 27) | (v << 26) | (imm << 5) | rt_num); + } + + let mem = match second { + Operand::Mem(m) => *m, + _ => return Err(form(i, "expected a memory operand")), + }; + let rn = mem.base.ok_or(form(i, "a memory operand needs a base"))?; + let rn = num(i, rn)?; + + if let Some((index, ext, amount)) = mem.index { + if mem.mode != AddrMode::Offset { + return Err(form(i, "an indexed address takes no writeback")); + } + let option = match ext { + Extend::Uxtw => 0b010u32, + Extend::Lsl | Extend::LslZero => 0b011, + Extend::Sxtw => 0b110, + Extend::Sxtx => 0b111, + _ => return Err(form(i, "an index extends by uxtw, lsl, sxtw or sxtx")), + }; + let scale = f.bytes.trailing_zeros(); + let s = if ext == Extend::LslZero { + 1 + } else if amount == 0 { + 0 + } else if amount as u32 == scale { + 1 + } else { + return Err(AsmError::Range { + what: "index shift, which is either zero or the access size", + value: amount as i64, + low: 0, + high: scale as i64, + }); + }; + return Ok((f.size << 30) + | (0b111 << 27) + | (f.v << 26) + | (f.opc << 22) + | (1 << 21) + | (num(i, index)? << 16) + | (option << 13) + | (s << 12) + | (0b10 << 10) + | (rn << 5) + | rt_num); + } + + let disp = mem.disp; + let scaled_ok = mem.mode == AddrMode::Offset + && forced_unscaled.is_none() + && disp >= 0 + && f.bytes != 0 + && disp % f.bytes as i64 == 0 + && disp / f.bytes as i64 <= 0xfff; + if scaled_ok { + let imm12 = (disp / f.bytes as i64) as u32; + return Ok((f.size << 30) + | (0b111 << 27) + | (f.v << 26) + | (1 << 24) + | (f.opc << 22) + | (imm12 << 10) + | (rn << 5) + | rt_num); + } + let imm9 = fits(disp, 9, "load or store offset")?; + let mode_bits = match mem.mode { + AddrMode::Offset => 0b00u32, + AddrMode::PostIndex => 0b01, + AddrMode::PreIndex => 0b11, + }; + Ok((f.size << 30) + | (0b111 << 27) + | (f.v << 26) + | (f.opc << 22) + | (imm9 << 12) + | (mode_bits << 10) + | (rn << 5) + | rt_num) +} + +/// A register number that also accepts the vector bank. +fn num_any(i: &Insn, r: Reg) -> Result { + match r.class { + RegClass::Vec => Ok(r.num as u32 & 31), + _ => num(i, r), + } +} + +fn load_store_pair(i: &Insn) -> Result { + let rt = match i.operands().first() { + Some(Operand::Reg(r)) => *r, + _ => return Err(form(i, "needs two register operands")), + }; + let rt2 = match i.operands().get(1) { + Some(Operand::Reg(r)) => *r, + _ => return Err(form(i, "needs two register operands")), + }; + let mem = match i.operands().get(2) { + Some(Operand::Mem(m)) => *m, + _ => return Err(form(i, "expected a memory operand")), + }; + let load = i.mnemonic.starts_with("ld"); + let (opc, v, bytes) = if rt.class == RegClass::Vec { + match rt.width { + Width::W32 => (0b00u32, 1u32, 4u64), + Width::W64 => (0b01, 1, 8), + Width::W128 => (0b10, 1, 16), + _ => return Err(form(i, "a vector pair is s, d or q")), + } + } else if i.mnemonic == "ldpsw" { + (0b01, 0, 4) + } else { + match rt.width { + Width::W32 => (0b00, 0, 4), + Width::W64 => (0b10, 0, 8), + _ => return Err(form(i, "a register pair is 32 or 64 bits")), + } + }; + let mode_bits = match (i.mnemonic, mem.mode) { + ("ldnp" | "stnp", AddrMode::Offset) => 0b00u32, + ("ldnp" | "stnp", _) => return Err(form(i, "a non-temporal pair has no writeback")), + (_, AddrMode::PostIndex) => 0b01, + (_, AddrMode::Offset) => 0b10, + (_, AddrMode::PreIndex) => 0b11, + }; + if mem.index.is_some() { + return Err(form(i, "a pair takes no index register")); + } + if mem.disp % bytes as i64 != 0 { + return Err(AsmError::Unaligned { + what: "pair offset", + value: mem.disp, + align: bytes as u32, + }); + } + let imm7 = fits( + mem.disp / bytes as i64, + 7, + "pair offset in units of the access size", + )?; + let rn = num(i, mem.base.ok_or(form(i, "a memory operand needs a base"))?)?; + Ok((opc << 30) + | (0b101 << 27) + | (v << 26) + | (mode_bits << 23) + | (u32::from(load) << 22) + | (imm7 << 15) + | (num_any(i, rt2)? << 10) + | (rn << 5) + | num_any(i, rt)?) +} + +// ------------------------------------------------------- bitfields and math + +fn bfm(sf: u32, opc: u32, immr: u32, imms: u32, rn: u32, rd: u32) -> u32 { + (sf << 31) + | (opc << 29) + | (0b100110 << 23) + | (sf << 22) + | (immr << 16) + | (imms << 10) + | (rn << 5) + | rd +} + +fn shift(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let sf = sf_of(rd).map_err(|_| form(i, "a shift is 32 or 64 bits"))?; + let width = if sf == 1 { 64u32 } else { 32 }; + let third = *i.operands().get(2).ok_or(form(i, "needs three operands"))?; + if let Operand::Reg(rm) = third { + let opcode = match i.mnemonic { + "lsl" => 0b001000u32, + "lsr" => 0b001001, + "asr" => 0b001010, + _ => 0b001011, + }; + return Ok((sf << 31) + | (0b11010110 << 21) + | (num(i, rm)? << 16) + | (opcode << 10) + | (num(i, rn)? << 5) + | num(i, rd)?); + } + let v = imm_of(&third).ok_or(form(i, "expected a shift amount"))?; + let n = unsigned_fits(v, 6, "shift amount")?; + if n >= width { + return Err(AsmError::Range { + what: "shift amount", + value: v, + low: 0, + high: width as i64 - 1, + }); + } + let (rd, rn) = (num(i, rd)?, num(i, rn)?); + Ok(match i.mnemonic { + "lsl" => bfm(sf, 0b10, (width - n) % width, width - 1 - n, rn, rd), + "lsr" => bfm(sf, 0b10, n, width - 1, rn, rd), + "asr" => bfm(sf, 0b00, n, width - 1, rn, rd), + // A rotate is an extract from a register and itself. + _ => (sf << 31) | (0b100111 << 23) | (sf << 22) | (rn << 16) | (n << 10) | (rn << 5) | rd, + }) +} + +fn dp2(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let rm = gpr_op(i, 2)?; + let sf = sf_of(rd).map_err(|_| form(i, "this operation is 32 or 64 bits"))?; + let opcode = match i.mnemonic { + "udiv" => 0b000010u32, + "sdiv" => 0b000011, + "lslv" => 0b001000, + "lsrv" => 0b001001, + "asrv" => 0b001010, + _ => 0b001011, + }; + Ok((sf << 31) + | (0b11010110 << 21) + | (num(i, rm)? << 16) + | (opcode << 10) + | (num(i, rn)? << 5) + | num(i, rd)?) +} + +fn dp1(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let sf = sf_of(rd).map_err(|_| form(i, "this operation is 32 or 64 bits"))?; + let opcode = match (i.mnemonic, sf) { + ("rbit", _) => 0b000000u32, + ("rev16", _) => 0b000001, + ("rev32", 1) => 0b000010, + ("rev", 0) => 0b000010, + ("rev", 1) => 0b000011, + ("clz", _) => 0b000100, + ("cls", _) => 0b000101, + _ => return Err(form(i, "this width has no such reverse")), + }; + Ok((sf << 31) + | (1 << 30) + | (0b11010110 << 21) + | (opcode << 10) + | (num(i, rn)? << 5) + | num(i, rd)?) +} + +fn bfm_raw(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let sf = sf_of(rd).map_err(|_| form(i, "a bitfield move is 32 or 64 bits"))?; + let immr = imm_of(i.operands().get(2).ok_or(form(i, "needs immr"))?) + .ok_or(form(i, "expected immr"))?; + let imms = imm_of(i.operands().get(3).ok_or(form(i, "needs imms"))?) + .ok_or(form(i, "expected imms"))?; + let opc = match i.mnemonic { + "sbfm" => 0b00u32, + "bfm" => 0b01, + _ => 0b10, + }; + Ok(bfm( + sf, + opc, + unsigned_fits(immr, 6, "immr")?, + unsigned_fits(imms, 6, "imms")?, + num(i, rn)?, + num(i, rd)?, + )) +} + +fn bitfield(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let sf = sf_of(rd).map_err(|_| form(i, "a bitfield operation is 32 or 64 bits"))?; + let regsize = if sf == 1 { 64u32 } else { 32 }; + let lsb = imm_of(i.operands().get(2).ok_or(form(i, "needs a bit position"))?) + .ok_or(form(i, "expected a bit position"))?; + let width = imm_of(i.operands().get(3).ok_or(form(i, "needs a width"))?) + .ok_or(form(i, "expected a width"))?; + let lsb = unsigned_fits(lsb, 6, "bit position")?; + if lsb >= regsize { + return Err(AsmError::Range { + what: "bit position", + value: lsb as i64, + low: 0, + high: regsize as i64 - 1, + }); + } + if width < 1 || width as u32 > regsize - lsb { + return Err(AsmError::Range { + what: "bitfield width", + value: width, + low: 1, + high: (regsize - lsb) as i64, + }); + } + let width = width as u32; + let opc = match i.mnemonic { + "sbfx" | "sbfiz" => 0b00u32, + "bfxil" | "bfi" => 0b01, + _ => 0b10, + }; + let insert = matches!(i.mnemonic, "sbfiz" | "ubfiz" | "bfi"); + let (immr, imms) = if insert { + ((regsize - lsb) % regsize, width - 1) + } else { + (lsb, lsb + width - 1) + }; + Ok(bfm(sf, opc, immr, imms, num(i, rn)?, num(i, rd)?)) +} + +fn extend_op(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let sf = sf_of(rd).map_err(|_| form(i, "an extend is 32 or 64 bits"))?; + let (opc, imms) = match i.mnemonic { + "sxtb" => (0b00u32, 7u32), + "sxth" => (0b00, 15), + "sxtw" => (0b00, 31), + "uxtb" => (0b10, 7), + _ => (0b10, 15), + }; + if i.mnemonic == "sxtw" && sf == 0 { + return Err(form(i, "sxtw writes a 64-bit register")); + } + if matches!(i.mnemonic, "uxtb" | "uxth") && sf == 1 { + return Err(form( + i, + "there is no 64-bit uxtb or uxth: writing a w register already zero extends", + )); + } + Ok(bfm(sf, opc, 0, imms, num(i, rn)?, num(i, rd)?)) +} + +fn multiply(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let rm = gpr_op(i, 2)?; + let sf = sf_of(rd).map_err(|_| form(i, "a multiply is 32 or 64 bits"))?; + let (op31, o0, long, needs_ra) = match i.mnemonic { + "madd" => (0b000u32, 0u32, false, true), + "msub" => (0b000, 1, false, true), + "mul" => (0b000, 0, false, false), + "mneg" => (0b000, 1, false, false), + "smaddl" => (0b001, 0, true, true), + "smsubl" => (0b001, 1, true, true), + "smull" => (0b001, 0, true, false), + "smnegl" => (0b001, 1, true, false), + "umaddl" => (0b101, 0, true, true), + "umsubl" => (0b101, 1, true, true), + "umull" => (0b101, 0, true, false), + "umnegl" => (0b101, 1, true, false), + "smulh" => (0b010, 0, false, false), + _ => (0b110, 0, false, false), + }; + if long && sf == 0 { + return Err(form(i, "a widening multiply writes a 64-bit register")); + } + let ra = if needs_ra { + match i.operands().get(3) { + Some(Operand::Reg(r)) => num(i, *r)?, + _ => return Err(form(i, "needs an accumulator register")), + } + } else { + 31 + }; + Ok((sf << 31) + | (0b11011 << 24) + | (op31 << 21) + | (num(i, rm)? << 16) + | (o0 << 15) + | (ra << 10) + | (num(i, rn)? << 5) + | num(i, rd)?) +} + +fn cond_of(i: &Insn, n: usize) -> Result { + match i.operands().get(n) { + Some(Operand::Cond(c)) => Ok(c.0 as u32 & 15), + _ => Err(form(i, "expected a condition code")), + } +} + +fn csel_bits(m: &str) -> (u32, u32) { + match m { + "csel" => (0, 0), + "csinc" | "cinc" | "cset" => (0, 1), + "csinv" | "cinv" | "csetm" => (1, 0), + _ => (1, 1), + } +} + +fn cond_select(sf: u32, op: u32, o2: u32, rm: u32, cond: u32, rn: u32, rd: u32) -> u32 { + (sf << 31) + | (op << 30) + | (0b11010100 << 21) + | (rm << 16) + | (cond << 12) + | (o2 << 10) + | (rn << 5) + | rd +} + +fn csel(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let rm = gpr_op(i, 2)?; + let sf = sf_of(rd).map_err(|_| form(i, "a conditional select is 32 or 64 bits"))?; + let (op, o2) = csel_bits(i.mnemonic); + Ok(cond_select( + sf, + op, + o2, + num(i, rm)?, + cond_of(i, 3)?, + num(i, rn)?, + num(i, rd)?, + )) +} + +fn cset(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let sf = sf_of(rd).map_err(|_| form(i, "a cset is 32 or 64 bits"))?; + let cond = cond_of(i, 1)?; + if cond >= 14 { + return Err(form(i, "cset has no always or never condition")); + } + let (op, o2) = csel_bits(i.mnemonic); + Ok(cond_select(sf, op, o2, 31, cond ^ 1, 31, num(i, rd)?)) +} + +fn cinc(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let sf = sf_of(rd).map_err(|_| form(i, "this operation is 32 or 64 bits"))?; + let cond = cond_of(i, 2)?; + if cond >= 14 { + return Err(form(i, "this alias has no always or never condition")); + } + let (op, o2) = csel_bits(i.mnemonic); + let rn = num(i, rn)?; + Ok(cond_select(sf, op, o2, rn, cond ^ 1, rn, num(i, rd)?)) +} + +fn ccmp(i: &Insn) -> Result { + let rn = gpr_op(i, 0)?; + let sf = sf_of(rn).map_err(|_| form(i, "a conditional compare is 32 or 64 bits"))?; + let second = *i + .operands() + .get(1) + .ok_or(form(i, "needs a second operand"))?; + let (rm, imm_form) = match second { + Operand::Reg(r) => (num(i, r)?, 0u32), + other => { + let v = imm_of(&other).ok_or(form(i, "expected a register or an immediate"))?; + (unsigned_fits(v, 5, "conditional compare immediate")?, 1) + } + }; + let nzcv = imm_of(i.operands().get(2).ok_or(form(i, "needs flags"))?) + .ok_or(form(i, "expected the flag value"))?; + let nzcv = unsigned_fits(nzcv, 4, "nzcv")?; + let cond = cond_of(i, 3)?; + Ok((sf << 31) + | (u32::from(i.mnemonic == "ccmp") << 30) + | (1 << 29) + | (0b11010010 << 21) + | (rm << 16) + | (cond << 12) + | (imm_form << 11) + | (num(i, rn)? << 5) + | nzcv) +} + +fn adc(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let rm = gpr_op(i, 2)?; + let sf = sf_of(rd).map_err(|_| form(i, "a carry operation is 32 or 64 bits"))?; + let sub = u32::from(i.mnemonic.starts_with("sbc")); + let s = u32::from(i.mnemonic.ends_with('s')); + Ok(carry(sf, sub, s, num(i, rm)?, num(i, rn)?, num(i, rd)?)) +} + +fn ngc(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rm = gpr_op(i, 1)?; + let sf = sf_of(rd).map_err(|_| form(i, "a carry negate is 32 or 64 bits"))?; + let s = u32::from(i.mnemonic.ends_with('s')); + Ok(carry(sf, 1, s, num(i, rm)?, 31, num(i, rd)?)) +} + +fn carry(sf: u32, sub: u32, s: u32, rm: u32, rn: u32, rd: u32) -> u32 { + (sf << 31) | (sub << 30) | (s << 29) | (0b11010000 << 21) | (rm << 16) | (rn << 5) | rd +} + +fn extr(i: &Insn) -> Result { + let rd = gpr_op(i, 0)?; + let rn = gpr_op(i, 1)?; + let rm = gpr_op(i, 2)?; + let sf = sf_of(rd).map_err(|_| form(i, "an extract is 32 or 64 bits"))?; + let v = imm_of(i.operands().get(3).ok_or(form(i, "needs a bit position"))?) + .ok_or(form(i, "expected a bit position"))?; + let imms = unsigned_fits(v, 6, "extract position")?; + if sf == 0 && imms >= 32 { + return Err(AsmError::Range { + what: "extract position", + value: v, + low: 0, + high: 31, + }); + } + Ok((sf << 31) + | (0b100111 << 23) + | (sf << 22) + | (num(i, rm)? << 16) + | (imms << 10) + | (num(i, rn)? << 5) + | num(i, rd)?) +} + +/// Four bytes of `nop`, the only padding A64 needs. +pub(crate) const NOP: u32 = 0xd503_201f; + +#[cfg(test)] +mod tests { + use super::*; + + /// Text in, one word out. Every expectation here was produced by + /// `aarch64-linux-gnu-as` and read back with `objdump -d`, so the table is + /// a record of what the toolchain says rather than of what this file does. + const CASES: &[(&str, u32)] = &[ + // Data processing, register and immediate. + ("add\tx0, x1, x2", 0x8b02_0020), + ("add\tw0, w1, w2, lsl #3", 0x0b02_0c20), + ("add\tx0, x1, #0x1, lsl #12", 0x9140_0420), + ("sub\tsp, sp, #0x10", 0xd100_43ff), + ("add\tx0, sp, w1, uxtw #2", 0x8b21_4be0), + ("cmp\tw0, #0x1", 0x7100_041f), + ("neg\tx0, x1", 0xcb01_03e0), + ("mvn\tw0, w1", 0x2a21_03e0), + ("tst\tx0, #0xff", 0xf240_1c1f), + ("and\tx0, x1, #0xff", 0x9240_1c20), + ("orr\tw0, w1, w2, ror #7", 0x2ac2_1c20), + ("adc\tx0, x1, x2", 0x9a02_0020), + ("extr\tx0, x1, x2, #5", 0x93c2_1420), + // The moves, including the aliases and what they lower to. + ("mov\tx0, x1", 0xaa01_03e0), + ("mov\tsp, x0", 0x9100_001f), + ("mov\tw0, #0x1234", 0x5282_4680), + ("mov\tx0, #0xffffffffffffffff", 0x9280_0000), + ("mov\tw0, #0xffff0000", 0x52bf_ffe0), + ("movk\tw0, #0x5a5a, lsl #16", 0x72ab_4b40), + // Loads and stores in each addressing mode the printer emits. + ("ldr\tx0, [x1, #16]", 0xf940_0820), + ("ldr\tw0, [x1, x2, lsl #2]", 0xb862_7820), + ("ldrb\tw0, [x1, #1]", 0x3940_0420), + ("ldrsw\tx0, [x1, #4]", 0xb980_0420), + ("str\tx0, [x1], #16", 0xf801_0420), + ("str\tx0, [x1, #-8]!", 0xf81f_8c20), + ("ldur\tx0, [x1, #-1]", 0xf85f_f020), + ("stp\tx29, x30, [sp, #-16]!", 0xa9bf_7bfd), + ("ldp\tx29, x30, [sp], #16", 0xa8c1_7bfd), + ("prfm\tpldl1keep, [x0, #8]", 0xf980_0400), + // Branches and the system odds and ends. + ("nop", 0xd503_201f), + ("ret", 0xd65f_03c0), + ("ret\tx1", 0xd65f_0020), + ("blr\tx2", 0xd63f_0040), + ("br\tx3", 0xd61f_0060), + ("svc\t#0x0", 0xd400_0001), + ("brk\t#0x1", 0xd420_0020), + ("dmb\tish", 0xd503_3bbf), + ("isb\tsy", 0xd503_3fdf), + ("bti\tc", 0xd503_245f), + ("paciasp", 0xd503_233f), + ("autiasp", 0xd503_23bf), + ("xpaclri", 0xd503_20ff), + // Shifts, bitfields and extends. + ("lsl\tw0, w1, #3", 0x531d_7020), + ("lsr\tx0, x1, #5", 0xd345_fc20), + ("asr\tw0, w1, w2", 0x1ac2_2820), + ("ubfx\tx0, x1, #4, #8", 0xd344_2c20), + ("sbfiz\tw0, w1, #2, #6", 0x131e_1420), + ("sxtb\tx0, w1", 0x9340_1c20), + ("uxth\tw0, w1", 0x5300_3c20), + // Multiply, divide and the conditional selects. + ("mul\tx0, x1, x2", 0x9b02_7c20), + ("madd\tw0, w1, w2, w3", 0x1b02_0c20), + ("smull\tx0, w1, w2", 0x9b22_7c20), + ("udiv\tx0, x1, x2", 0x9ac2_0820), + ("csel\tx0, x1, x2, eq", 0x9a82_0020), + ("cset\tw0, ne", 0x1a9f_07e0), + ("cinc\tx0, x1, lt", 0x9a81_a420), + ("ccmp\tx0, #0x1, #0x2, eq", 0xfa41_0802), + ("rbit\tx0, x1", 0xdac0_0020), + ("clz\tw0, w1", 0x5ac0_1020), + ("rev\tx0, x1", 0xdac0_0c20), + ]; + + fn asm(text: &str, at: u64) -> u32 { + let e = crate::assemble(&e5r_core::Arch::AArch64, text, Addr(at)) + .unwrap_or_else(|e| panic!("{text}: {e}")); + u32::from_le_bytes(e.bytes().try_into().expect("four bytes")) + } + + #[test] + fn the_mnemonic_table_is_sorted_and_unique() { + for w in MNEMONICS.windows(2) { + assert!(w[0] < w[1], "{} then {}", w[0], w[1]); + } + for w in NAMES.windows(2) { + assert!(w[0] < w[1], "{} then {}", w[0], w[1]); + } + } + + #[test] + fn the_encodings_are_the_toolchains() { + for (text, want) in CASES { + assert_eq!(asm(text, 0x1000), *want, "{text}"); + } + } + + /// Everything above, back through the decoder: the encoder and the decoder + /// have to be inverses, not merely both plausible. + #[test] + fn every_case_decodes_to_what_was_written() { + for (text, _) in CASES { + let bytes = asm(text, 0x1000).to_le_bytes(); + let back = e5r_arch::aarch64::decode(&bytes, Addr(0x1000)) + .unwrap_or_else(|| panic!("{text} does not decode")); + let printed = + e5r_arch::aarch64::format(&back, e5r_arch::aarch64::text::Style { objdump: false }); + assert_eq!(&printed, text, "{text}"); + } + } + + #[test] + fn branches_are_relative_to_where_the_instruction_sits() { + assert_eq!(asm("b\t0x1004", 0x1000), 0x1400_0001); + assert_eq!(asm("bl\t0xffc", 0x1000), 0x97ff_ffff); + assert_eq!(asm("b.eq\t0x1008", 0x1000), 0x5400_0040); + assert_eq!(asm("cbz\tx0, 0x1008", 0x1000), 0xb400_0040); + assert_eq!(asm("tbnz\tw0, #3, 0x1008", 0x1000), 0x3718_0040); + // adr is byte relative and adrp is page relative, from the page the + // instruction is on rather than from the instruction. + assert_eq!(asm("adr\tx0, 0x1008", 0x1000), 0x1000_0040); + // Two pages up: the low two bits of the page count live at the top + // of the word, which is the field layout most likely to be got + // backwards and the reason this case is written out. + assert_eq!(asm("adrp\tx0, 0x3000", 0x1000), 0xd000_0000); + } + + /// Spellings the printer never emits but a person writes, including the + /// move-wide instructions the `mov` alias hides. These are checked for + /// bytes only, since by construction they print back as the alias. + #[test] + fn the_spellings_a_person_writes_encode_the_same() { + // The shifted add immediate: printed with the shift, written whole. + assert_eq!(asm("add\tx0, x1, #0x1000", 0x1000), 0x9140_0420); + assert_eq!(asm("add\tx0, x1, #0x1, lsl #12", 0x1000), 0x9140_0420); + // The all-ones move: printed as the bit pattern, written as -1. + assert_eq!(asm("mov\tx0, #-0x1", 0x1000), 0x9280_0000); + assert_eq!(asm("mov\tx0, #0xffffffffffffffff", 0x1000), 0x9280_0000); + // What `mov` lowers to, written out. + assert_eq!(asm("movz\tx0, #0x1234, lsl #16", 0x1000), 0xd2a2_4680); + assert_eq!(asm("mov\tx0, #0x12340000", 0x1000), 0xd2a2_4680); + assert_eq!(asm("movn\tx0, #0x0", 0x1000), 0x9280_0000); + assert_eq!(asm("movz\tw0, #0x1234", 0x1000), 0x5282_4680); + // The barrier option is `sy` whether it is written or left out. + assert_eq!(asm("isb", 0x1000), 0xd503_3fdf); + // `sal` has no A64 spelling, but `lsl` by a register does, and the + // alias and the raw instruction are the same bytes. + assert_eq!( + asm("lsl\tw0, w1, w2", 0x1000), + asm("lslv\tw0, w1, w2", 0x1000) + ); + assert_eq!( + asm("asr\tx0, x1, #5", 0x1000), + asm("sbfm\tx0, x1, #5, #63", 0x1000) + ); + } + + #[test] + fn a_target_the_encoding_cannot_reach_is_refused() { + for text in [ + "b.eq\t0x200000", + "cbz\tx0, 0x200000", + "tbz\tx0, #1, 0x20000", + ] { + let e = crate::assemble(&e5r_core::Arch::AArch64, text, Addr(0)).expect_err(text); + assert!(matches!(e, AsmError::BranchRange { .. }), "{text}: {e}"); + } + // An unaligned target is its own error, not a rounded displacement. + let e = crate::assemble(&e5r_core::Arch::AArch64, "b\t0x1002", Addr(0x1000)) + .expect_err("unaligned"); + assert!(matches!(e, AsmError::Unaligned { .. }), "{e}"); + } + + #[test] + fn a_value_no_immediate_can_build_is_refused_rather_than_approximated() { + let e = crate::assemble(&e5r_core::Arch::AArch64, "mov\tx0, #0x123456789", Addr(0)) + .expect_err("no single immediate builds this"); + assert!(matches!(e, AsmError::NoEncoding { .. }), "{e}"); + let e = crate::assemble(&e5r_core::Arch::AArch64, "and\tx0, x1, #0x3", Addr(0)); + assert!(e.is_ok(), "a rotated run of ones does encode"); + let e = crate::assemble(&e5r_core::Arch::AArch64, "and\tx0, x1, #0x5", Addr(0)) + .expect_err("0x5 is not a rotated run of ones"); + assert!(matches!(e, AsmError::NoEncoding { .. }), "{e}"); + } + + #[test] + fn simd_is_declined_by_name_rather_than_called_a_typo() { + let e = crate::assemble( + &e5r_core::Arch::AArch64, + "add\tv0.4s, v1.4s, v2.4s", + Addr(0), + ) + .expect_err("no SIMD encoder"); + assert!( + e.to_string().contains("SIMD"), + "the refusal should say what it is: {e}" + ); + } +} diff --git a/crates/e5r-asm/src/error.rs b/crates/e5r-asm/src/error.rs new file mode 100644 index 0000000..b7ec61e --- /dev/null +++ b/crates/e5r-asm/src/error.rs @@ -0,0 +1,154 @@ +//! The typed refusals. +//! +//! An assembler that guesses is worse than one that stops, because a patch +//! that assembles into the wrong instruction is discovered by the program +//! crashing. Every path that cannot encode says what it could not encode. + +use std::fmt; + +/// The longest instruction text the assembler will look at. +/// +/// Every count read out of the text is bounded before anything is allocated, +/// and this is the outermost of those bounds. +pub const MAX_TEXT: usize = 512; + +/// The most tokens one instruction may lex to. +pub const MAX_TOKENS: usize = 96; + +/// Why a piece of text could not be turned into bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum AsmError { + /// The text held no mnemonic. + Empty, + /// The text is longer than [`MAX_TEXT`], or lexes to more than + /// [`MAX_TOKENS`] tokens. + TooLong { + /// What was measured. + what: &'static str, + /// How much of it there was. + len: usize, + /// The most that is accepted. + limit: usize, + }, + /// A character or token that cannot appear here. + Syntax { + /// Byte offset into the text. + at: usize, + /// What was found, or what was expected instead. + what: String, + }, + /// The mnemonic is not one this assembler encodes. + UnknownMnemonic(String), + /// The name is not a register of this architecture. + UnknownRegister(String), + /// The mnemonic is known, but not with these operands. + UnsupportedForm { + /// The mnemonic that was asked for. + mnemonic: String, + /// Which part of the form is the problem. + detail: &'static str, + }, + /// The architecture has no encoder here. + UnsupportedArch(String), + /// A memory operand carries no width and no register operand implies one. + UnsizedOperand { + /// The mnemonic that was asked for. + mnemonic: String, + }, + /// A value does not fit the field that has to hold it. + Range { + /// Which field. + what: &'static str, + /// The value that was asked for. + value: i64, + /// Lowest value the field encodes. + low: i64, + /// Highest value the field encodes. + high: i64, + }, + /// A value fits but is not a multiple the field can scale to. + Unaligned { + /// Which field. + what: &'static str, + /// The value that was asked for. + value: i64, + /// The multiple it has to be. + align: u32, + }, + /// A branch target is out of reach of the encoding. + /// + /// Named separately from [`AsmError::Range`] because a truncated + /// displacement is the one mistake an assembler must never make quietly. + BranchRange { + /// The mnemonic that was asked for. + mnemonic: String, + /// Where the branch is. + from: u64, + /// Where it was asked to go. + to: u64, + /// The most negative displacement the encoding holds. + low: i64, + /// The most positive displacement the encoding holds. + high: i64, + }, + /// A bit pattern no immediate field of this architecture can express. + NoEncoding { + /// The mnemonic that was asked for. + mnemonic: String, + /// The value that has no encoding. + value: u64, + /// Which family of immediates was tried. + what: &'static str, + }, +} + +impl fmt::Display for AsmError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AsmError::Empty => f.write_str("no instruction in the text"), + AsmError::TooLong { what, len, limit } => { + write!(f, "{what} is {len}, the limit is {limit}") + } + AsmError::Syntax { at, what } => write!(f, "syntax error at byte {at}: {what}"), + AsmError::UnknownMnemonic(m) => write!(f, "unknown mnemonic `{m}`"), + AsmError::UnknownRegister(r) => write!(f, "unknown register `{r}`"), + AsmError::UnsupportedForm { mnemonic, detail } => { + write!(f, "cannot encode `{mnemonic}`: {detail}") + } + AsmError::UnsupportedArch(a) => write!(f, "no assembler for {a}"), + AsmError::UnsizedOperand { mnemonic } => write!( + f, + "`{mnemonic}` needs an operand size: write `byte ptr`, `word ptr`, \ + `dword ptr` or `qword ptr` before the memory operand" + ), + AsmError::Range { + what, + value, + low, + high, + } => write!(f, "{what} {value} is outside {low}..={high}"), + AsmError::Unaligned { what, value, align } => { + write!(f, "{what} {value} is not a multiple of {align}") + } + AsmError::BranchRange { + mnemonic, + from, + to, + low, + high, + } => write!( + f, + "`{mnemonic}` at {from:#x} cannot reach {to:#x}: the encoding holds \ + a displacement of {low}..={high} bytes" + ), + AsmError::NoEncoding { + mnemonic, + value, + what, + } => write!(f, "`{mnemonic}`: {value:#x} is not a {what}"), + } + } +} + +impl std::error::Error for AsmError {} diff --git a/crates/e5r-asm/src/lex.rs b/crates/e5r-asm/src/lex.rs new file mode 100644 index 0000000..bcdac3b --- /dev/null +++ b/crates/e5r-asm/src/lex.rs @@ -0,0 +1,190 @@ +//! A tokenizer both architectures share. +//! +//! The text this has to accept is whatever `e5r disas` prints, so the rules +//! are set by the two printers in `e5r-arch` rather than by any assembler's +//! grammar: tabs separate the mnemonic from its operands, `#` introduces an +//! AArch64 immediate, and a mnemonic may contain a dot (`b.eq`). + +use crate::error::{AsmError, MAX_TEXT, MAX_TOKENS}; + +/// One token, borrowed from the input. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Tok<'a> { + /// A bare word: a mnemonic, a register, a condition, a size keyword. + Ident(&'a str), + /// An unsigned magnitude. A leading `-` arrives as its own punctuation + /// token, because `[rax - 0x1]` spells the sign as an operator. + Num(u64), + /// A single punctuation character. + Punct(u8), +} + +/// A token and where it started, for the error message. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Spanned<'a> { + /// The token. + pub tok: Tok<'a>, + /// Byte offset of its first character. + pub at: usize, +} + +fn ident_start(b: u8) -> bool { + b.is_ascii_alphabetic() || b == b'_' || b == b'.' || b == b'%' +} + +fn ident_body(b: u8) -> bool { + b.is_ascii_alphanumeric() || b == b'_' || b == b'.' +} + +/// Split `text` into tokens. +/// +/// Both bounds are checked before the vector is reserved, so a hostile string +/// cannot make this allocate more than a fixed amount. +pub fn lex(text: &str) -> Result>, AsmError> { + if text.len() > MAX_TEXT { + return Err(AsmError::TooLong { + what: "instruction text length", + len: text.len(), + limit: MAX_TEXT, + }); + } + let b = text.as_bytes(); + let mut out: Vec> = Vec::new(); + let mut i = 0usize; + while i < b.len() { + let c = b[i]; + if c.is_ascii_whitespace() { + i += 1; + continue; + } + // A comment runs to the end of the line; both objdumps put the + // resolved target and the symbol name there, and a user pasting a + // line should not have to strip them. `#` is the awkward one, since + // it also introduces every AArch64 immediate: a comment `#` is the + // one with no digit behind it. + let comment = c == b';' + || c == b'<' + || (c == b'/' && b.get(i + 1) == Some(&b'/')) + || (c == b'#' + && !matches!(b.get(i + 1), Some(d) if d.is_ascii_digit() || *d == b'-' || *d == b'+')); + if comment { + break; + } + if out.len() >= MAX_TOKENS { + return Err(AsmError::TooLong { + what: "token count", + len: out.len() + 1, + limit: MAX_TOKENS, + }); + } + let at = i; + let tok = if c.is_ascii_digit() { + let (v, next) = number(b, i)?; + i = next; + Tok::Num(v) + } else if ident_start(c) { + let start = i; + i += 1; + while i < b.len() && ident_body(b[i]) { + i += 1; + } + // A trailing dot belongs to the next token, not to this word. + let mut end = i; + while end > start + 1 && b[end - 1] == b'.' { + end -= 1; + i = end; + } + Tok::Ident(&text[start..end]) + } else { + i += 1; + Tok::Punct(c) + }; + out.push(Spanned { tok, at }); + } + Ok(out) +} + +/// Read one unsigned number. Hex with `0x`, otherwise decimal. +/// +/// The digit count is bounded by the field width, so no number in the text can +/// make this loop longer than seventy times. +fn number(b: &[u8], start: usize) -> Result<(u64, usize), AsmError> { + let mut i = start; + let (radix, mut i0) = if b[i] == b'0' && matches!(b.get(i + 1), Some(b'x' | b'X')) { + (16u32, i + 2) + } else { + (10u32, i) + }; + if i0 >= b.len() || !(b[i0] as char).is_digit(radix) { + return Err(AsmError::Syntax { + at: start, + what: "a number with no digits".to_string(), + }); + } + let mut v: u64 = 0; + let mut digits = 0usize; + while i0 < b.len() { + let d = match (b[i0] as char).to_digit(radix) { + Some(d) => d, + None => break, + }; + digits += 1; + // 64 bits of binary is the widest field any operand has; anything + // longer is a typo, and stopping here keeps the loop bounded. + if digits > 24 { + return Err(AsmError::Syntax { + at: start, + what: "a number with more digits than a 64-bit field holds".to_string(), + }); + } + v = v.wrapping_mul(radix as u64).wrapping_add(d as u64); + i0 += 1; + } + i = i0; + // A number cannot run straight into a word: `0x10foo` is a typo, and + // accepting it would quietly assemble something the user did not write. + if i < b.len() && ident_body(b[i]) { + return Err(AsmError::Syntax { + at: start, + what: "a number followed by a letter".to_string(), + }); + } + Ok((v, i)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tabs_and_commas_separate() { + let t = lex("mov\trax, rcx").unwrap(); + assert_eq!(t.len(), 4); + assert_eq!(t[0].tok, Tok::Ident("mov")); + assert_eq!(t[2].tok, Tok::Punct(b',')); + } + + #[test] + fn a_dotted_mnemonic_stays_one_word() { + let t = lex("b.eq\t0x1000").unwrap(); + assert_eq!(t[0].tok, Tok::Ident("b.eq")); + assert_eq!(t[1].tok, Tok::Num(0x1000)); + } + + #[test] + fn hex_and_decimal_both_read() { + assert_eq!(lex("#0x10").unwrap()[1].tok, Tok::Num(16)); + assert_eq!(lex("#16").unwrap()[1].tok, Tok::Num(16)); + } + + #[test] + fn a_number_running_into_a_word_is_refused() { + assert!(lex("0x10foo").is_err()); + } + + #[test] + fn the_text_length_is_bounded() { + let long = "a".repeat(MAX_TEXT + 1); + assert!(matches!(lex(&long), Err(AsmError::TooLong { .. }))); + } +} diff --git a/crates/e5r-asm/src/lib.rs b/crates/e5r-asm/src/lib.rs new file mode 100644 index 0000000..c70eadc --- /dev/null +++ b/crates/e5r-asm/src/lib.rs @@ -0,0 +1,175 @@ +//! Assembler: mnemonics and operands to bytes, one instruction at a time. +//! +//! The inverse of `e5r-arch`, and built on top of it rather than beside it. +//! Text is parsed into the same [`Insn`] the decoders produce and the encoder +//! then works from that, so the property that matters for patching falls out +//! of the structure: a line copied from `e5r disas` assembles back to the +//! bytes it came from. +//! +//! There is no linker here, no relocation, no object writer and no macro +//! language. One instruction, one address, one answer, and a typed refusal +//! everywhere else. `e5r patch` needs exactly that and nothing more. +//! +//! ``` +//! use e5r_asm::assemble; +//! use e5r_core::{Addr, Arch}; +//! +//! let e = assemble(&Arch::X86_64, "mov eax, 0x1", Addr(0x1000)).unwrap(); +//! assert_eq!(e.bytes(), &[0xb8, 0x01, 0x00, 0x00, 0x00]); +//! ``` +//! +//! One caveat on the text accepted: AArch64 branch targets must be written +//! `0x1000`, not objdump's bare `1000`, because a bare number reads as decimal +//! everywhere else in the grammar and a silently decimal branch target is the +//! kind of mistake this crate exists to refuse. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod aarch64; +pub mod error; +pub mod lex; +pub mod parse; +pub mod x86; + +use std::fmt; + +use e5r_arch::insn::Insn; +use e5r_core::{Addr, Arch}; + +pub use error::AsmError; +pub use parse::parse; + +/// The longest encoding either architecture has, which is x86's fifteen-byte +/// limit. AArch64 is always four. +pub const MAX_INSN: usize = 15; + +/// The bytes one instruction encodes to. +/// +/// Inline rather than a `Vec`: an assembler that allocates per instruction is +/// the wrong shape for filling a patch region with no-ops. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Encoded { + buf: [u8; MAX_INSN], + len: u8, +} + +impl Encoded { + /// The encoded bytes, in memory order. + pub fn bytes(&self) -> &[u8] { + &self.buf[..self.len as usize] + } + + /// How many bytes the instruction occupies. + pub fn len(&self) -> usize { + self.len as usize + } + + /// True when nothing was encoded, which no successful encode produces. + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// A fixed-width little-endian word, which is every A64 encoding. + pub(crate) fn from_u32_le(w: u32) -> Encoded { + let mut buf = [0u8; MAX_INSN]; + buf[..4].copy_from_slice(&w.to_le_bytes()); + Encoded { buf, len: 4 } + } + + /// Bytes gathered by the variable-length x86 path. + pub(crate) fn from_slice(bytes: &[u8]) -> Result { + if bytes.len() > MAX_INSN { + return Err(AsmError::TooLong { + what: "encoded instruction length", + len: bytes.len(), + limit: MAX_INSN, + }); + } + let mut buf = [0u8; MAX_INSN]; + buf[..bytes.len()].copy_from_slice(bytes); + Ok(Encoded { + buf, + len: bytes.len() as u8, + }) + } +} + +impl fmt::Debug for Encoded { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for b in self.bytes() { + write!(f, "{b:02x}")?; + } + Ok(()) + } +} + +/// Assemble one instruction for `arch`, as if it were placed at `addr`. +/// +/// The address matters because a branch or a pc-relative operand encodes a +/// displacement from where the instruction sits, so the same text at a +/// different address is different bytes. +pub fn assemble(arch: &Arch, text: &str, addr: Addr) -> Result { + let insn = parse::parse(arch, text, addr)?; + encode(arch, &insn) +} + +/// Encode an instruction that is already in [`Insn`] shape. +/// +/// This is the path a caller takes when it has decoded an instruction, changed +/// one operand, and wants the bytes back. +pub fn encode(arch: &Arch, insn: &Insn) -> Result { + match arch { + Arch::AArch64 => aarch64::encode(insn), + Arch::X86_64 => x86::encode(insn), + other => Err(AsmError::UnsupportedArch(format!("{other:?}"))), + } +} + +/// Assemble several lines, each at the address the one before it ended at. +/// +/// Blank lines and comment-only lines encode to nothing, so a pasted block of +/// disassembly with its annotations still assembles. +pub fn assemble_all(arch: &Arch, text: &str, addr: Addr) -> Result, AsmError> { + let mut out = Vec::new(); + let mut at = addr; + for line in text.lines() { + match assemble(arch, line, at) { + Ok(e) => { + out.extend_from_slice(e.bytes()); + at = at.wrapping_offset(e.len() as i64); + } + // A line that holds only a comment is not an error; it is what a + // pasted listing is full of. + Err(AsmError::Empty) => {} + Err(e) => return Err(e), + } + } + Ok(out) +} + +/// Fill exactly `len` bytes with no-ops. +/// +/// Padding is a patching primitive rather than a convenience: replacing a long +/// instruction with a short one leaves a hole, and a hole filled with anything +/// but a no-op is a crash waiting for the fall-through path. +pub fn pad(arch: &Arch, len: usize) -> Result, AsmError> { + match arch { + Arch::AArch64 => { + if len % 4 != 0 { + return Err(AsmError::Unaligned { + what: "padding length", + value: len as i64, + align: 4, + }); + } + let mut out = Vec::with_capacity(len); + for _ in 0..len / 4 { + out.extend_from_slice(&aarch64::NOP.to_le_bytes()); + } + Ok(out) + } + Arch::X86_64 => Ok(x86::padding(len)), + other => Err(AsmError::UnsupportedArch(format!("{other:?}"))), + } +} diff --git a/crates/e5r-asm/src/parse.rs b/crates/e5r-asm/src/parse.rs new file mode 100644 index 0000000..d57e754 --- /dev/null +++ b/crates/e5r-asm/src/parse.rs @@ -0,0 +1,522 @@ +//! Text to [`Insn`], the shape the decoders produce. +//! +//! The grammar is not an assembler's: it is exactly what the two printers in +//! `e5r-arch` emit, so a line copied out of `e5r disas` parses. Anything +//! else that happens to parse is a bonus, never a promise. + +use e5r_core::{Addr, Arch}; + +use e5r_arch::insn::{AddrMode, Cond, Extend, Flow, Insn, MAX_OPERANDS, Mem, Operand, Reg, Shift}; + +use crate::error::AsmError; +use crate::lex::{Spanned, Tok, lex}; +use crate::{aarch64, x86}; + +/// Parse one instruction for `arch`, sited at `addr`. +/// +/// The `len` field is left at zero: only the encoder knows it. +pub fn parse(arch: &Arch, text: &str, addr: Addr) -> Result { + let toks = lex(text)?; + match arch { + Arch::AArch64 => parse_with(&toks, addr, true), + Arch::X86_64 => parse_with(&toks, addr, false), + other => Err(AsmError::UnsupportedArch(format!("{other:?}"))), + } +} + +fn parse_with(toks: &[Spanned<'_>], addr: Addr, a64: bool) -> Result { + let mut p = P { + toks, + i: 0, + a64, + addr, + }; + p.insn() +} + +struct P<'a> { + toks: &'a [Spanned<'a>], + i: usize, + a64: bool, + addr: Addr, +} + +impl<'a> P<'a> { + fn peek(&self) -> Option> { + self.toks.get(self.i).map(|s| s.tok) + } + + fn peek_at(&self, n: usize) -> Option> { + self.toks.get(self.i + n).map(|s| s.tok) + } + + fn bump(&mut self) -> Option> { + let t = self.peek(); + if t.is_some() { + self.i += 1; + } + t + } + + fn at(&self) -> usize { + self.toks.get(self.i).map(|s| s.at).unwrap_or(0) + } + + fn err(&self, what: &str) -> AsmError { + AsmError::Syntax { + at: self.at(), + what: what.to_string(), + } + } + + fn eat_punct(&mut self, c: u8) -> bool { + if self.peek() == Some(Tok::Punct(c)) { + self.i += 1; + true + } else { + false + } + } + + fn expect_punct(&mut self, c: u8) -> Result<(), AsmError> { + if self.eat_punct(c) { + Ok(()) + } else { + Err(self.err(&format!("expected `{}`", c as char))) + } + } + + fn ident(&mut self) -> Option<&'a str> { + match self.peek() { + Some(Tok::Ident(s)) => { + self.i += 1; + Some(s) + } + _ => None, + } + } + + /// An unsigned magnitude with an optional sign in front of it. + fn signed(&mut self) -> Result { + let neg = self.eat_punct(b'-'); + if !neg { + self.eat_punct(b'+'); + } + match self.bump() { + Some(Tok::Num(v)) => Ok(if neg { + (v as i64).wrapping_neg() + } else { + v as i64 + }), + _ => Err(self.err("expected a number")), + } + } + + /// The whole instruction. + fn insn(&mut self) -> Result { + let mut prefix: Option<&'static str> = None; + if !self.a64 { + // `lock`, `rep` and `repne` print ahead of the mnemonic. + while let Some(Tok::Ident(w)) = self.peek() { + let p = match w { + "lock" => "lock", + "rep" | "repe" => "rep", + "repne" | "repnz" => "repne", + _ => break, + }; + // A bare `rep` with nothing after it is the mnemonic itself. + if self.peek_at(1).is_none() { + break; + } + self.i += 1; + prefix = Some(p); + } + } + let name = match self.ident() { + Some(n) => n, + None => return Err(AsmError::Empty), + }; + let mnemonic = if self.a64 { + aarch64::intern(name) + } else { + x86::intern(name) + } + .ok_or_else(|| AsmError::UnknownMnemonic(name.to_string()))?; + + let mut ops: Vec = Vec::new(); + if self.peek().is_some() { + loop { + if ops.len() >= MAX_OPERANDS { + return Err(AsmError::UnsupportedForm { + mnemonic: mnemonic.to_string(), + detail: "more operands than any encoding takes", + }); + } + if self.a64 { + self.a64_operand(&mut ops)?; + } else { + let o = self.x86_operand(mnemonic)?; + ops.push(o); + } + if !self.eat_punct(b',') { + break; + } + } + } + if self.i != self.toks.len() { + return Err(self.err("trailing text after the operands")); + } + + retarget(mnemonic, &mut ops); + let mut i = Insn::new(self.addr, 0, mnemonic, Flow::Next); + i.prefix = prefix; + for o in ops { + i.push(o); + } + Ok(i) + } + + // ------------------------------------------------------------ AArch64 + + /// One AArch64 operand, plus the shift or extend that rides behind it. + fn a64_operand(&mut self, ops: &mut Vec) -> Result<(), AsmError> { + let base = self.a64_atom()?; + // `x2, lsl #2` and `#0x1, lsl #12` both spell the qualifier as a + // comma-separated word, so it is read here rather than by the loop. + if self.peek() == Some(Tok::Punct(b',')) + && let Some(Tok::Ident(w)) = self.peek_at(1) + && let Some(q) = qualifier(w) + { + self.i += 2; + let amount = if self.peek() == Some(Tok::Punct(b'#')) { + self.i += 1; + let v = self.signed()?; + u8::try_from(v).map_err(|_| AsmError::Range { + what: "shift amount", + value: v, + low: 0, + high: 63, + })? + } else { + 0 + }; + match (base, q) { + (Operand::Reg(r), Qual::Shift(s)) => ops.push(Operand::Shifted(r, s, amount)), + (Operand::Reg(r), Qual::Extend(e)) => ops.push(Operand::Extended(r, e, amount)), + (other, Qual::Shift(s)) => { + ops.push(other); + ops.push(Operand::ShiftOp(s, amount)); + } + (_, Qual::Extend(_)) => return Err(self.err("an extend needs a register")), + } + return Ok(()); + } + ops.push(base); + Ok(()) + } + + fn a64_atom(&mut self) -> Result { + match self.peek() { + Some(Tok::Punct(b'#')) => { + self.i += 1; + let v = self.signed()?; + Ok(Operand::Imm(v)) + } + Some(Tok::Punct(b'[')) => self.a64_mem(), + Some(Tok::Num(_)) | Some(Tok::Punct(b'-')) => { + // A bare number in A64 text is only ever a branch or literal + // target, since every immediate carries a `#`. + let v = self.signed()?; + Ok(Operand::Addr(Addr(v as u64))) + } + Some(Tok::Ident(w)) => { + self.i += 1; + if let Some(r) = aarch64::register(w) { + return Ok(Operand::Reg(r)); + } + if let Some(c) = aarch64::condition(w) { + return Ok(Operand::Cond(Cond(c))); + } + if let Some(n) = aarch64::name(w) { + return Ok(Operand::Name(n)); + } + // A lane spelling is a SIMD operand, which this crate declines + // rather than half-encodes. Saying so beats "unknown register", + // which reads like a typo the user could fix. + if w.contains('.') && w.starts_with('v') { + return Err(AsmError::UnsupportedForm { + mnemonic: w.to_string(), + detail: "Advanced SIMD and floating point are not encoded", + }); + } + Err(AsmError::UnknownRegister(w.to_string())) + } + Some(Tok::Punct(b'{')) => Err(AsmError::UnsupportedForm { + mnemonic: "a SIMD register list".to_string(), + detail: "Advanced SIMD and floating point are not encoded", + }), + _ => Err(self.err("expected an operand")), + } + } + + fn a64_mem(&mut self) -> Result { + self.expect_punct(b'[')?; + let base = match self.ident().and_then(aarch64::register) { + Some(r) => r, + None => return Err(self.err("a memory operand needs a base register")), + }; + let mut disp = 0i64; + let mut index: Option<(Reg, Extend, u8)> = None; + if self.eat_punct(b',') { + if self.eat_punct(b'#') { + disp = self.signed()?; + } else { + let ix = match self.ident().and_then(aarch64::register) { + Some(r) => r, + None => return Err(self.err("expected an index register or `#`")), + }; + let mut ext = Extend::Lsl; + let mut amount = 0u8; + if self.eat_punct(b',') { + let w = self.ident().ok_or_else(|| self.err("expected an extend"))?; + ext = match qualifier(w) { + Some(Qual::Extend(e)) => e, + Some(Qual::Shift(Shift::Lsl)) => Extend::Lsl, + _ => return Err(self.err("an index takes lsl, uxtw, sxtw or sxtx")), + }; + if self.eat_punct(b'#') { + let v = self.signed()?; + amount = u8::try_from(v).map_err(|_| AsmError::Range { + what: "index shift", + value: v, + low: 0, + high: 4, + })?; + // `lsl #0` is written out only where the scale bit is + // set, and the decoder spells that LslZero. + if ext == Extend::Lsl && amount == 0 { + ext = Extend::LslZero; + } + } + } + index = Some((ix, ext, amount)); + } + } + self.expect_punct(b']')?; + let mut mode = AddrMode::Offset; + if self.eat_punct(b'!') { + mode = AddrMode::PreIndex; + } else if self.peek() == Some(Tok::Punct(b',')) && self.peek_at(1) == Some(Tok::Punct(b'#')) + { + // `[sp], #16`: the only thing that follows a closed bracket. + self.i += 2; + disp = self.signed()?; + mode = AddrMode::PostIndex; + } + Ok(Operand::Mem(Mem { + seg: None, + base: Some(base), + index, + disp, + mode, + // The transfer size comes from the mnemonic, which the encoder + // knows and the text does not. + size: 0, + })) + } + + // ---------------------------------------------------------------- x86 + + fn x86_operand(&mut self, mnemonic: &'static str) -> Result { + // A size keyword, then `ptr`, then optionally a segment override. + let mut size = 0u64; + if let Some(Tok::Ident(w)) = self.peek() + && let Some(n) = size_keyword(w) + && self.peek_at(1) == Some(Tok::Ident("ptr")) + { + self.i += 2; + size = n; + } + let mut seg = None; + if let Some(Tok::Ident(w)) = self.peek() + && self.peek_at(1) == Some(Tok::Punct(b':')) + && let Some(s) = x86::segment(w) + { + self.i += 2; + seg = Some(s); + } + if self.peek() == Some(Tok::Punct(b'[')) { + return self.x86_mem(seg, size); + } + if size != 0 || seg.is_some() { + return Err(self.err("a size or segment prefix needs a memory operand")); + } + match self.peek() { + Some(Tok::Ident(w)) => { + self.i += 1; + if let Some(r) = x86::register(w) { + return Ok(Operand::Reg(r)); + } + if let Some(n) = x86::name(w) { + return Ok(Operand::Name(n)); + } + Err(AsmError::UnknownRegister(w.to_string())) + } + Some(Tok::Num(_)) | Some(Tok::Punct(b'-')) | Some(Tok::Punct(b'+')) => { + let v = self.signed()?; + let _ = mnemonic; + Ok(Operand::Imm(v)) + } + _ => Err(self.err("expected an operand")), + } + } + + fn x86_mem(&mut self, seg: Option, size: u64) -> Result { + self.expect_punct(b'[')?; + let mut base: Option = None; + let mut index: Option<(Reg, Extend, u8)> = None; + let mut disp: i64 = 0; + let mut sign = 1i64; + let mut first = true; + loop { + if self.peek() == Some(Tok::Punct(b']')) { + break; + } + if !first { + match self.bump() { + Some(Tok::Punct(b'+')) => sign = 1, + Some(Tok::Punct(b'-')) => sign = -1, + _ => return Err(self.err("expected `+`, `-` or `]`")), + } + } + first = false; + match self.peek() { + Some(Tok::Ident(w)) => { + self.i += 1; + let r = x86::register(w).ok_or(AsmError::UnknownRegister(w.to_string()))?; + let scale = if self.eat_punct(b'*') { + let v = self.signed()?; + scale_shift(v).ok_or(AsmError::Range { + what: "index scale", + value: v, + low: 1, + high: 8, + })? + } else { + u8::MAX + }; + if scale != u8::MAX || base.is_some() { + if index.is_some() { + return Err(self.err("a memory operand has one index register")); + } + index = Some((r, Extend::Lsl, if scale == u8::MAX { 0 } else { scale })); + } else { + base = Some(r); + } + } + Some(Tok::Num(v)) => { + self.i += 1; + // `4*rcx` writes the scale first, which is how the + // printer spells it. + if self.eat_punct(b'*') { + let shift = scale_shift(v as i64).ok_or(AsmError::Range { + what: "index scale", + value: v as i64, + low: 1, + high: 8, + })?; + let w = self + .ident() + .ok_or_else(|| self.err("expected a register"))?; + let r = x86::register(w).ok_or(AsmError::UnknownRegister(w.to_string()))?; + if index.is_some() { + return Err(self.err("a memory operand has one index register")); + } + index = Some((r, Extend::Lsl, shift)); + } else { + disp = disp.wrapping_add(sign.wrapping_mul(v as i64)); + } + } + _ => return Err(self.err("expected a register or a number")), + } + } + self.expect_punct(b']')?; + Ok(Operand::Mem(Mem { + seg, + base, + index, + disp, + mode: AddrMode::Offset, + size, + })) + } +} + +/// A trailing register qualifier. +enum Qual { + Shift(Shift), + Extend(Extend), +} + +fn qualifier(w: &str) -> Option { + Some(match w { + "lsl" => Qual::Shift(Shift::Lsl), + "lsr" => Qual::Shift(Shift::Lsr), + "asr" => Qual::Shift(Shift::Asr), + "ror" => Qual::Shift(Shift::Ror), + "msl" => Qual::Shift(Shift::Msl), + "uxtb" => Qual::Extend(Extend::Uxtb), + "uxth" => Qual::Extend(Extend::Uxth), + "uxtw" => Qual::Extend(Extend::Uxtw), + "uxtx" => Qual::Extend(Extend::Uxtx), + "sxtb" => Qual::Extend(Extend::Sxtb), + "sxth" => Qual::Extend(Extend::Sxth), + "sxtw" => Qual::Extend(Extend::Sxtw), + "sxtx" => Qual::Extend(Extend::Sxtx), + _ => return None, + }) +} + +fn size_keyword(w: &str) -> Option { + Some(match w { + "byte" => 1, + "word" => 2, + "dword" => 4, + "qword" => 8, + "tbyte" => 10, + "xmmword" => 16, + _ => return None, + }) +} + +fn scale_shift(v: i64) -> Option { + Some(match v { + 1 => 0, + 2 => 1, + 4 => 2, + 8 => 3, + _ => return None, + }) +} + +/// Turn the immediate a direct branch carries into the address it is. +/// +/// The printers emit an absolute target for these, and analysis expects +/// [`Operand::Addr`], so a parsed instruction has to carry the same shape. +fn retarget(mnemonic: &str, ops: &mut [Operand]) { + let branch = matches!( + mnemonic, + "b" | "bl" | "cbz" | "cbnz" | "tbz" | "tbnz" | "adr" | "adrp" + ) || mnemonic.starts_with("b.") + || matches!(mnemonic, "jmp" | "call") + || (mnemonic.starts_with('j') && mnemonic.len() >= 2) + || matches!(mnemonic, "loop" | "loope" | "loopne"); + if !branch { + return; + } + if let Some(last) = ops.last_mut() + && let Operand::Imm(v) = *last + { + *last = Operand::Addr(Addr(v as u64)); + } +} diff --git a/crates/e5r-asm/src/x86.rs b/crates/e5r-asm/src/x86.rs new file mode 100644 index 0000000..24d3610 --- /dev/null +++ b/crates/e5r-asm/src/x86.rs @@ -0,0 +1,1643 @@ +//! x86-64 encoding: one [`Insn`] to between one and fifteen bytes. +//! +//! REX is computed rather than tabulated. Every form decides an operand size +//! and a set of register numbers, and the prefix falls out of those two facts: +//! W from the size, R, X and B from whichever register numbers reach past +//! seven. A table of REX bytes per opcode is a table that goes wrong quietly +//! the first time a form is added, which is the failure this crate cannot have. +//! +//! Where more than one encoding exists the shortest is chosen, except where +//! the shorter one decodes to a different [`Insn`] than the text named. That +//! exception is what keeps the round trip exact: `shl eax, 0x1` encodes as the +//! count form rather than the one-byte `d1` shift, because the decoder prints +//! `d1` with no count at all and the two would not compare equal. + +use e5r_arch::insn::{AddrMode, Insn, Mem, Operand, Reg, RegClass, Width}; +use e5r_core::Addr; + +use crate::Encoded; +use crate::error::AsmError; + +// ------------------------------------------------------------------- naming + +/// Every mnemonic this encoder accepts. +/// +/// Sorted, because the lookup is a binary search and because a duplicate or a +/// stray entry shows up as a failing sort test rather than as a wrong encoding. +const MNEMONICS: &[&str] = &[ + "adc", "add", "and", "bsf", "bsr", "bswap", "bt", "btc", "btr", "bts", "call", "cbw", "cdq", + "cdqe", "clc", "cld", "cmc", "cmova", "cmovae", "cmovb", "cmovbe", "cmove", "cmovg", "cmovge", + "cmovl", "cmovle", "cmovne", "cmovno", "cmovnp", "cmovns", "cmovo", "cmovp", "cmovs", "cmp", + "cmpxchg", "cqo", "cwd", "cwde", "dec", "div", "endbr32", "endbr64", "hlt", "idiv", "imul", + "inc", "int", "int3", "ja", "jae", "jb", "jbe", "je", "jg", "jge", "jl", "jle", "jmp", "jne", + "jno", "jnp", "jns", "jo", "jp", "js", "lea", "leave", "lfence", "mfence", "mov", "movabs", + "movsx", "movsxd", "movzx", "mul", "neg", "nop", "not", "or", "pause", "pop", "popfq", "push", + "pushfq", "rcl", "rcr", "ret", "rol", "ror", "sar", "sbb", "seta", "setae", "setb", "setbe", + "sete", "setg", "setge", "setl", "setle", "setne", "setno", "setnp", "setns", "seto", "setp", + "sets", "sfence", "shl", "shr", "stc", "std", "sub", "syscall", "test", "ud2", "xadd", "xchg", + "xor", +]; + +/// Resolve a mnemonic to the static string an [`Insn`] can hold. +pub(crate) fn intern(s: &str) -> Option<&'static str> { + MNEMONICS.binary_search(&s).ok().map(|n| MNEMONICS[n]).or({ + // `sal` is the assembler's other spelling of `shl`; the decoder never + // prints it, so it is accepted rather than listed. + match s { + "sal" => Some("shl"), + "repz" => Some("rep"), + _ => None, + } + }) +} + +/// Bare words that are operands rather than registers. x86 has none the +/// encoder handles, but the parser asks, so the answer is written down. +pub(crate) fn name(_s: &str) -> Option<&'static str> { + None +} + +const GPR64: [&str; 16] = [ + "rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", "r8", "r9", "r10", "r11", "r12", "r13", + "r14", "r15", +]; +const GPR32: [&str; 16] = [ + "eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi", "r8d", "r9d", "r10d", "r11d", "r12d", + "r13d", "r14d", "r15d", +]; +const GPR16: [&str; 16] = [ + "ax", "cx", "dx", "bx", "sp", "bp", "si", "di", "r8w", "r9w", "r10w", "r11w", "r12w", "r13w", + "r14w", "r15w", +]; +const GPR8: [&str; 16] = [ + "al", "cl", "dl", "bl", "spl", "bpl", "sil", "dil", "r8b", "r9b", "r10b", "r11b", "r12b", + "r13b", "r14b", "r15b", +]; +const GPR8H: [&str; 4] = ["ah", "ch", "dh", "bh"]; +const SEG: [&str; 6] = ["es", "cs", "ss", "ds", "fs", "gs"]; + +fn find(table: &[&str], s: &str) -> Option { + table.iter().position(|n| *n == s).map(|n| n as u8) +} + +/// Parse a register name, in any of the four general purpose widths plus the +/// high byte bank, the segments, the vector bank and the program counter. +pub(crate) fn register(s: &str) -> Option { + if let Some(n) = find(&GPR64, s) { + return Some(Reg::gpr(n, Width::W64)); + } + if let Some(n) = find(&GPR32, s) { + return Some(Reg::gpr(n, Width::W32)); + } + if let Some(n) = find(&GPR16, s) { + return Some(Reg::gpr(n, Width::W16)); + } + if let Some(n) = find(&GPR8, s) { + return Some(Reg::gpr(n, Width::W8)); + } + if let Some(n) = find(&GPR8H, s) { + return Some(Reg { + class: RegClass::GprHigh, + num: n, + width: Width::W8, + }); + } + if let Some(n) = find(&SEG, s) { + return Some(Reg { + class: RegClass::Seg, + num: n, + width: Width::W16, + }); + } + match s { + "rip" => Some(Reg { + class: RegClass::Pc, + num: 0, + width: Width::W64, + }), + "eip" => Some(Reg { + class: RegClass::Pc, + num: 0, + width: Width::W32, + }), + // The phantom SIB index llvm prints when the index field is empty but + // the scale still has to show. + "riz" => Some(Reg { + class: RegClass::Zr, + num: 0, + width: Width::W64, + }), + "eiz" => Some(Reg { + class: RegClass::Zr, + num: 0, + width: Width::W32, + }), + _ => s + .strip_prefix("xmm") + .and_then(|n| n.parse::().ok()) + .filter(|n| *n < 16) + .map(|n| Reg::vec(n, Width::W128)), + } +} + +/// A segment register, for the `fs:` style override the parser reads. +pub(crate) fn segment(s: &str) -> Option { + find(&SEG, s).map(|n| Reg { + class: RegClass::Seg, + num: n, + width: Width::W16, + }) +} + +/// The condition number a `jcc`, `setcc` or `cmovcc` suffix names. +fn cond(suffix: &str) -> Option { + const CC: [&str; 16] = [ + "o", "no", "b", "ae", "e", "ne", "be", "a", "s", "ns", "p", "np", "l", "ge", "le", "g", + ]; + // The synonyms objdump never prints but a person writes by habit. + let canon = match suffix { + "nae" | "c" => "b", + "nb" | "nc" => "ae", + "z" => "e", + "nz" => "ne", + "na" => "be", + "nbe" => "a", + "pe" => "p", + "po" => "np", + "nge" => "l", + "nl" => "ge", + "ng" => "le", + "nle" => "g", + other => other, + }; + find(&CC, canon) +} + +// ------------------------------------------------------------------ helpers + +fn form(i: &Insn, detail: &'static str) -> AsmError { + AsmError::UnsupportedForm { + mnemonic: i.mnemonic.to_string(), + detail, + } +} + +/// What a ModRM r/m field can name. +#[derive(Clone, Copy)] +enum Rm { + Reg(Reg), + Mem(Mem), +} + +fn rm_of(i: &Insn, op: &Operand) -> Result { + match op { + Operand::Reg(r) => Ok(Rm::Reg(*r)), + Operand::Mem(m) => Ok(Rm::Mem(*m)), + _ => Err(form(i, "expected a register or a memory operand")), + } +} + +/// The number a register encodes as. The high byte bank is numbered from zero +/// in [`Reg`] because `ah` is not the low byte of `rax`, but the ModRM field it +/// lands in is the same four to seven the low bytes would use without REX. +fn rnum(r: Reg) -> u8 { + match r.class { + RegClass::GprHigh => r.num + 4, + _ => r.num, + } +} + +fn imm_of(op: &Operand) -> Option { + match op { + Operand::Imm(v) | Operand::Count(v) => Some(*v), + Operand::UImm(v) => Some(*v as i64), + Operand::Addr(a) => Some(a.get() as i64), + _ => None, + } +} + +fn addr_of(op: &Operand) -> Option { + match op { + Operand::Addr(a) => Some(*a), + Operand::Imm(v) | Operand::Count(v) => Some(Addr(*v as u64)), + Operand::UImm(v) => Some(Addr(*v)), + _ => None, + } +} + +/// True when `v` reaches the field either as a signed or as an unsigned +/// number. `mov al, 0xff` and `mov al, -0x1` are the same byte, and refusing +/// one of them would make half the disassembly in the world unassemblable. +fn imm_fits(v: i64, bytes: u8, extends: bool) -> bool { + let bits = bytes as u32 * 8; + if bits >= 64 { + return true; + } + let low = -(1i64 << (bits - 1)); + // An immediate narrower than the operand is sign-extended by the + // processor, so only the signed half of the field is reachable: + // `mov eax, 0xffffffff` is the doubleword form and `mov rax, 0xffffffff` + // cannot be, because that field would sign-extend to all ones. + let high = if extends { + (1i64 << (bits - 1)) - 1 + } else { + (1i64 << bits) - 1 + }; + (low..=high).contains(&v) +} + +fn check_imm(v: i64, bytes: u8, extends: bool) -> Result<(), AsmError> { + if imm_fits(v, bytes, extends) { + return Ok(()); + } + let bits = bytes as u32 * 8; + Err(AsmError::Range { + what: "immediate", + value: v, + low: -(1i64 << (bits - 1)), + high: if extends { + (1i64 << (bits - 1)) - 1 + } else { + (1i64 << bits) - 1 + }, + }) +} + +/// How many bytes the immediate field of a form at this operand size is, and +/// whether the processor sign-extends it. +fn imm_field(w: Width) -> (u8, bool) { + match w { + Width::W8 => (1, false), + Width::W16 => (2, false), + Width::W64 => (4, true), + _ => (4, false), + } +} + +fn width_bytes(w: Width) -> u64 { + w.bytes() +} + +/// The operand size a form works at, taken from whichever operand states one. +/// +/// A register settles it; failing that the `byte ptr` on a memory operand +/// does. When neither says, the instruction is genuinely ambiguous and the +/// refusal names the keyword the user has to add. +fn operand_width(i: &Insn, ops: &[&Operand]) -> Result { + for op in ops { + if let Operand::Reg(r) = op + && matches!(r.class, RegClass::Gpr | RegClass::GprHigh) + { + return Ok(r.width); + } + } + for op in ops { + if let Operand::Mem(m) = op { + return match m.size { + 1 => Ok(Width::W8), + 2 => Ok(Width::W16), + 4 => Ok(Width::W32), + 8 => Ok(Width::W64), + _ => Err(AsmError::UnsizedOperand { + mnemonic: i.mnemonic.to_string(), + }), + }; + } + } + Err(AsmError::UnsizedOperand { + mnemonic: i.mnemonic.to_string(), + }) +} + +// ------------------------------------------------------------------ emitter + +/// The pieces of one encoding, gathered before any byte is written. +/// +/// Built in this shape because the prefix bytes depend on decisions the +/// operand walk makes after the opcode is already chosen: whether a register +/// number reaches past seven, whether an address register is 32 bits, whether +/// a byte register needs REX to exist at all. +#[derive(Default)] +struct Emit { + lock: bool, + rep: Option, + seg: Option, + o16: bool, + a32: bool, + w: bool, + r: bool, + x: bool, + b: bool, + /// A byte register that only exists with REX present: `spl` and friends. + force_rex: bool, + /// A byte register that cannot coexist with REX: `ah` and friends. + deny_rex: bool, + opcode: [u8; 3], + opcode_len: u8, + modrm: Option, + sib: Option, + disp: Option<(i64, u8)>, + imm: Option<(i64, u8)>, +} + +impl Emit { + fn op(&mut self, bytes: &[u8]) { + self.opcode_len = bytes.len() as u8; + self.opcode[..bytes.len()].copy_from_slice(bytes); + } + + /// Set the operand size, which is where REX.W and the `0x66` prefix come + /// from. Byte and 32-bit operands need neither. + fn width(&mut self, w: Width) { + self.o16 = w == Width::W16; + self.w = w == Width::W64; + } + + /// Record a register used as a plain operand, for the REX rules that are + /// about the register bank rather than about a ModRM field. + fn note(&mut self, r: Reg) { + if r.class == RegClass::GprHigh { + self.deny_rex = true; + } + if r.class == RegClass::Gpr && r.width == Width::W8 && (4..8).contains(&r.num) { + self.force_rex = true; + } + } + + fn finish(&self, i: &Insn) -> Result { + let rex_bits = (u8::from(self.w) << 3) + | (u8::from(self.r) << 2) + | (u8::from(self.x) << 1) + | u8::from(self.b); + let rex = rex_bits != 0 || self.force_rex; + if rex && self.deny_rex { + return Err(form( + i, + "ah, ch, dh and bh cannot appear alongside a register or size that needs REX", + )); + } + let mut out: Vec = Vec::with_capacity(crate::MAX_INSN); + if self.lock { + out.push(0xf0); + } + if let Some(p) = self.rep { + out.push(p); + } + if let Some(s) = self.seg { + out.push(s); + } + if self.o16 { + out.push(0x66); + } + if self.a32 { + out.push(0x67); + } + if rex { + out.push(0x40 | rex_bits); + } + out.extend_from_slice(&self.opcode[..self.opcode_len as usize]); + if let Some(m) = self.modrm { + out.push(m); + } + if let Some(s) = self.sib { + out.push(s); + } + if let Some((v, n)) = self.disp { + out.extend_from_slice(&v.to_le_bytes()[..n as usize]); + } + if let Some((v, n)) = self.imm { + out.extend_from_slice(&v.to_le_bytes()[..n as usize]); + } + Encoded::from_slice(&out) + } + + /// Fill in ModRM, SIB, the displacement and the REX extension bits for a + /// `reg, r/m` pair. + fn modrm_pair(&mut self, i: &Insn, reg_field: u8, rm: Rm) -> Result<(), AsmError> { + self.r = reg_field >= 8; + match rm { + Rm::Reg(r) => { + self.note(r); + let n = match r.class { + RegClass::Gpr | RegClass::Vec | RegClass::GprHigh => rnum(r), + _ => return Err(form(i, "this operand must be a general purpose register")), + }; + self.b = n >= 8; + self.modrm = Some(0xc0 | ((reg_field & 7) << 3) | (n & 7)); + Ok(()) + } + Rm::Mem(m) => self.modrm_mem(i, reg_field, &m), + } + } + + fn modrm_mem(&mut self, i: &Insn, reg_field: u8, m: &Mem) -> Result<(), AsmError> { + // Set here as well as in `modrm_pair`, because the forms that know + // their operand is memory call straight into this one. + self.r = reg_field >= 8; + if m.mode != AddrMode::Offset { + return Err(form(i, "x86 has no pre- or post-indexed addressing")); + } + if let Some(s) = m.seg { + if s.class != RegClass::Seg || s.num > 5 { + return Err(form(i, "expected a segment register")); + } + self.seg = Some([0x26u8, 0x2e, 0x36, 0x3e, 0x64, 0x65][s.num as usize]); + } + let reg3 = (reg_field & 7) << 3; + + // A rip-relative operand carries the displacement the bytes hold, not + // a resolved address: the decoder leaves it that way so the text and + // the encoding cannot disagree, and this is the other half of that. + if let Some(base) = m.base + && base.class == RegClass::Pc + { + if m.index.is_some() { + return Err(form(i, "a rip-relative address takes no index")); + } + self.a32 = base.width == Width::W32; + check_imm(m.disp, 4, true)?; + self.modrm = Some(reg3 | 0b101); + self.disp = Some((m.disp, 4)); + return Ok(()); + } + + // Every address register has to agree on its width, since one `0x67` + // covers the whole operand. + let mut a32 = false; + for r in [m.base, m.index.map(|x| x.0)].into_iter().flatten() { + match r.width { + Width::W32 => a32 = true, + Width::W64 => {} + _ => return Err(form(i, "an address register is 32 or 64 bits")), + } + } + self.a32 = a32; + + let base = match m.base { + Some(b) if matches!(b.class, RegClass::Gpr) => Some(b.num), + Some(_) => return Err(form(i, "a base register is general purpose")), + None => None, + }; + let index = match m.index { + Some((r, ext, shift)) => { + if !matches!( + ext, + e5r_arch::insn::Extend::Lsl | e5r_arch::insn::Extend::LslZero + ) { + return Err(form(i, "an x86 index is scaled, never extended")); + } + if shift > 3 { + return Err(AsmError::Range { + what: "index scale", + value: 1i64 << shift, + low: 1, + high: 8, + }); + } + match r.class { + // `riz` is no index at all, kept so the scale stays visible. + RegClass::Zr => Some((4u8, shift)), + RegClass::Gpr => { + if r.num == 4 { + return Err(form(i, "rsp cannot be an index register")); + } + Some((r.num, shift)) + } + _ => return Err(form(i, "an index register is general purpose")), + } + } + None => None, + }; + if let Some((n, _)) = index { + self.x = n >= 8; + } + + let Some(base) = base else { + // No base: the only 64-bit form is a SIB with base 101 and a full + // displacement, which is how an absolute address encodes. + check_imm(m.disp, 4, true)?; + let (ix, scale) = index.unwrap_or((4, 0)); + self.modrm = Some(reg3 | 0b100); + self.sib = Some((scale << 6) | ((ix & 7) << 3) | 0b101); + self.disp = Some((m.disp, 4)); + return Ok(()); + }; + self.b = base >= 8; + + // rbp and r13 have no mod-zero form, so a zero displacement there is + // still written out as a byte. + let md = if m.disp == 0 && base & 7 != 5 { + 0u8 + } else if (-0x80..=0x7f).contains(&m.disp) { + 1 + } else { + check_imm(m.disp, 4, true)?; + 2 + }; + let need_sib = index.is_some() || base & 7 == 4; + if need_sib { + let (ix, scale) = index.unwrap_or((4, 0)); + self.modrm = Some((md << 6) | reg3 | 0b100); + self.sib = Some((scale << 6) | ((ix & 7) << 3) | (base & 7)); + } else { + self.modrm = Some((md << 6) | reg3 | (base & 7)); + } + self.disp = match md { + 1 => Some((m.disp, 1)), + 2 => Some((m.disp, 4)), + _ => None, + }; + Ok(()) + } +} + +// ----------------------------------------------------------------- dispatch + +/// The eight arithmetic and logic operations, in opcode order. +fn arith_index(m: &str) -> Option { + Some(match m { + "add" => 0, + "or" => 1, + "adc" => 2, + "sbb" => 3, + "and" => 4, + "sub" => 5, + "xor" => 6, + "cmp" => 7, + _ => return None, + }) +} + +/// The eight shifts and rotates, in ModRM reg order. +fn shift_index(m: &str) -> Option { + Some(match m { + "rol" => 0, + "ror" => 1, + "rcl" => 2, + "rcr" => 3, + "shl" => 4, + "shr" => 5, + "sar" => 7, + _ => return None, + }) +} + +/// Encode one instruction. +pub fn encode(i: &Insn) -> Result { + let mut e = Emit::default(); + match i.prefix { + None => {} + Some("lock") => e.lock = true, + Some("rep") => e.rep = Some(0xf3), + Some("repne") => e.rep = Some(0xf2), + Some(_) => return Err(form(i, "unknown prefix")), + } + let m = i.mnemonic; + if let Some(n) = arith_index(m) { + return arith(i, e, n); + } + if shift_index(m).is_some() { + return shift(i, e); + } + if let Some(c) = m.strip_prefix("set").and_then(cond) { + return setcc(i, e, c); + } + if let Some(c) = m.strip_prefix("cmov").and_then(cond) { + return two_byte_rm_reg(i, e, 0x40 + c); + } + if m.len() >= 2 + && m.starts_with('j') + && m != "jmp" + && let Some(c) = cond(&m[1..]) + { + return jcc(i, e, c); + } + match m { + "mov" | "movabs" => mov(i, e), + "lea" => lea(i, e), + "push" => push(i, e), + "pop" => pop(i, e), + "test" => test(i, e), + "xchg" => xchg(i, e), + "jmp" | "call" => jmp_call(i, e), + "ret" => ret(i, e), + "nop" => nop(i, e), + "movzx" | "movsx" => movx(i, e), + "movsxd" => movsxd(i, e), + "imul" => imul(i, e), + "inc" | "dec" => inc_dec(i, e), + "not" | "neg" | "mul" | "div" | "idiv" => unary(i, e), + "bt" | "bts" | "btr" | "btc" => bit_test(i, e), + "bsf" | "bsr" => two_byte_rm_reg(i, e, if m == "bsf" { 0xbc } else { 0xbd }), + "bswap" => bswap(i, e), + "cmpxchg" => cmpxchg(i, e), + "xadd" => xadd(i, e), + "int" => int_n(i, e), + _ => no_operand(i, e), + } +} + +/// The forms that are a fixed byte string and nothing else. +fn no_operand(i: &Insn, e: Emit) -> Result { + if !i.operands().is_empty() { + return Err(form(i, "this mnemonic takes no operands")); + } + let bytes: &[u8] = match i.mnemonic { + "int3" => &[0xcc], + "leave" => &[0xc9], + "hlt" => &[0xf4], + "syscall" => &[0x0f, 0x05], + "ud2" => &[0x0f, 0x0b], + "endbr64" => &[0xf3, 0x0f, 0x1e, 0xfa], + "endbr32" => &[0xf3, 0x0f, 0x1e, 0xfb], + "pause" => &[0xf3, 0x90], + "clc" => &[0xf8], + "stc" => &[0xf9], + "cmc" => &[0xf5], + "cld" => &[0xfc], + "std" => &[0xfd], + "lfence" => &[0x0f, 0xae, 0xe8], + "mfence" => &[0x0f, 0xae, 0xf0], + "sfence" => &[0x0f, 0xae, 0xf8], + "pushfq" => &[0x9c], + "popfq" => &[0x9d], + "cbw" => &[0x66, 0x98], + "cwde" => &[0x98], + "cdqe" => &[0x48, 0x98], + "cwd" => &[0x66, 0x99], + "cdq" => &[0x99], + "cqo" => &[0x48, 0x99], + _ => return Err(form(i, "no encoder for this mnemonic")), + }; + // These are written out whole, prefixes included, so the builder's own + // prefix slots have nowhere to go. + if e.lock || e.rep.is_some() { + return Err(form(i, "this mnemonic takes no prefix")); + } + Encoded::from_slice(bytes) +} + +fn two(i: &Insn) -> Result<(&Operand, &Operand), AsmError> { + let ops = i.operands(); + match (ops.first(), ops.get(1)) { + (Some(a), Some(b)) if ops.len() == 2 => Ok((a, b)), + _ => Err(form(i, "needs exactly two operands")), + } +} + +fn arith(i: &Insn, mut e: Emit, n: u8) -> Result { + let (dst, src) = two(i)?; + let w = operand_width(i, &[dst, src])?; + e.width(w); + let byte = w == Width::W8; + let base = n * 8; + match (dst, src) { + // Register to register takes the `r/m, reg` direction, which is what + // every assembler picks and therefore what most corpora hold. + (Operand::Reg(d), Operand::Reg(s)) => { + e.note(*s); + e.op(&[base + u8::from(!byte)]); + e.modrm_pair(i, rnum(*s), Rm::Reg(*d))?; + } + (Operand::Mem(m), Operand::Reg(s)) => { + e.note(*s); + e.op(&[base + u8::from(!byte)]); + e.modrm_mem(i, rnum(*s), m)?; + } + (Operand::Reg(d), Operand::Mem(m)) => { + e.note(*d); + e.op(&[base + 2 + u8::from(!byte)]); + e.modrm_mem(i, rnum(*d), m)?; + } + (rm, imm) => { + let v = imm_of(imm).ok_or(form(i, "expected a register, memory or an immediate"))?; + let rm = rm_of(i, rm)?; + arith_imm(i, &mut e, n, w, rm, v)?; + } + } + e.finish(i) +} + +fn arith_imm(i: &Insn, e: &mut Emit, n: u8, w: Width, rm: Rm, v: i64) -> Result<(), AsmError> { + if w == Width::W8 { + check_imm(v, 1, false)?; + if let Rm::Reg(r) = rm + && r.class == RegClass::Gpr + && r.num == 0 + { + e.op(&[n * 8 + 4]); + e.imm = Some((v, 1)); + return Ok(()); + } + e.op(&[0x80]); + e.modrm_pair(i, n, rm)?; + e.imm = Some((v, 1)); + return Ok(()); + } + // Shortest first: the sign-extended byte, then the accumulator form, then + // the general one. + if (-0x80..=0x7f).contains(&v) { + e.op(&[0x83]); + e.modrm_pair(i, n, rm)?; + e.imm = Some((v, 1)); + return Ok(()); + } + let (zbytes, extends) = imm_field(w); + check_imm(v, zbytes, extends)?; + if let Rm::Reg(r) = rm + && r.class == RegClass::Gpr + && r.num == 0 + { + e.op(&[n * 8 + 5]); + e.imm = Some((v, zbytes)); + return Ok(()); + } + e.op(&[0x81]); + e.modrm_pair(i, n, rm)?; + e.imm = Some((v, zbytes)); + Ok(()) +} + +fn mov(i: &Insn, mut e: Emit) -> Result { + let (dst, src) = two(i)?; + let w = operand_width(i, &[dst, src])?; + e.width(w); + let byte = w == Width::W8; + match (dst, src) { + (Operand::Reg(d), Operand::Reg(s)) + if matches!(d.class, RegClass::Seg) || matches!(s.class, RegClass::Seg) => + { + segment_move(i, e, *d, *s) + } + (Operand::Reg(d), Operand::Reg(s)) => { + e.note(*s); + e.op(&[0x88 + u8::from(!byte)]); + e.modrm_pair(i, rnum(*s), Rm::Reg(*d))?; + e.finish(i) + } + (Operand::Mem(m), Operand::Reg(s)) => { + e.note(*s); + if s.class == RegClass::Seg { + e.width(Width::W16); + e.o16 = false; + e.op(&[0x8c]); + e.modrm_mem(i, s.num, m)?; + return e.finish(i); + } + e.op(&[0x88 + u8::from(!byte)]); + e.modrm_mem(i, rnum(*s), m)?; + e.finish(i) + } + (Operand::Reg(d), Operand::Mem(m)) => { + e.note(*d); + if d.class == RegClass::Seg { + e.width(Width::W16); + e.o16 = false; + e.op(&[0x8e]); + e.modrm_mem(i, d.num, m)?; + return e.finish(i); + } + e.op(&[0x8a + u8::from(!byte)]); + e.modrm_mem(i, rnum(*d), m)?; + e.finish(i) + } + (Operand::Reg(d), imm) => { + let v = imm_of(imm).ok_or(form(i, "expected a register, memory or an immediate"))?; + e.note(*d); + if d.class != RegClass::Gpr && d.class != RegClass::GprHigh { + return Err(form(i, "expected a general purpose register")); + } + let full = width_bytes(w) as u8; + // The ten-byte form is the only one that carries a full 64-bit + // immediate, and it is the only thing `movabs` ever means. + let wide = i.mnemonic == "movabs" || (w == Width::W64 && !imm_fits(v, 4, true)); + if wide || w != Width::W64 { + if !wide { + check_imm(v, full, false)?; + } + let n = rnum(*d); + e.b = n >= 8; + e.op(&[(if byte { 0xb0 } else { 0xb8 }) + (n & 7)]); + e.imm = Some((v, full)); + return e.finish(i); + } + check_imm(v, 4, true)?; + e.op(&[0xc7]); + e.modrm_pair(i, 0, Rm::Reg(*d))?; + e.imm = Some((v, 4)); + e.finish(i) + } + (Operand::Mem(m), imm) => { + let v = imm_of(imm).ok_or(form(i, "expected a register or an immediate"))?; + let (bytes, extends) = imm_field(w); + check_imm(v, bytes, extends)?; + e.op(&[0xc6 + u8::from(!byte)]); + e.modrm_mem(i, 0, m)?; + e.imm = Some((v, bytes)); + e.finish(i) + } + _ => Err(form(i, "expected a register or a memory destination")), + } +} + +fn segment_move(i: &Insn, mut e: Emit, d: Reg, s: Reg) -> Result { + // The segment moves are sixteen bits wide in the register file but take + // no operand-size prefix, because the other operand's width selects the + // form rather than a prefix. + e.o16 = false; + e.w = false; + if d.class == RegClass::Seg { + if s.class != RegClass::Gpr { + return Err(form(i, "expected a general purpose source")); + } + e.op(&[0x8e]); + e.modrm_pair(i, d.num, Rm::Reg(s))?; + } else { + if d.class != RegClass::Gpr { + return Err(form(i, "expected a general purpose destination")); + } + e.w = d.width == Width::W64; + e.op(&[0x8c]); + e.modrm_pair(i, s.num, Rm::Reg(d))?; + } + e.finish(i) +} + +fn lea(i: &Insn, mut e: Emit) -> Result { + let (dst, src) = two(i)?; + let (Operand::Reg(d), Operand::Mem(m)) = (dst, src) else { + return Err(form(i, "lea takes a register and a memory operand")); + }; + e.width(d.width); + e.note(*d); + e.op(&[0x8d]); + e.modrm_mem(i, rnum(*d), m)?; + e.finish(i) +} + +/// Push, pop, and the indirect branches default to 64-bit operands in long +/// mode, so a 64-bit form needs no REX.W and a 32-bit one cannot be written. +fn stack_width(i: &Insn, w: Width, e: &mut Emit) -> Result<(), AsmError> { + match w { + Width::W64 => Ok(()), + Width::W16 => { + e.o16 = true; + Ok(()) + } + _ => Err(form(i, "long mode pushes and pops 64 or 16 bits, never 32")), + } +} + +fn push(i: &Insn, mut e: Emit) -> Result { + let ops = i.operands(); + let [op] = ops else { + return Err(form(i, "needs one operand")); + }; + match op { + Operand::Reg(r) => { + stack_width(i, r.width, &mut e)?; + if r.class != RegClass::Gpr { + return Err(form(i, "expected a general purpose register")); + } + e.b = r.num >= 8; + e.op(&[0x50 + (r.num & 7)]); + } + Operand::Mem(m) => { + stack_width(i, if m.size == 2 { Width::W16 } else { Width::W64 }, &mut e)?; + e.op(&[0xff]); + e.modrm_mem(i, 6, m)?; + } + other => { + let v = imm_of(other).ok_or(form(i, "expected a register, memory or an immediate"))?; + if (-0x80..=0x7f).contains(&v) { + e.op(&[0x6a]); + e.imm = Some((v, 1)); + } else { + check_imm(v, 4, true)?; + e.op(&[0x68]); + e.imm = Some((v, 4)); + } + } + } + e.finish(i) +} + +fn pop(i: &Insn, mut e: Emit) -> Result { + let ops = i.operands(); + let [op] = ops else { + return Err(form(i, "needs one operand")); + }; + match op { + Operand::Reg(r) => { + stack_width(i, r.width, &mut e)?; + if r.class != RegClass::Gpr { + return Err(form(i, "expected a general purpose register")); + } + e.b = r.num >= 8; + e.op(&[0x58 + (r.num & 7)]); + } + Operand::Mem(m) => { + stack_width(i, if m.size == 2 { Width::W16 } else { Width::W64 }, &mut e)?; + e.op(&[0x8f]); + e.modrm_mem(i, 0, m)?; + } + _ => return Err(form(i, "pop takes a register or a memory operand")), + } + e.finish(i) +} + +fn test(i: &Insn, mut e: Emit) -> Result { + let (dst, src) = two(i)?; + let w = operand_width(i, &[dst, src])?; + e.width(w); + let byte = w == Width::W8; + match (dst, src) { + (rm, Operand::Reg(s)) => { + e.note(*s); + e.op(&[0x84 + u8::from(!byte)]); + e.modrm_pair(i, rnum(*s), rm_of(i, rm)?)?; + } + (rm, imm) => { + let v = imm_of(imm).ok_or(form(i, "expected a register or an immediate"))?; + let (bytes, extends) = imm_field(w); + check_imm(v, bytes, extends)?; + // The accumulator form is a byte shorter and carries no ModRM. + if let Rm::Reg(r) = rm_of(i, rm)? + && r.class == RegClass::Gpr + && r.num == 0 + { + e.note(r); + e.op(&[0xa8 + u8::from(!byte)]); + e.imm = Some((v, bytes)); + return e.finish(i); + } + e.op(&[0xf6 + u8::from(!byte)]); + e.modrm_pair(i, 0, rm_of(i, rm)?)?; + e.imm = Some((v, bytes)); + } + } + e.finish(i) +} + +fn xchg(i: &Insn, mut e: Emit) -> Result { + let (a, b) = two(i)?; + let w = operand_width(i, &[a, b])?; + e.width(w); + let byte = w == Width::W8; + if let (Operand::Reg(x), Operand::Reg(y)) = (a, b) { + // The one-byte accumulator form, except where it would encode 0x90, + // which is `nop` and decodes as `nop`. + let short = |acc: &Reg, other: &Reg| { + !byte + && acc.class == RegClass::Gpr + && acc.num == 0 + && other.class == RegClass::Gpr + && !(other.num == 0 && w == Width::W32) + }; + if short(x, y) { + e.b = y.num >= 8; + e.op(&[0x90 + (y.num & 7)]); + return e.finish(i); + } + if short(y, x) { + e.b = x.num >= 8; + e.op(&[0x90 + (x.num & 7)]); + return e.finish(i); + } + } + // The decoder prints the ModRM reg operand first for a register pair and + // the memory operand first otherwise, so the two orders are read here the + // way they were printed. + let (reg, rm) = match (a, b) { + (Operand::Reg(r), other) if !matches!(other, Operand::Reg(_)) => (*r, rm_of(i, other)?), + (other, Operand::Reg(r)) => (*r, rm_of(i, other)?), + _ => return Err(form(i, "xchg needs at least one register operand")), + }; + e.note(reg); + e.op(&[0x86 + u8::from(!byte)]); + e.modrm_pair(i, rnum(reg), rm)?; + e.finish(i) +} + +fn shift(i: &Insn, mut e: Emit) -> Result { + let n = shift_index(i.mnemonic).ok_or(form(i, "not a shift"))?; + let ops = i.operands(); + let dst = ops.first().ok_or(form(i, "needs a destination"))?; + let w = operand_width(i, &[dst])?; + e.width(w); + let byte = w == Width::W8; + match ops.get(1) { + // The opcode itself is the count of one, which is how the decoder + // prints it: with no second operand at all. + None => { + e.op(&[0xd0 + u8::from(!byte)]); + e.modrm_pair(i, n, rm_of(i, dst)?)?; + } + Some(Operand::Reg(r)) if r.class == RegClass::Gpr && r.num == 1 && r.width == Width::W8 => { + e.op(&[0xd2 + u8::from(!byte)]); + e.modrm_pair(i, n, rm_of(i, dst)?)?; + } + Some(op) => { + let v = imm_of(op).ok_or(form(i, "a shift count is cl or an immediate"))?; + if !(0..=0xff).contains(&v) { + return Err(AsmError::Range { + what: "shift count", + value: v, + low: 0, + high: 0xff, + }); + } + e.op(&[0xc0 + u8::from(!byte)]); + e.modrm_pair(i, n, rm_of(i, dst)?)?; + e.imm = Some((v, 1)); + } + } + if ops.len() > 2 { + return Err(form(i, "a shift takes at most two operands")); + } + e.finish(i) +} + +fn setcc(i: &Insn, mut e: Emit, c: u8) -> Result { + let ops = i.operands(); + let [dst] = ops else { + return Err(form(i, "needs one operand")); + }; + let w = operand_width(i, &[dst])?; + if w != Width::W8 { + return Err(form(i, "setcc writes one byte")); + } + e.op(&[0x0f, 0x90 + c]); + e.modrm_pair(i, 0, rm_of(i, dst)?)?; + e.finish(i) +} + +/// The `reg, r/m` two-byte forms: `cmovcc`, `bsf`, `bsr`. +fn two_byte_rm_reg(i: &Insn, mut e: Emit, op2: u8) -> Result { + let (dst, src) = two(i)?; + let Operand::Reg(d) = dst else { + return Err(form(i, "the destination is a register")); + }; + let w = operand_width(i, &[dst, src])?; + if w == Width::W8 { + return Err(form(i, "this form has no byte width")); + } + e.width(w); + e.note(*d); + e.op(&[0x0f, op2]); + e.modrm_pair(i, rnum(*d), rm_of(i, src)?)?; + e.finish(i) +} + +fn movx(i: &Insn, mut e: Emit) -> Result { + let (dst, src) = two(i)?; + let Operand::Reg(d) = dst else { + return Err(form(i, "the destination is a register")); + }; + let src_bytes = match src { + Operand::Reg(r) => width_bytes(r.width), + Operand::Mem(m) => m.size, + _ => return Err(form(i, "expected a register or a memory source")), + }; + let op2 = match (i.mnemonic, src_bytes) { + ("movzx", 1) => 0xb6u8, + ("movzx", 2) => 0xb7, + ("movsx", 1) => 0xbe, + ("movsx", 2) => 0xbf, + (_, 4) => return Err(form(i, "a 32-bit source widens with movsxd, not movsx")), + _ => { + return Err(AsmError::UnsizedOperand { + mnemonic: i.mnemonic.to_string(), + }); + } + }; + if let Operand::Reg(s) = src { + e.note(*s); + } + e.width(d.width); + e.note(*d); + e.op(&[0x0f, op2]); + e.modrm_pair(i, rnum(*d), rm_of(i, src)?)?; + e.finish(i) +} + +fn movsxd(i: &Insn, mut e: Emit) -> Result { + let (dst, src) = two(i)?; + let Operand::Reg(d) = dst else { + return Err(form(i, "the destination is a register")); + }; + if d.width != Width::W64 { + return Err(form(i, "movsxd writes a 64-bit register")); + } + e.width(Width::W64); + e.op(&[0x63]); + e.modrm_pair(i, rnum(*d), rm_of(i, src)?)?; + e.finish(i) +} + +fn imul(i: &Insn, mut e: Emit) -> Result { + let ops = i.operands(); + match ops.len() { + 1 => unary(i, e), + 2 => two_byte_rm_reg(i, e, 0xaf), + 3 => { + let Operand::Reg(d) = ops[0] else { + return Err(form(i, "the destination is a register")); + }; + let w = operand_width(i, &[&ops[0], &ops[1]])?; + if w == Width::W8 { + return Err(form(i, "the three-operand multiply has no byte width")); + } + e.width(w); + let v = imm_of(&ops[2]).ok_or(form(i, "expected an immediate"))?; + let bytes = if (-0x80..=0x7f).contains(&v) { + e.op(&[0x6b]); + 1u8 + } else { + let (n, extends) = imm_field(w); + check_imm(v, n, extends)?; + e.op(&[0x69]); + n + }; + e.modrm_pair(i, rnum(d), rm_of(i, &ops[1])?)?; + e.imm = Some((v, bytes)); + e.finish(i) + } + _ => Err(form(i, "imul takes one, two or three operands")), + } +} + +fn unary(i: &Insn, mut e: Emit) -> Result { + let n = match i.mnemonic { + "not" => 2u8, + "neg" => 3, + "mul" => 4, + "imul" => 5, + "div" => 6, + _ => 7, + }; + let ops = i.operands(); + let [dst] = ops else { + return Err(form(i, "needs one operand")); + }; + let w = operand_width(i, &[dst])?; + e.width(w); + e.op(&[0xf6 + u8::from(w != Width::W8)]); + e.modrm_pair(i, n, rm_of(i, dst)?)?; + e.finish(i) +} + +fn inc_dec(i: &Insn, mut e: Emit) -> Result { + let ops = i.operands(); + let [dst] = ops else { + return Err(form(i, "needs one operand")); + }; + let w = operand_width(i, &[dst])?; + e.width(w); + e.op(&[0xfe + u8::from(w != Width::W8)]); + e.modrm_pair(i, u8::from(i.mnemonic == "dec"), rm_of(i, dst)?)?; + e.finish(i) +} + +fn bit_test(i: &Insn, mut e: Emit) -> Result { + let (dst, src) = two(i)?; + let n = match i.mnemonic { + "bt" => 4u8, + "bts" => 5, + "btr" => 6, + _ => 7, + }; + let w = operand_width(i, &[dst, src])?; + if w == Width::W8 { + return Err(form(i, "a bit test has no byte width")); + } + e.width(w); + if let Operand::Reg(s) = src + && matches!(s.class, RegClass::Gpr) + { + e.op(&[0x0f, 0x83 + n * 8]); + e.modrm_pair(i, rnum(*s), rm_of(i, dst)?)?; + return e.finish(i); + } + let v = imm_of(src).ok_or(form(i, "expected a register or an immediate"))?; + if !(0..=0xff).contains(&v) { + return Err(AsmError::Range { + what: "bit number", + value: v, + low: 0, + high: 0xff, + }); + } + e.op(&[0x0f, 0xba]); + e.modrm_pair(i, n, rm_of(i, dst)?)?; + e.imm = Some((v, 1)); + e.finish(i) +} + +fn bswap(i: &Insn, mut e: Emit) -> Result { + let ops = i.operands(); + let [Operand::Reg(r)] = ops else { + return Err(form(i, "bswap takes one register")); + }; + if !matches!(r.width, Width::W32 | Width::W64) { + return Err(form(i, "bswap is 32 or 64 bits")); + } + e.width(r.width); + e.b = r.num >= 8; + e.op(&[0x0f, 0xc8 + (r.num & 7)]); + e.finish(i) +} + +fn cmpxchg(i: &Insn, mut e: Emit) -> Result { + let (dst, src) = two(i)?; + let Operand::Reg(s) = src else { + return Err(form(i, "the source is a register")); + }; + let w = operand_width(i, &[dst, src])?; + e.width(w); + e.note(*s); + e.op(&[0x0f, 0xb0 + u8::from(w != Width::W8)]); + e.modrm_pair(i, rnum(*s), rm_of(i, dst)?)?; + e.finish(i) +} + +fn xadd(i: &Insn, mut e: Emit) -> Result { + let (dst, src) = two(i)?; + let Operand::Reg(s) = src else { + return Err(form(i, "the source is a register")); + }; + let w = operand_width(i, &[dst, src])?; + e.width(w); + e.note(*s); + e.op(&[0x0f, 0xc0 + u8::from(w != Width::W8)]); + e.modrm_pair(i, rnum(*s), rm_of(i, dst)?)?; + e.finish(i) +} + +fn int_n(i: &Insn, mut e: Emit) -> Result { + let ops = i.operands(); + let [op] = ops else { + return Err(form(i, "needs one operand")); + }; + let v = imm_of(op).ok_or(form(i, "expected an interrupt number"))?; + if !(0..=0xff).contains(&v) { + return Err(AsmError::Range { + what: "interrupt number", + value: v, + low: 0, + high: 0xff, + }); + } + // `int 3` stays two bytes and `int3` stays one: they raise the same + // exception but they are not the same instruction to a debugger, so + // neither is silently rewritten as the other. + e.op(&[0xcd]); + e.imm = Some((v, 1)); + e.finish(i) +} + +fn ret(i: &Insn, mut e: Emit) -> Result { + match i.operands() { + [] => { + e.op(&[0xc3]); + } + [op] => { + let v = imm_of(op).ok_or(form(i, "expected an immediate"))?; + if !(0..=0xffff).contains(&v) { + return Err(AsmError::Range { + what: "return pop count", + value: v, + low: 0, + high: 0xffff, + }); + } + e.op(&[0xc2]); + e.imm = Some((v, 2)); + } + _ => return Err(form(i, "ret takes at most one operand")), + } + e.finish(i) +} + +fn nop(i: &Insn, mut e: Emit) -> Result { + match i.operands() { + [] => { + e.op(&[0x90]); + e.finish(i) + } + [dst] => { + let w = operand_width(i, &[dst])?; + if w == Width::W8 { + return Err(form(i, "the multi-byte nop is 16, 32 or 64 bits")); + } + e.width(w); + e.op(&[0x0f, 0x1f]); + e.modrm_pair(i, 0, rm_of(i, dst)?)?; + e.finish(i) + } + _ => Err(form(i, "nop takes at most one operand")), + } +} + +// ---------------------------------------------------------------- branching + +/// A displacement from the end of an instruction of `len` bytes. +fn rel(i: &Insn, to: Addr, len: i64) -> i64 { + (to.get().wrapping_sub(i.addr.get()) as i64).wrapping_sub(len) +} + +fn branch_range(i: &Insn, to: Addr, low: i64, high: i64) -> AsmError { + AsmError::BranchRange { + mnemonic: i.mnemonic.to_string(), + from: i.addr.get(), + to: to.get(), + low, + high, + } +} + +fn jcc(i: &Insn, mut e: Emit, c: u8) -> Result { + let ops = i.operands(); + let [op] = ops else { + return Err(form(i, "needs a target")); + }; + let to = addr_of(op).ok_or(form(i, "a conditional jump takes a direct target"))?; + let short = rel(i, to, 2); + if (-0x80..=0x7f).contains(&short) { + e.op(&[0x70 + c]); + e.imm = Some((short, 1)); + return e.finish(i); + } + let near = rel(i, to, 6); + if !(-0x8000_0000..=0x7fff_ffff).contains(&near) { + return Err(branch_range(i, to, -0x8000_0000 + 6, 0x7fff_ffff + 6)); + } + e.op(&[0x0f, 0x80 + c]); + e.imm = Some((near, 4)); + e.finish(i) +} + +fn jmp_call(i: &Insn, mut e: Emit) -> Result { + let ops = i.operands(); + let [op] = ops else { + return Err(form(i, "needs a target")); + }; + let call = i.mnemonic == "call"; + match op { + Operand::Addr(_) | Operand::Imm(_) | Operand::UImm(_) | Operand::Count(_) => { + let to = addr_of(op).expect("matched an address-shaped operand"); + if !call { + let short = rel(i, to, 2); + if (-0x80..=0x7f).contains(&short) { + e.op(&[0xeb]); + e.imm = Some((short, 1)); + return e.finish(i); + } + } + let near = rel(i, to, 5); + if !(-0x8000_0000..=0x7fff_ffff).contains(&near) { + return Err(branch_range(i, to, -0x8000_0000 + 5, 0x7fff_ffff + 5)); + } + e.op(&[if call { 0xe8 } else { 0xe9 }]); + e.imm = Some((near, 4)); + e.finish(i) + } + rm => { + // An indirect branch is 64-bit by default, so its operand needs no + // REX.W and cannot be written 32 bits wide. + let w = operand_width(i, &[rm])?; + if w != Width::W64 { + return Err(form( + i, + "an indirect branch in long mode goes through a 64-bit operand", + )); + } + e.op(&[0xff]); + e.modrm_pair(i, if call { 2 } else { 4 }, rm_of(i, rm)?)?; + e.finish(i) + } + } +} + +// ------------------------------------------------------------------ padding + +/// The canonical multi-byte no-ops, indexed by length minus one. +/// +/// These are the sequences the Intel optimization manual recommends and the +/// ones every toolchain emits, which matters because a patch that pads with an +/// unusual no-op is a patch a later reader has to stop and decode. +const NOPS: [&[u8]; 9] = [ + &[0x90], + &[0x66, 0x90], + &[0x0f, 0x1f, 0x00], + &[0x0f, 0x1f, 0x40, 0x00], + &[0x0f, 0x1f, 0x44, 0x00, 0x00], + &[0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00], + &[0x0f, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00], + &[0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00], + &[0x66, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00], +]; + +/// Fill `len` bytes with as few no-ops as possible. +pub fn padding(len: usize) -> Vec { + let mut out = Vec::with_capacity(len); + let mut left = len; + while left > 0 { + let take = left.min(NOPS.len()); + out.extend_from_slice(NOPS[take - 1]); + left -= take; + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use e5r_arch::insn::Flow; + + fn asm(text: &str) -> Vec { + asm_at(text, 0x1000) + } + + fn asm_at(text: &str, at: u64) -> Vec { + crate::assemble(&e5r_core::Arch::X86_64, text, Addr(at)) + .unwrap_or_else(|e| panic!("{text}: {e}")) + .bytes() + .to_vec() + } + + fn refused(text: &str) -> AsmError { + crate::assemble(&e5r_core::Arch::X86_64, text, Addr(0x1000)).expect_err("should refuse") + } + + #[test] + fn the_mnemonic_table_is_sorted_and_unique() { + for w in MNEMONICS.windows(2) { + assert!(w[0] < w[1], "{} then {}", w[0], w[1]); + } + } + + #[test] + fn rex_is_computed_from_the_registers() { + assert_eq!(asm("mov rax, rcx"), [0x48, 0x89, 0xc8]); + assert_eq!(asm("mov r8, r15"), [0x4d, 0x89, 0xf8]); + assert_eq!(asm("mov eax, ecx"), [0x89, 0xc8]); + // spl exists only with REX present, even though no extension bit is. + assert_eq!(asm("mov spl, al"), [0x40, 0x88, 0xc4]); + assert_eq!(asm("mov ah, al"), [0x88, 0xc4]); + } + + #[test] + fn a_high_byte_register_and_rex_cannot_meet() { + assert!(matches!( + refused("mov ah, r8b"), + AsmError::UnsupportedForm { .. } + )); + } + + #[test] + fn immediates_take_the_shortest_form_that_holds_them() { + assert_eq!(asm("add rax, 0x1"), [0x48, 0x83, 0xc0, 0x01]); + assert_eq!(asm("add eax, 0x1234"), [0x05, 0x34, 0x12, 0x00, 0x00]); + assert_eq!(asm("add ecx, 0x1234"), [0x81, 0xc1, 0x34, 0x12, 0x00, 0x00]); + assert_eq!(asm("mov eax, 0x1"), [0xb8, 0x01, 0x00, 0x00, 0x00]); + assert_eq!( + asm("mov rax, 0x1"), + [0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00] + ); + } + + #[test] + fn a_wide_immediate_needs_the_ten_byte_move() { + assert_eq!( + asm("movabs rax, 0x1122334455667788"), + [0x48, 0xb8, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11] + ); + // A value that does not fit a sign-extended doubleword picks the same + // form without being asked. + assert_eq!(asm("mov rax, 0xffffffff")[1], 0xb8); + } + + #[test] + fn addressing_modes_build_the_right_sib() { + assert_eq!(asm("mov eax, dword ptr [rbx]"), [0x8b, 0x03]); + assert_eq!(asm("mov eax, dword ptr [rsp]"), [0x8b, 0x04, 0x24]); + assert_eq!(asm("mov eax, dword ptr [rbp]"), [0x8b, 0x45, 0x00]); + assert_eq!( + asm("mov eax, dword ptr [rbx + 4*rcx + 0x10]"), + [0x8b, 0x44, 0x8b, 0x10] + ); + assert_eq!( + asm("lea rax, [rip + 0x10]"), + [0x48, 0x8d, 0x05, 0x10, 0, 0, 0] + ); + assert_eq!(asm("mov eax, dword ptr [r12]"), [0x41, 0x8b, 0x04, 0x24]); + } + + #[test] + fn rsp_cannot_be_an_index() { + assert!(matches!( + refused("mov eax, dword ptr [rax + 1*rsp]"), + AsmError::UnsupportedForm { .. } + )); + } + + #[test] + fn a_short_jump_is_chosen_when_it_reaches() { + assert_eq!(asm_at("jmp 0x1002", 0x1000), [0xeb, 0x00]); + assert_eq!(asm_at("jmp 0x2000", 0x1000), [0xe9, 0xfb, 0x0f, 0x00, 0x00]); + assert_eq!(asm_at("je 0x1002", 0x1000), [0x74, 0x00]); + assert_eq!( + asm_at("je 0x2000", 0x1000), + [0x0f, 0x84, 0xfa, 0x0f, 0x00, 0x00] + ); + assert_eq!( + asm_at("call 0x2000", 0x1000), + [0xe8, 0xfb, 0x0f, 0x00, 0x00] + ); + } + + #[test] + fn an_unreachable_branch_names_its_range() { + let e = crate::assemble(&e5r_core::Arch::X86_64, "call 0x90000000", Addr(0)) + .expect_err("out of range"); + assert!(matches!(e, AsmError::BranchRange { .. }), "{e}"); + // A truncated displacement is the one mistake this must never make. + assert!(crate::assemble(&e5r_core::Arch::X86_64, "call 0x7ffffffc", Addr(0)).is_ok()); + } + + #[test] + fn a_memory_operand_with_no_width_is_refused_by_name() { + assert!(matches!( + refused("mov [rax], 0x1"), + AsmError::UnsizedOperand { .. } + )); + } + + #[test] + fn the_shift_count_forms_stay_distinct() { + // The decoder prints the one-byte shift with no count at all, so the + // two spellings have to encode differently or the round trip loses. + assert_eq!(asm("shl eax"), [0xd1, 0xe0]); + assert_eq!(asm("shl eax, 0x1"), [0xc1, 0xe0, 0x01]); + assert_eq!(asm("shl eax, cl"), [0xd3, 0xe0]); + assert_eq!(asm("sar rax, 0x3f"), [0x48, 0xc1, 0xf8, 0x3f]); + } + + #[test] + fn the_odds_and_ends_a_patch_needs() { + assert_eq!(asm("nop"), [0x90]); + assert_eq!(asm("int3"), [0xcc]); + assert_eq!(asm("ret"), [0xc3]); + assert_eq!(asm("ret 0x8"), [0xc2, 0x08, 0x00]); + assert_eq!(asm("leave"), [0xc9]); + assert_eq!(asm("endbr64"), [0xf3, 0x0f, 0x1e, 0xfa]); + assert_eq!(asm("syscall"), [0x0f, 0x05]); + assert_eq!(asm("push rbp"), [0x55]); + assert_eq!(asm("push r12"), [0x41, 0x54]); + assert_eq!(asm("pop rbp"), [0x5d]); + assert_eq!(asm("sete al"), [0x0f, 0x94, 0xc0]); + assert_eq!(asm("movzx eax, byte ptr [rdi]"), [0x0f, 0xb6, 0x07]); + assert_eq!(asm("movsxd rax, ecx"), [0x48, 0x63, 0xc1]); + assert_eq!(asm("imul eax, ecx, 0x10"), [0x6b, 0xc1, 0x10]); + assert_eq!(asm("xchg rax, rcx"), [0x48, 0x91]); + assert_eq!(asm("test eax, eax"), [0x85, 0xc0]); + assert_eq!(asm("cmp byte ptr [rdi], 0x0"), [0x80, 0x3f, 0x00]); + assert_eq!(asm("cmp al, 0x1"), [0x3c, 0x01]); + } + + #[test] + fn a_prefix_rides_ahead_of_the_instruction() { + assert_eq!( + asm("lock\tcmpxchg dword ptr [rdi], esi"), + [0xf0, 0x0f, 0xb1, 0x37] + ); + assert_eq!(asm("pause"), [0xf3, 0x90]); + } + + #[test] + fn padding_is_made_of_canonical_no_ops() { + for n in 1..64usize { + let p = padding(n); + assert_eq!(p.len(), n, "padding {n}"); + // Every byte of it has to decode as a no-op, or the fall-through + // path runs into something. + let mut at = 0usize; + while at < p.len() { + let d = e5r_arch::x86::decode(&p[at..], Addr(0x1000 + at as u64)) + .unwrap_or_else(|| panic!("padding {n} at {at} does not decode")); + assert_eq!(d.mnemonic, "nop"); + at += d.len as usize; + } + } + } + + #[test] + fn an_insn_built_by_hand_encodes_without_text() { + let mut i = Insn::new(Addr(0x1000), 0, "ret", Flow::Return); + assert_eq!(encode(&i).unwrap().bytes(), [0xc3]); + i = Insn::new(Addr(0x1000), 0, "push", Flow::Next); + i.push(Operand::Reg(Reg::gpr(5, Width::W64))); + assert_eq!(encode(&i).unwrap().bytes(), [0x55]); + } +} diff --git a/crates/e5r-asm/tests/common/mod.rs b/crates/e5r-asm/tests/common/mod.rs new file mode 100644 index 0000000..e093857 --- /dev/null +++ b/crates/e5r-asm/tests/common/mod.rs @@ -0,0 +1,112 @@ +//! Shared corpus plumbing for the assembler's gates. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_arch::insn::Insn; +use e5r_core::{Addr, Arch}; + +/// One instruction as an external disassembler reported it. +pub struct Line { + pub addr: u64, + pub bytes: Vec, +} + +pub fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// The disassembler used only to find instruction boundaries. Decoding is +/// still e5r's own; this says where one instruction ends and the next starts. +pub fn objdump(arch: &Arch) -> Option<&'static str> { + let candidates: &[&str] = match arch { + Arch::X86_64 => &["llvm-objdump-18", "llvm-objdump-15", "llvm-objdump"], + // The prefixed binutils first: a plain `objdump` built for another + // host reads an AArch64 file and prints no instructions, which reads + // here as an oracle that had nothing to say rather than one that + // cannot answer. + _ => &[ + "aarch64-linux-gnu-objdump", + "objdump", + "llvm-objdump-18", + "llvm-objdump", + ], + }; + candidates + .iter() + .copied() + .find(|c| Command::new(c).arg("--version").output().is_ok()) +} + +pub fn disassemble(tool: &str, path: &Path, arch: &Arch) -> Vec { + let mut cmd = Command::new(tool); + cmd.arg("-d"); + if *arch == Arch::X86_64 && tool.starts_with("llvm") { + cmd.arg("--x86-asm-syntax=intel"); + } + let Ok(out) = cmd.arg(path).output() else { + return Vec::new(); + }; + let text = String::from_utf8_lossy(&out.stdout); + let mut lines = Vec::new(); + for l in text.lines() { + // Both spellings of the listing: llvm's " 0: 85 ff \ttest..." and + // binutils' " 0:\t85 ff \ttest...". + let Some((left, rest)) = l.split_once(':') else { + continue; + }; + let Ok(addr) = u64::from_str_radix(left.trim(), 16) else { + continue; + }; + let raw = match rest.find('\t') { + Some(i) if rest[..i].trim().is_empty() => match rest[i + 1..].find('\t') { + Some(j) => &rest[i + 1..i + 1 + j], + None => continue, + }, + Some(i) => &rest[..i], + None => continue, + }; + // binutils prints an A64 instruction as one eight-digit word and an + // x86 one as separated bytes, so both spellings are read here. + let fields: Vec<&str> = raw.split_whitespace().collect(); + let bytes = if fields.len() == 1 && fields[0].len() == 8 { + match u32::from_str_radix(fields[0], 16) { + Ok(w) => w.to_le_bytes().to_vec(), + Err(_) => continue, + } + } else { + let parsed: Option> = fields + .iter() + .map(|h| u8::from_str_radix(h, 16).ok()) + .collect(); + match parsed { + Some(b) => b, + None => continue, + } + }; + if bytes.is_empty() || bytes.len() > 15 { + continue; + } + lines.push(Line { addr, bytes }); + } + lines +} + +pub fn decode_for(arch: &Arch, bytes: &[u8], addr: Addr) -> Option { + e5r_arch::decode(arch, bytes, addr) +} + +/// The text the assembler has to accept, which is what `e5r disas` prints. +pub fn render(arch: &Arch, i: &Insn) -> String { + e5r_arch::format(arch, i, false) +} + +/// Two instructions compare by what they are, not by where they were found or +/// how long the encoding that produced them happened to be. +pub fn normalize(i: &Insn) -> Insn { + let mut c = *i; + c.len = 0; + c.flow = e5r_arch::insn::Flow::Next; + c +} diff --git a/crates/e5r-asm/tests/fuzz.rs b/crates/e5r-asm/tests/fuzz.rs new file mode 100644 index 0000000..f01618a --- /dev/null +++ b/crates/e5r-asm/tests/fuzz.rs @@ -0,0 +1,253 @@ +//! Mutation fuzzing of the assembler's text path, in the ordinary test run. +//! +//! Modelled on `e5r-format`'s loader fuzz for the same reason: a fuzz target +//! that needs nightly and a separate invocation finds nothing on a machine +//! where nobody runs it. The contract checked here is the one the error model +//! promises, and it is the whole reason the parser has its own bounds: given +//! any text at all, the assembler returns bytes or a typed error, and never +//! panics, hangs, or allocates from a number the text chose. + +use std::time::{Duration, Instant}; + +use e5r_asm::{assemble, assemble_all, error::MAX_TEXT, pad, parse}; +use e5r_core::{Addr, Arch}; + +/// How long each target gets. Enough to find a systematic fault, short enough +/// that nobody is tempted to skip the suite. +const BUDGET: Duration = Duration::from_millis(1200); + +/// A small deterministic generator, so a failure is reproducible from its seed. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545_f491_4f6c_dd1d) + } + + fn below(&mut self, n: usize) -> usize { + if n == 0 { + 0 + } else { + (self.next() % n as u64) as usize + } + } + + fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T { + &xs[self.below(xs.len())] + } +} + +/// Real disassembly, which is what a user pastes in and therefore the only +/// interesting seed corpus. +const SEEDS: &[&str] = &[ + "mov\trax, rcx", + "mov\teax, dword ptr [rbx + 4*rcx + 0x10]", + "lea\trax, [rip + 0x2a]", + "movabs\trax, 0x1122334455667788", + "push\trbp", + "jmp\t0x401000", + "je\t0x401000", + "call\t0x401000", + "nop\tword ptr cs:[rax + rax]", + "shl\teax, 0x3", + "imul\teax, ecx, 0x10", + "test\tbyte ptr [rdi], 0x1", + "setne\tal", + "movzx\teax, word ptr [rsi]", + "rep\t\tstosq", + "lock\tcmpxchg dword ptr [rdi], esi", + "add\tx0, x1, x2, lsl #3", + "ldr\tx0, [sp, #16]", + "stp\tx29, x30, [sp, #-16]!", + "ldrb\tw0, [x1, x2, sxtw #0]", + "b.eq\t0x401000", + "cbz\tx0, 0x401000", + "tbnz\tw0, #3, 0x401000", + "adrp\tx0, 0x402000", + "movk\tw0, #0x5a5a, lsl #16", + "ret", + "svc\t#0", + "bti\tc", + "dmb\tish", + "csel\tx0, x1, x2, eq", + "ubfx\tx0, x1, #4, #8", + "", + " \t ", + "# a comment only", +]; + +const BYTES: &[u8] = b"0123456789abcdefxyzXABCDEF[](),.:;#+-*!/%$_ \t\n\r\\\"'{}<>=|&^~?@`"; + +fn mutate(seed: &str, rng: &mut Rng) -> String { + let mut v: Vec = seed.as_bytes().to_vec(); + match rng.below(7) { + // Truncate, which is what a half-pasted line looks like. + 0 => { + let n = if v.is_empty() { 0 } else { rng.below(v.len()) }; + v.truncate(n); + } + // Replace a byte with another printable one. + 1 if !v.is_empty() => { + let i = rng.below(v.len()); + v[i] = *rng.pick(BYTES); + } + // Insert a byte. + 2 => { + let i = rng.below(v.len() + 1); + v.insert(i, *rng.pick(BYTES)); + } + // Delete a byte. + 3 if !v.is_empty() => { + let i = rng.below(v.len()); + v.remove(i); + } + // Repeat a run, which is how a number becomes enormous. + 4 if !v.is_empty() => { + let i = rng.below(v.len()); + let n = rng.below(40); + let b = v[i]; + for _ in 0..n { + v.insert(i, b); + } + } + // Splice two seeds together. + 5 => { + v.extend_from_slice(b", "); + v.extend_from_slice(rng.pick(SEEDS).as_bytes()); + } + // Scramble, which reaches the shapes a targeted mutation never does. + _ => { + for b in v.iter_mut() { + if rng.below(4) == 0 { + *b = *rng.pick(BYTES); + } + } + } + } + String::from_utf8_lossy(&v).into_owned() +} + +fn arches() -> [Arch; 2] { + [Arch::X86_64, Arch::AArch64] +} + +#[test] +fn mutated_assembly_text_never_panics() { + let mut rng = Rng(0x5eed_1234_abcd_0101); + let start = Instant::now(); + let mut n = 0u64; + while start.elapsed() < BUDGET { + let text = mutate(rng.pick(SEEDS), &mut rng); + let arch = arches()[rng.below(2)].clone(); + let at = Addr(rng.next()); + // The contract: bytes or a typed error, nothing else. A successful + // encode has to fit the architecture's own limit. + if let Ok(e) = assemble(&arch, &text, at) { + assert!(!e.is_empty() && e.len() <= e5r_asm::MAX_INSN, "{text:?}"); + if arch == Arch::AArch64 { + assert_eq!(e.len(), 4, "{text:?}"); + } + } + let _ = parse(&arch, &text, at); + n += 1; + } + assert!( + n > 1000, + "only {n} cases in {BUDGET:?}; something is very slow" + ); + println!("assembler fuzz: {n} mutated lines, no panics"); +} + +#[test] +fn arbitrary_bytes_never_panic() { + let mut rng = Rng(0x5eed_dead_beef_0202); + let start = Instant::now(); + let mut n = 0u64; + while start.elapsed() < BUDGET { + let len = rng.below(MAX_TEXT * 2); + let text: String = (0..len).map(|_| *rng.pick(BYTES) as char).collect(); + let arch = arches()[rng.below(2)].clone(); + let _ = assemble(&arch, &text, Addr(rng.next())); + n += 1; + } + assert!(n > 500, "only {n} cases in {BUDGET:?}"); + println!("assembler fuzz: {n} random lines, no panics"); +} + +/// Every input is bounded before anything is allocated from it, so the time +/// one line takes cannot depend on what the line says. +#[test] +fn a_hostile_line_is_bounded_in_time_and_length() { + let cases = [ + "mov ".to_string() + &"rax, ".repeat(4000), + "mov rax, 0x".to_string() + &"f".repeat(4000), + "[".repeat(4000), + "mov rax, ".to_string() + &"[".repeat(400), + "b".to_string() + &".".repeat(4000), + "\0".repeat(4000), + "mov\trax, [rax + ".to_string() + &"1*rcx + ".repeat(1000) + "0]", + ]; + for text in cases { + for arch in arches() { + let started = Instant::now(); + let r = assemble(&arch, &text, Addr(0x1000)); + assert!( + started.elapsed() < Duration::from_millis(100), + "{arch:?} took too long on {} bytes", + text.len() + ); + // Anything this long is refused, and the refusal says why. + if text.len() > MAX_TEXT { + assert!(r.is_err(), "{arch:?} accepted {} bytes", text.len()); + } + } + } +} + +/// A multi-line paste is the same contract, once per line. +#[test] +fn a_block_of_text_assembles_line_by_line() { + let block = "push\trbp\nmov\trbp, rsp\n# a note\n\nsub\trsp, 0x20\npop\trbp\nret\n"; + let bytes = assemble_all(&Arch::X86_64, block, Addr(0x1000)).expect("block"); + assert_eq!( + bytes, + [0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20, 0x5d, 0xc3] + ); + + let a64 = "stp\tx29, x30, [sp, #-16]!\nmov\tx29, sp\nldp\tx29, x30, [sp], #16\nret\n"; + let bytes = assemble_all(&Arch::AArch64, a64, Addr(0x1000)).expect("block"); + assert_eq!(bytes.len(), 16); + + let mut rng = Rng(0x5eed_0f0f_0303); + for _ in 0..2000 { + let mut block = String::new(); + for _ in 0..rng.below(8) { + block.push_str(&mutate(rng.pick(SEEDS), &mut rng)); + block.push('\n'); + } + let arch = arches()[rng.below(2)].clone(); + let _ = assemble_all(&arch, &block, Addr(rng.next())); + } +} + +/// Padding is exact or it is refused; a patch that pads the wrong length has +/// left a hole in the middle of a function. +#[test] +fn padding_is_exactly_the_length_asked_for() { + for n in 0..256usize { + let p = pad(&Arch::X86_64, n).expect("x86 pads any length"); + assert_eq!(p.len(), n); + match pad(&Arch::AArch64, n) { + Ok(p) => { + assert_eq!(n % 4, 0); + assert_eq!(p.len(), n); + } + Err(_) => assert_ne!(n % 4, 0), + } + } +} diff --git a/crates/e5r-asm/tests/oracle.rs b/crates/e5r-asm/tests/oracle.rs new file mode 100644 index 0000000..303bc76 --- /dev/null +++ b/crates/e5r-asm/tests/oracle.rs @@ -0,0 +1,469 @@ +//! G2 for the assembler: the system assembler is asked the same question. +//! +//! The round-trip gate proves the encoder agrees with e5r's own decoder, +//! which would still be satisfied by two halves that are wrong in the same +//! direction. This one sends the same text to `as` and compares the bytes, +//! so an error would have to be shared with binutils to survive. +//! +//! A difference in bytes is not automatically a fault: `add eax, 0x1` has +//! several correct encodings and only one can be emitted. Those are counted +//! and reported. A difference that decodes to a different instruction is a +//! bug and fails the test. +//! +//! Each instruction is placed at its own sixteen-byte slot, so one assembler +//! run covers thousands of them and the k-th slot is the k-th instruction +//! whatever length either side chose. Instructions whose encoding depends on +//! where they sit are excluded here and covered by +//! [`branch_displacements_match_the_system_assembler`], which places them at +//! addresses it controls. + +mod common; + +use std::collections::BTreeMap; +use std::path::Path; +use std::process::Command; + +use common::{corpus, decode_for, disassemble, normalize, objdump, render}; + +use e5r_asm::assemble; +use e5r_core::{Addr, Arch}; + +/// Bytes per slot. Longer than the longest x86 encoding, and a power of two so +/// `.p2align` states it directly. +const SLOT: u64 = 16; + +/// The system assembler for `arch`, and the flags that make it read what the +/// printers emit. +fn assembler(arch: &Arch) -> Option<(&'static str, Vec)> { + match arch { + Arch::X86_64 => { + for (tool, args) in [ + ("clang", vec!["-target", "x86_64-linux-gnu", "-c"]), + ("as", vec!["--64"]), + ] { + if Command::new(tool).arg("--version").output().is_ok() { + return Some((tool, args.into_iter().map(str::to_string).collect())); + } + } + None + } + Arch::AArch64 => ["aarch64-linux-gnu-as", "as"] + .into_iter() + .find(|t| Command::new(t).arg("--version").output().is_ok()) + .map(|t| (t, Vec::new())), + _ => None, + } +} + +/// What `.org` pads a gap with, chosen so the gap decodes in step. +fn fill(arch: &Arch) -> &'static str { + match arch { + Arch::X86_64 => ", 0x90", + _ => "", + } +} + +fn header(arch: &Arch) -> &'static str { + match arch { + // The printers emit Intel syntax with no register sigils, which is not + // what `as` defaults to. + Arch::X86_64 => ".intel_syntax noprefix\n.text\n", + _ => ".text\n", + } +} + +/// Run the system assembler over `body`, returning the object file path. +fn run_assembler(arch: &Arch, body: &str, dir: &Path) -> Option { + let (tool, args) = assembler(arch)?; + let src = dir.join("oracle.s"); + let obj = dir.join("oracle.o"); + std::fs::write(&src, body).ok()?; + let out = Command::new(tool) + .args(&args) + .arg(&src) + .arg("-o") + .arg(&obj) + .output() + .ok()?; + if !out.status.success() { + eprintln!( + "system assembler refused the whole file:\n{}", + String::from_utf8_lossy(&out.stderr) + .lines() + .take(10) + .collect::>() + .join("\n") + ); + return None; + } + Some(obj) +} + +/// The line numbers `as` or `clang` complained about, one-based. +fn rejected_lines(arch: &Arch, body: &str, dir: &Path) -> Vec { + let Some((tool, args)) = assembler(arch) else { + return Vec::new(); + }; + let src = dir.join("probe.s"); + if std::fs::write(&src, body).is_err() { + return Vec::new(); + } + let Ok(out) = Command::new(tool) + .args(&args) + .arg(&src) + .arg("-o") + .arg(dir.join("probe.o")) + .output() + else { + return Vec::new(); + }; + let text = String::from_utf8_lossy(&out.stderr); + let mut bad = Vec::new(); + for l in text.lines() { + // "probe.s:17: Error: ..." from as, "probe.s:17:5: error: ..." from clang. + let Some(rest) = l.split(".s:").nth(1) else { + continue; + }; + let n: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect(); + if let Ok(n) = n.parse::() { + bad.push(n); + } + } + bad.sort_unstable(); + bad.dedup(); + bad +} + +#[derive(Default)] +struct Tally { + compared: usize, + same: usize, + alternative: BTreeMap, + wrong: Vec, + unassemblable: usize, +} + +/// True when the bytes depend on where the instruction sits. +fn position_dependent(arch: &Arch, i: &e5r_arch::insn::Insn) -> bool { + if matches!( + i.flow, + e5r_arch::insn::Flow::Branch(_) + | e5r_arch::insn::Flow::CondBranch(_) + | e5r_arch::insn::Flow::Call(_) + ) { + return true; + } + i.operands().iter().any(|o| match o { + e5r_arch::insn::Operand::Addr(_) => true, + e5r_arch::insn::Operand::Mem(m) => m.is_pc_relative(), + _ => false, + }) + // `adrp` and the literal loads carry a target without a branching flow. + || matches!(arch, Arch::AArch64) && matches!(i.mnemonic, "adr" | "adrp") +} + +fn compare(arch: &Arch, texts: &[(String, Vec)], dir: &Path, t: &mut Tally) { + if texts.is_empty() { + return; + } + // Some of what the printers emit is not what `as` accepts; those lines are + // dropped rather than allowed to fail the whole file. Two passes is enough + // in practice and keeps the runtime bounded. + let mut kept: Vec = (0..texts.len()).collect(); + for _ in 0..2 { + let body = source(arch, texts, &kept); + let bad = rejected_lines(arch, &body, dir); + if bad.is_empty() { + break; + } + // Line one is the syntax directive, so the k-th instruction is on line + // `2 + 2 * k`: a `.p2align` and then the instruction itself. + let drop: std::collections::BTreeSet = bad + .iter() + .filter_map(|n| n.checked_sub(3).map(|d| d / 2)) + .collect(); + t.unassemblable += drop.len(); + kept = kept + .iter() + .enumerate() + .filter(|(k, _)| !drop.contains(k)) + .map(|(_, v)| *v) + .collect(); + } + if kept.is_empty() { + return; + } + let body = source(arch, texts, &kept); + let Some(obj) = run_assembler(arch, &body, dir) else { + return; + }; + let Some(tool) = objdump(arch) else { return }; + let lines = disassemble(tool, &obj, arch); + let mut at_slot: BTreeMap> = BTreeMap::new(); + for l in lines { + if l.addr % SLOT == 0 { + at_slot.entry(l.addr).or_insert(l.bytes); + } + } + for (k, ix) in kept.iter().enumerate() { + let Some(theirs) = at_slot.get(&(k as u64 * SLOT)) else { + continue; + }; + let (text, ours) = &texts[*ix]; + t.compared += 1; + if theirs == ours { + t.same += 1; + continue; + } + let mine = decode_for(arch, ours, Addr(0)); + let yours = decode_for(arch, theirs, Addr(0)); + match (mine, yours) { + (Some(a), Some(b)) if normalize(&a) == normalize(&b) => { + *t.alternative + .entry(format!( + "{}\t{} bytes for {}", + a.mnemonic, + ours.len(), + theirs.len() + )) + .or_default() += 1; + } + _ => t + .wrong + .push(format!("{text:?}: e5r {ours:02x?}, system {theirs:02x?}")), + } + } +} + +fn source(arch: &Arch, texts: &[(String, Vec)], kept: &[usize]) -> String { + let mut body = String::from(header(arch)); + for ix in kept { + body.push_str(".p2align 4\n"); + // A tab separates the mnemonic from its operands in what the printers + // emit, which is exactly what an assembler wants anyway. + body.push_str(&texts[*ix].0); + body.push('\n'); + } + body +} + +fn run(arch: &Arch, tag: &str) { + let Some(dir) = corpus() else { + eprintln!("no fixtures, skipping"); + return; + }; + if assembler(arch).is_none() { + eprintln!("no system assembler for {arch:?}, skipping"); + return; + } + let Some(tool) = objdump(arch) else { + eprintln!("no disassembler for {arch:?}, skipping"); + return; + }; + let tmp = std::env::temp_dir().join(format!("e5r-asm-oracle-{tag}")); + let _ = std::fs::create_dir_all(&tmp); + + // One entry per distinct text, so the oracle spends its budget on + // distinct forms rather than on the same prologue a thousand times. + let mut seen: BTreeMap> = BTreeMap::new(); + for e in std::fs::read_dir(&dir).expect("fixtures").flatten() { + let p = e.path(); + let Some(n) = p.file_name().and_then(|n| n.to_str()) else { + continue; + }; + if !n.contains(tag) || !p.is_file() { + continue; + } + for l in disassemble(tool, &p, arch) { + let Some(insn) = decode_for(arch, &l.bytes, Addr(l.addr)) else { + continue; + }; + if insn.len as usize != l.bytes.len() || position_dependent(arch, &insn) { + continue; + } + let text = render(arch, &insn); + if seen.contains_key(&text) { + continue; + } + let Ok(ours) = assemble(arch, &text, Addr(0)) else { + continue; + }; + seen.insert(text, ours.bytes().to_vec()); + } + } + let texts: Vec<(String, Vec)> = seen.into_iter().collect(); + let mut t = Tally::default(); + // In batches, so one line the system assembler cannot parse costs a batch + // rather than the corpus. + for chunk in texts.chunks(400) { + compare(arch, chunk, &tmp, &mut t); + } + println!( + "{arch:?} oracle: distinct forms {}, compared {}, identical {}, alternative {}, unparsed by the system assembler {}, wrong {}", + texts.len(), + t.compared, + t.same, + t.alternative.values().sum::(), + t.unassemblable, + t.wrong.len() + ); + let mut alts: Vec<_> = t.alternative.iter().collect(); + alts.sort_by_key(|(_, n)| std::cmp::Reverse(**n)); + for (k, n) in alts.iter().take(25) { + println!(" alternative {n:>5} {k}"); + } + for w in t.wrong.iter().take(25) { + println!(" WRONG {w}"); + } + assert!( + t.wrong.is_empty(), + "{} instructions disagree with the system assembler in meaning, not only in encoding", + t.wrong.len() + ); + assert!(t.compared > 100, "the oracle compared almost nothing"); +} + +#[test] +fn x86_64_agrees_with_the_system_assembler() { + run(&Arch::X86_64, ".x64."); +} + +#[test] +fn aarch64_agrees_with_the_system_assembler() { + run(&Arch::AArch64, ".a64."); +} + +/// Branches, placed where this test says rather than where a fixture put them. +/// +/// `.org` gives the system assembler the same addresses e5r was told, and the +/// targets are real labels in the same section so the assembler resolves them +/// itself rather than emitting a relocation. This is where the short-form +/// choice is checked: an assembler that always emitted the near form would +/// pass every other test in this file. +/// +/// The displacements straddle the byte-displacement boundary on purpose, and +/// none of them is small enough to land a label inside another instruction, +/// which is what would make `.org` walk backwards. +#[test] +fn branch_displacements_match_the_system_assembler() { + const BASE: u64 = 0x4000; + const STRIDE: u64 = 0x100; + for (arch, mnemonics, spread) in [ + ( + Arch::X86_64, + &["jmp", "call", "je", "jne", "jg", "jbe"][..], + &[ + -0x1000i64, -200, -127, -126, 0x20, 0x40, 129, 130, 200, 0x1000, 0x2000, + ][..], + ), + ( + Arch::AArch64, + &["b", "bl", "b.eq", "b.ne", "cbz", "cbnz", "tbz"][..], + &[-0x2000i64, -0x1000, -8, 8, 0x1000, 0x2000][..], + ), + ] { + if assembler(&arch).is_none() { + eprintln!("no system assembler for {arch:?}, skipping"); + continue; + } + let tmp = std::env::temp_dir().join("e5r-asm-oracle-branch"); + let _ = std::fs::create_dir_all(&tmp); + + // Operands the branch takes ahead of its target, so the compare and + // test forms are exercised too. + let lead = |m: &str| match m { + "cbz" | "cbnz" => "x0, ", + "tbz" | "tbnz" => "x0, #3, ", + _ => "", + }; + + let mut want: Vec<(String, Vec, u64)> = Vec::new(); + let mut labels: BTreeMap> = BTreeMap::new(); + let mut code: BTreeMap = BTreeMap::new(); + let mut at = BASE; + for m in mnemonics { + for d in spread { + let target = at.wrapping_add(*d as u64); + let text = format!("{m}\t{}{target:#x}", lead(m)); + let Ok(e) = assemble(&arch, &text, Addr(at)) else { + continue; + }; + let label = format!("t{}", want.len()); + code.insert(at, format!("{m}\t{}{label}", lead(m))); + labels.entry(target).or_default().push(label); + want.push((text, e.bytes().to_vec(), at)); + at += STRIDE; + } + } + let mut body = String::from(header(&arch)); + let mut addrs: Vec = labels.keys().chain(code.keys()).copied().collect(); + addrs.sort_unstable(); + addrs.dedup(); + for a in addrs { + // Filling the gaps with one-byte no-ops rather than zeroes keeps + // the disassembler in step: a five-byte instruction followed by an + // odd number of zero bytes desynchronizes it, and the next + // instruction is then never reported at the address it is at. + body.push_str(&format!(".org {a}{}\n", fill(&arch))); + for l in labels.get(&a).into_iter().flatten() { + body.push_str(&format!("{l}:\n")); + } + if let Some(c) = code.get(&a) { + body.push_str(c); + body.push('\n'); + } + } + let Some(obj) = run_assembler(&arch, &body, &tmp) else { + panic!("the system assembler refused the branch file"); + }; + let tool = objdump(&arch).expect("disassembler"); + let got: BTreeMap> = disassemble(tool, &obj, &arch) + .into_iter() + .map(|l| (l.addr, l.bytes)) + .collect(); + let mut checked = 0usize; + let mut differ = Vec::new(); + for (text, ours, at) in &want { + let Some(theirs) = got.get(at) else { + continue; + }; + checked += 1; + if theirs != ours { + differ.push(format!( + "{text} at {at:#x}: e5r {ours:02x?}, system {theirs:02x?}" + )); + } + } + assert!(differ.is_empty(), "{}", differ.join("\n")); + println!("{arch:?}: {checked} branch displacements match"); + assert!( + checked > 10, + "{arch:?} branch oracle compared almost nothing" + ); + } +} + +/// A target the encoding cannot reach is an error that says so, never a +/// displacement with its top bits thrown away. +#[test] +fn an_unreachable_branch_is_refused_rather_than_truncated() { + use e5r_asm::AsmError; + for (arch, text, from, to) in [ + (Arch::AArch64, "b.eq\t0x200000", 0u64, 0x200000u64), + (Arch::AArch64, "cbz\tx0, 0x200000", 0, 0x200000), + (Arch::AArch64, "tbz\tx0, #1, 0x20000", 0, 0x20000), + (Arch::AArch64, "b\t0x10000000", 0, 0x10000000), + (Arch::X86_64, "call\t0x90000000", 0, 0x90000000), + ] { + let e = assemble(&arch, text, Addr(from)).expect_err(text); + match e { + AsmError::BranchRange { from: f, to: t, .. } => { + assert_eq!((f, t), (from, to), "{text}"); + } + other => panic!("{text}: expected a branch range error, got {other}"), + } + } + // One inside the range, so the test cannot pass by refusing everything. + assert!(assemble(&Arch::AArch64, "b.eq\t0x1000", Addr(0)).is_ok()); + assert!(assemble(&Arch::AArch64, "b\t0x1000000", Addr(0)).is_ok()); +} diff --git a/crates/e5r-asm/tests/roundtrip.rs b/crates/e5r-asm/tests/roundtrip.rs new file mode 100644 index 0000000..d7955e3 --- /dev/null +++ b/crates/e5r-asm/tests/roundtrip.rs @@ -0,0 +1,182 @@ +//! G1 for the assembler: every instruction in the corpus, decoded, printed, +//! assembled, and compared against the bytes it came from. +//! +//! This is the property the crate exists for. A user copies a line out of +//! `e5r disas`, changes a register, and assembles it back; if the unchanged +//! line does not reproduce its own bytes then nothing built on top of this can +//! be trusted. The corpus is enumerated with an external disassembler rather +//! than with `e5r-format`, because the loader lives above this crate in the +//! dependency order and an assembler that needs a container parser to be +//! tested is an assembler wired the wrong way round. +//! +//! Where an encoding is not unique the comparison weakens: the produced bytes +//! are decoded again and the two instructions must be equal. The count of +//! those is reported, because it is the size of the gap between "assembles +//! back" and "assembles back to the same bytes". + +mod common; + +use common::{Line, corpus, decode_for, disassemble, normalize, objdump, render}; + +use e5r_asm::{AsmError, assemble}; +use e5r_core::{Addr, Arch}; + +#[derive(Default)] +struct Tally { + attempted: usize, + exact: usize, + equivalent: Vec, + refused: Vec, + wrong: Vec, +} + +fn roundtrip(arch: &Arch, lines: &[Line], t: &mut Tally) { + for l in lines { + let Some(insn) = decode_for(arch, &l.bytes, Addr(l.addr)) else { + continue; + }; + if insn.len as usize != l.bytes.len() { + continue; + } + let text = render(arch, &insn); + t.attempted += 1; + match assemble(arch, &text, Addr(l.addr)) { + Ok(e) if e.bytes() == l.bytes.as_slice() => t.exact += 1, + Ok(e) => match decode_for(arch, e.bytes(), Addr(l.addr)) { + Some(again) + if normalize(&again) == normalize(&insn) && again.len as usize == e.len() => + { + t.equivalent.push(format!( + "{}\t{} bytes for {}", + insn.mnemonic, + e.len(), + l.bytes.len() + )) + } + other => t.wrong.push(format!( + "{text:?} at {:#x}: {:02x?} became {:02x?}{}", + l.addr, + l.bytes, + e.bytes(), + match other { + Some(a) => format!(" which is {}", render(arch, &a)), + None => " which does not decode".to_string(), + } + )), + }, + Err(e) => t.refused.push(reason(insn.mnemonic, &e)), + } + } +} + +/// One line per refusal, keyed so the report can count them by kind. +fn reason(mnemonic: &str, e: &AsmError) -> String { + let kind = match e { + AsmError::UnknownMnemonic(_) => "unknown mnemonic", + AsmError::UnknownRegister(_) => "unknown register", + AsmError::UnsupportedForm { .. } => "unsupported form", + AsmError::UnsizedOperand { .. } => "unsized operand", + AsmError::Syntax { .. } => "syntax", + AsmError::Range { .. } | AsmError::BranchRange { .. } => "out of range", + AsmError::Unaligned { .. } => "unaligned", + AsmError::NoEncoding { .. } => "no encoding", + _ => "other", + }; + format!("{mnemonic}\t{kind}") +} + +fn report(name: &str, t: &Tally) { + let mut by_kind: std::collections::BTreeMap<&str, usize> = Default::default(); + for r in &t.refused { + *by_kind.entry(r.as_str()).or_default() += 1; + } + println!( + "{name}: attempted {}, exact {}, equivalent {}, refused {}, wrong {}", + t.attempted, + t.exact, + t.equivalent.len(), + t.refused.len(), + t.wrong.len() + ); + let mut alt: std::collections::BTreeMap<&str, usize> = Default::default(); + for a in &t.equivalent { + *alt.entry(a.as_str()).or_default() += 1; + } + let mut alts: Vec<_> = alt.into_iter().collect(); + alts.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); + for (k, n) in alts.iter().take(30) { + println!(" alternative {n:>6} {k}"); + } + let mut kinds: Vec<_> = by_kind.into_iter().collect(); + kinds.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); + for (k, n) in kinds.iter().take(200) { + println!(" refused {n:>6} {k}"); + } + for w in t.wrong.iter().take(30) { + println!(" WRONG {w}"); + } +} + +fn run(arch: &Arch, tag: &str, floor: f64) { + let Some(dir) = corpus() else { + eprintln!("no fixtures, skipping"); + return; + }; + let Some(tool) = objdump(arch) else { + eprintln!("no disassembler for {arch:?}, skipping"); + return; + }; + let mut t = Tally::default(); + let mut files = 0usize; + for e in std::fs::read_dir(&dir).expect("fixtures").flatten() { + let p = e.path(); + let Some(n) = p.file_name().and_then(|n| n.to_str()) else { + continue; + }; + if !n.contains(tag) || !p.is_file() { + continue; + } + let lines = disassemble(tool, &p, arch); + if lines.is_empty() { + continue; + } + files += 1; + roundtrip(arch, &lines, &mut t); + } + if t.attempted == 0 { + eprintln!("no instructions found for {arch:?}, skipping"); + return; + } + println!("{arch:?}: {files} files"); + report(&format!("{arch:?}"), &t); + assert!( + t.wrong.is_empty(), + "{} instructions assembled to different instructions", + t.wrong.len() + ); + let rate = (t.exact + t.equivalent.len()) as f64 / t.attempted as f64; + assert!( + rate >= floor, + "{arch:?} round trip {rate:.4} is below the floor {floor:.4}" + ); +} + +// The floors sit under the measured rates rather than on them. What is +// refused is SIMD and floating point, and how much of a corpus that is depends +// on which fixtures the fetch script got; a floor pinned to today's number +// would fail on a more vector-heavy fixture set without anything having +// regressed. A floor this far below it still fails the moment a whole family +// stops encoding, which is the regression worth catching. +// +// Measured when this was written: AArch64 13751/14387 exact, x86-64 11226 +// exact plus 843 equivalent of 13777. Both zero wrong. + +#[test] +fn x86_64_round_trips() { + run(&Arch::X86_64, ".x64.", 0.80); +} + +#[test] +fn aarch64_round_trips() { + run(&Arch::AArch64, ".a64.", 0.90); +} diff --git a/crates/e5r-cli/Cargo.toml b/crates/e5r-cli/Cargo.toml new file mode 100644 index 0000000..95a488b --- /dev/null +++ b/crates/e5r-cli/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "e5r-cli" +description = "e5r — the command line. Load a binary, recover functions, disassemble, decompile, diff." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +homepage.workspace = true +documentation.workspace = true +# Inherited, so the path is the workspace README rather than a second copy. +readme.workspace = true +keywords.workspace = true +categories.workspace = true +# No `include` list on purpose: everything under the crate directory is the +# package, so a file added later cannot be left out of it silently. Nothing in +# the build reads a file outside the package either (no build script, no +# include_str! of a fixture), which is what makes the published copy build +# where fixtures/ and scripts/ do not exist. + +[[bin]] +name = "e5r" +path = "src/main.rs" + +[dependencies] +e5r-core = { workspace = true } +e5r-format = { workspace = true } +e5r-arch = { workspace = true } +e5r-analysis = { workspace = true } +e5r-asm = { workspace = true } +e5r-db = { workspace = true } +e5r-diff = { workspace = true } +e5r-types = { workspace = true } +e5r-ir = { workspace = true } +e5r-decomp = { workspace = true } +e5r-api = { workspace = true } +clap = { workspace = true } +memmap2 = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/crates/e5r-cli/src/addr.rs b/crates/e5r-cli/src/addr.rs new file mode 100644 index 0000000..b60fb58 --- /dev/null +++ b/crates/e5r-cli/src/addr.rs @@ -0,0 +1,81 @@ +//! Address expressions on the command line. +//! +//! `main`, `0x4006e8`, `main+0x20`, `sub_4006e8`. Anything a person would type +//! after reading a listing. + +use e5r_analysis::Program; +use e5r_core::Addr; + +/// Parse a plain number in hex, decimal, octal or binary. +pub fn parse_number(s: &str) -> Option { + let s = s.trim().replace('_', ""); + let (radix, digits) = match s.get(..2) { + Some("0x") | Some("0X") => (16, &s[2..]), + Some("0b") | Some("0B") => (2, &s[2..]), + Some("0o") | Some("0O") => (8, &s[2..]), + _ => (10, &s[..]), + }; + u64::from_str_radix(digits, radix).ok() +} + +/// Resolve an expression against a program. +pub fn resolve(program: &Program, expr: &str) -> Option { + let expr = expr.trim(); + // A trailing offset, as a listing prints it. + if let Some(i) = expr.rfind(['+', '-']) { + if i > 0 { + let (base, rest) = expr.split_at(i); + let sign = rest.starts_with('-'); + if let (Some(b), Some(off)) = + (resolve_atom(program, base.trim()), parse_number(&rest[1..])) + { + return if sign { + b.checked_sub(off) + } else { + b.checked_add(off) + }; + } + } + } + resolve_atom(program, expr) +} + +fn resolve_atom(program: &Program, s: &str) -> Option { + if let Some(n) = parse_number(s) { + return Some(Addr(n)); + } + // The synthetic name a function gets when nothing named it. + if let Some(hex) = s.strip_prefix("sub_") { + if let Ok(v) = u64::from_str_radix(hex, 16) { + return Some(Addr(v)); + } + } + if let Some(f) = program + .functions + .values() + .find(|f| f.name.as_deref() == Some(s)) + { + return Some(f.entry); + } + program + .object + .symbols + .iter() + .find(|sym| sym.name == s && sym.addr != Addr::ZERO) + .map(|sym| sym.addr) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn numbers_in_every_base() { + assert_eq!(parse_number("0x4006e8"), Some(0x4006e8)); + assert_eq!(parse_number("4198632"), Some(4198632)); + assert_eq!(parse_number("0b1010"), Some(10)); + assert_eq!(parse_number("0o17"), Some(15)); + assert_eq!(parse_number("0x40_06e8"), Some(0x4006e8)); + assert_eq!(parse_number("main"), None); + } +} diff --git a/crates/e5r-cli/src/annotate.rs b/crates/e5r-cli/src/annotate.rs new file mode 100644 index 0000000..2afed53 --- /dev/null +++ b/crates/e5r-cli/src/annotate.rs @@ -0,0 +1,276 @@ +//! The annotation command: writing to the log and reading it back. +//! +//! This is where the analysis side and the storage side meet. Anchors are +//! built from a fresh analysis, so a log written against one build applies to +//! the next one without anything being rewritten. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::Program; +use e5r_core::Addr; +use e5r_db::{Anchor, AnchorIndex, Field, Log, Resolution}; + +use crate::addr; +use crate::exit; +use crate::out::{Out, outln}; + +/// Where the log for a binary lives by default. +pub fn default_path(binary: &Path) -> PathBuf { + let mut p = binary.to_path_buf(); + let name = p + .file_name() + .map(|n| format!("{}.e5r", n.to_string_lossy())) + .unwrap_or_else(|| "e5r".into()); + p.set_file_name(name); + p +} + +/// Read a log, treating a missing file as an empty one. +pub fn load(path: &Path) -> Result { + match std::fs::read_to_string(path) { + Ok(text) => Log::from_text(&text).map_err(|e| format!("{}: {e}", path.display())), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Log::new()), + Err(e) => Err(format!("{}: {e}", path.display())), + } +} + +/// Write a log, atomically, so an interrupted write cannot lose the file. +pub fn save(path: &Path, log: &Log) -> Result<(), String> { + let tmp = path.with_extension("e5r.tmp"); + std::fs::write(&tmp, log.to_text()).map_err(|e| format!("{}: {e}", tmp.display()))?; + std::fs::rename(&tmp, path).map_err(|e| format!("{}: {e}", path.display()))?; + warn_if_git_lacks_union(path); + Ok(()) +} + +/// Say once if the log is in a git repo with no union merge rule. +/// +/// Without it two branches conflict on adjacent insertions. The rule is one +/// line, and it is the difference between "merges by itself" and "resolve this +/// by hand". Printed rather than written, because editing someone's +/// `.gitattributes` unasked is not ours to do. +fn warn_if_git_lacks_union(path: &Path) { + let dir = path.parent().unwrap_or(Path::new(".")); + let out = std::process::Command::new("git") + .args(["check-attr", "merge", "--"]) + .arg(path) + .current_dir(dir) + .output(); + let Ok(out) = out else { return }; + if !out.status.success() { + return; // not a repo, or no git + } + if String::from_utf8_lossy(&out.stdout).contains("merge: union") { + return; + } + eprintln!( + "note: add `*.e5r merge=union` to .gitattributes so two branches of \n\ + annotations merge without conflicts" + ); +} + +/// One anchor per recovered function, plus the index over them. +pub fn index(p: &Program) -> AnchorIndex { + AnchorIndex::build(p.functions_by_address().map(|f| anchor_of(p, f))) +} + +/// Anchor one function. +pub fn anchor_of(p: &Program, f: &e5r_analysis::Function) -> Anchor { + let insns = p.instructions(f); + // The body is the bytes the blocks actually cover, not the hull, so a + // split function does not fold in whatever sits between its parts. + let mut body = Vec::with_capacity(f.cfg.covered_bytes() as usize); + for b in f.cfg.blocks.values() { + if let Some(bytes) = p.object.memory.slice(b.range.start(), b.range.len()) { + body.extend_from_slice(bytes); + } + } + Anchor::function(f.entry, &insns, &body) +} + +/// The anchor covering an address, with the offset into it. +pub fn anchor_for(p: &Program, at: Addr) -> Option { + let f = p.function(at).or_else(|| p.function_at(at))?; + let offset = at.get().saturating_sub(f.entry.get()) as u32; + Some(anchor_of(p, f).at_offset(offset)) +} + +/// Set a field on a location. +pub fn set( + w: &mut Out, + p: &Program, + db: &Path, + field: Field, + target: &str, + value: Option, + who: &str, +) -> Result { + let Some(at) = addr::resolve(p, target) else { + return Err(format!("{target:?} is not an address or a symbol")); + }; + let Some(anchor) = anchor_for(p, at) else { + return Err(format!("no function covers {at}")); + }; + let mut log = load(db)?; + // Checked rather than bare, so a typo in a type is refused at the moment + // it is typed. An assertion that cannot be read is worse than no + // assertion: it sits in the log looking like work that was done. + log.assert_checked(anchor, field, value.clone(), who) + .map_err(|e| e.to_string())?; + save(db, &log)?; + match value { + Some(v) => outln!(w, "{at} {field} = {v:?}"), + None => outln!(w, "{at} {field} cleared"), + } + Ok(exit::OK) +} + +/// Show the current state, resolved against this binary. +pub fn list(w: &mut Out, p: &Program, db: &Path, as_json: bool) -> Result { + let log = load(db)?; + let applied = log.apply(&index(p)); + if as_json { + #[derive(serde::Serialize)] + struct Row { + addr: String, + field: String, + value: String, + resolution: String, + confident: bool, + by: String, + } + let rows: Vec = applied + .iter() + .map(|a| Row { + addr: format!("{:#x}", a.addr.get()), + field: a.field.to_string(), + value: a.value.clone(), + resolution: a.resolution.to_string(), + confident: a.resolution.is_confident(), + by: a.who.clone(), + }) + .collect(); + return crate::json::emit(w, &rows); + } + if applied.is_empty() { + eprintln!("no annotations in {}", db.display()); + return Ok(exit::NOT_FOUND); + } + for a in &applied { + // An unconfident match is flagged: the annotation landed somewhere, but + // only the address agreed, and the address is the weakest layer. + let flag = if a.resolution.is_confident() { + String::new() + } else { + format!(" [{} match]", a.resolution) + }; + outln!( + w, + "{:<20}{:<9}{:?}{}", + a.addr.to_string(), + a.field.to_string(), + a.value, + flag + ); + } + Ok(exit::OK) +} + +/// Undo or redo the most recent assertion. +pub fn step(w: &mut Out, db: &Path, forward: bool) -> Result { + let mut log = load(db)?; + let done = if forward { log.redo() } else { log.undo() }; + match done { + Some(a) => { + save(db, &log)?; + outln!( + w, + "{} {} {} = {:?}", + if forward { "redid" } else { "undid" }, + a.target.abs, + a.field, + a.value.unwrap_or_default() + ); + Ok(exit::OK) + } + None => { + eprintln!("nothing to {}", if forward { "redo" } else { "undo" }); + Ok(exit::NOT_FOUND) + } + } +} + +/// Names from the log, by address, for a listing to show. +pub fn names(p: &Program, db: &Path) -> Vec<(Addr, String, Resolution)> { + of_kind(p, db, Field::Name) +} + +/// Type assertions from the log, by the address each one resolved to. +/// +/// The text as it was written, not a parsed type: the log is reviewed in a +/// diff, so what a person typed is what is stored, and it is parsed where it +/// is used, against the types that program already knows about. +pub fn declarations(p: &Program, db: &Path) -> BTreeMap { + of_kind(p, db, Field::Type) + .into_iter() + .map(|(a, v, _)| (a, v)) + .collect() +} + +/// Comments from the log, by the address each one resolved to. +/// +/// A comment is the cheapest thing an analyst writes and the one they most +/// expect to see again. Storing it and never rendering it makes the log +/// write-only, which is the opposite of the point. +pub fn comments(p: &Program, db: &Path) -> BTreeMap { + of_kind(p, db, Field::Comment) + .into_iter() + .map(|(a, v, _)| (a, v)) + .collect() +} + +/// Assertions of one kind, resolved against this build. +fn of_kind(p: &Program, db: &Path, want: Field) -> Vec<(Addr, String, Resolution)> { + let Ok(log) = load(db) else { + return Vec::new(); + }; + log.apply(&index(p)) + .into_iter() + .filter(|a| a.field == want) + .map(|a| (a.addr, a.value, a.resolution)) + .collect() +} + +/// Load the program database a PE names, if it is there and it matches. +/// +/// Symbols from the wrong build are worse than no symbols, because they are +/// confidently wrong, so the identity is checked before anything is believed. +/// The path in the image is where the linker wrote it, which is rarely where +/// the file is now, so the database is also looked for beside the binary. +pub fn load_pdb(obj: &mut e5r_format::Object, binary: &Path) -> Option { + let named = obj.metadata.get("pe.pdb")?.clone(); + let stem = Path::new(&named).file_name()?; + let beside = binary.parent().unwrap_or(Path::new(".")).join(stem); + let (data, from) = [Path::new(&named), beside.as_path()] + .into_iter() + .find_map(|p| std::fs::read(p).ok().map(|d| (d, p.display().to_string())))?; + + let want = obj.metadata.get("pe.pdb.key").cloned(); + let db = e5r_format::pdb::read(&data, &obj.sections)?; + if want.is_some_and(|k| k != db.identity.key()) { + eprintln!( + "note: {from} is not the database {} was built with", + binary.display() + ); + return None; + } + let hints = db.hints(); + let found = hints.len(); + obj.function_hints.extend(hints); + obj.debug = Some(db.debug); + for w in &db.warnings { + eprintln!("note: {from}: {w}"); + } + Some(format!("{from}: {found} function(s)")) +} diff --git a/crates/e5r-cli/src/batch.rs b/crates/e5r-cli/src/batch.rs new file mode 100644 index 0000000..4a3071b --- /dev/null +++ b/crates/e5r-cli/src/batch.rs @@ -0,0 +1,197 @@ +//! Running several commands over one file and printing one document. +//! +//! An agent or a script that wants ten answers should not pay for ten +//! analyses, and should not have to match ten outputs back to what it asked. +//! Each command is run against the program already in memory and its answer is +//! returned next to the command that produced it. +//! +//! A command that fails does not stop the rest: its entry carries the error +//! instead of an answer, which is what a caller wants from a batch. + +use std::path::Path; + +use clap::Parser; + +use crate::out::{Out, outln}; +use crate::{Cli, Command, Common, exit}; + +/// Run a script of commands and print one JSON document. +pub fn run( + w: &mut Out, + cli: &Cli, + common: &Common, + script: Option<&Path>, + commands: &[String], +) -> Result { + let mut lines: Vec = commands.to_vec(); + if let Some(path) = script { + let text = if path == Path::new("-") { + std::io::read_to_string(std::io::stdin()).map_err(|e| e.to_string())? + } else { + std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))? + }; + lines.extend( + text.lines() + .map(|l| l.split('#').next().unwrap_or("").trim().to_string()) + .filter(|l| !l.is_empty()), + ); + } + if lines.is_empty() { + return Err("a batch needs commands: pass --command or --script".into()); + } + + let file = common.file.display().to_string(); + outln!(w, "{{"); + outln!(w, " \"schema\": \"e5r/1\","); + outln!(w, " \"file\": {},", quote(&file)); + outln!(w, " \"results\": ["); + + let mut worst = exit::OK; + for (n, line) in lines.iter().enumerate() { + let separator = if n + 1 == lines.len() { "" } else { "," }; + let (status, body) = one(cli, common, line); + if status != exit::OK { + worst = worst.max(status); + } + outln!(w, " {{"); + outln!(w, " \"command\": {},", quote(line)); + outln!(w, " \"status\": {status},"); + outln!(w, " \"output\": {body}"); + outln!(w, " }}{separator}"); + } + outln!(w, " ]"); + outln!(w, "}}"); + Ok(worst) +} + +/// Run one command and return its status and its answer as JSON. +fn one(cli: &Cli, common: &Common, line: &str) -> (u8, String) { + let Some(words) = split(line) else { + return (exit::USAGE, quote("a quote was never closed")); + }; + // The file comes from the batch, not from the line, so a script cannot be + // written once and silently read a different binary. It goes straight + // after the subcommand, where the command line puts it: a command whose + // last argument takes everything left would otherwise swallow it. + let mut words = words.into_iter(); + let Some(subcommand) = words.next() else { + return (exit::USAGE, quote("an empty command")); + }; + let mut argv: Vec = vec![ + "e5r".to_string(), + subcommand, + common.file.display().to_string(), + ]; + argv.extend(words); + argv.push("--json".to_string()); + + let parsed = match Cli::try_parse_from(&argv) { + Ok(parsed) => parsed, + Err(e) => return (exit::USAGE, quote(&first_line(&e.to_string()))), + }; + // A batch of batches would analyze the file again for each one, which is + // the cost this exists to avoid. + if matches!(parsed.command, Command::Batch { .. }) { + return (exit::USAGE, quote("a batch cannot run a batch or a server")); + } + let _ = cli; + + let mut buffer = Out::buffer(); + let status = match crate::run(&parsed, &mut buffer) { + Ok(status) => status, + Err(e) => return (exit::BAD_INPUT, quote(&e)), + }; + let text = buffer.take(); + let text = text.trim(); + if text.is_empty() { + return (status, "null".to_string()); + } + (status, text.to_string()) +} + +/// The words of a command line, respecting quotes. +pub fn split(line: &str) -> Option> { + let mut out = Vec::new(); + let mut current = String::new(); + let mut quote: Option = None; + let mut any = false; + for c in line.chars() { + match (quote, c) { + (Some(q), c) if c == q => quote = None, + (Some(_), c) => current.push(c), + (None, '"') | (None, '\'') => { + quote = Some(c); + any = true; + } + (None, c) if c.is_whitespace() => { + if !current.is_empty() || any { + out.push(std::mem::take(&mut current)); + any = false; + } + } + (None, c) => current.push(c), + } + } + if quote.is_some() { + return None; + } + if !current.is_empty() || any { + out.push(current); + } + Some(out) +} + +fn first_line(text: &str) -> String { + text.lines() + .find(|l| !l.trim().is_empty()) + .unwrap_or("") + .trim_start_matches("error: ") + .to_string() +} + +/// A JSON string, escaped. +fn quote(text: &str) -> String { + let mut out = String::with_capacity(text.len() + 2); + out.push('"'); + for c in text.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)), + c => out.push(c), + } + } + out.push('"'); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn quotes_keep_words_together() { + let words = split("query 'functions where insns > 10'").expect("splits"); + assert_eq!(words, vec!["query", "functions where insns > 10"]); + } + + #[test] + fn an_unclosed_quote_is_an_error() { + assert!(split("query 'oops").is_none()); + } + + #[test] + fn an_empty_argument_survives_splitting() { + let words = split("annotate name main ''").expect("splits"); + assert_eq!(words.len(), 4); + assert_eq!(words[3], ""); + } + + #[test] + fn a_string_is_escaped_for_json() { + assert_eq!(quote("a\"b\\c\nd"), "\"a\\\"b\\\\c\\nd\""); + } +} diff --git a/crates/e5r-cli/src/budget.rs b/crates/e5r-cli/src/budget.rs new file mode 100644 index 0000000..c6785de --- /dev/null +++ b/crates/e5r-cli/src/budget.rs @@ -0,0 +1,158 @@ +//! A deadline and a count, so a long job gives back what it finished. +//! +//! The case this exists for: an agent asks to decompile a 40,000 function +//! binary. Running to completion may take minutes, and the caller has no way +//! to say "enough". Returning nothing after two minutes is the worst of the +//! options, because the work was done and then thrown away. +//! +//! So a job that exceeds its budget stops, returns what it finished, and says +//! why and how much is missing. That is a partial answer that says it is +//! partial, which a caller can act on; a truncated answer that looks complete +//! is not. + +use std::time::{Duration, Instant}; + +/// Why a job stopped. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Stopped { + /// Everything asked for was done. + Finished, + /// The time limit was reached. + OutOfTime, + /// The item limit was reached. + OutOfItems, +} + +impl Stopped { + /// True when nothing was left undone. + pub fn complete(self) -> bool { + self == Stopped::Finished + } + + /// The word that goes in a JSON document. + pub fn as_str(self) -> &'static str { + match self { + Stopped::Finished => "finished", + Stopped::OutOfTime => "out of time", + Stopped::OutOfItems => "out of items", + } + } +} + +/// What a job is allowed to spend. +#[derive(Debug, Clone, Copy, Default)] +pub struct Budget { + /// Wall clock, or `None` for no limit. + limit: Option, + /// Items, or `None` for no limit. + items: Option, + /// When the budget started. + started: Option, + /// How many items have been taken. + taken: usize, +} + +impl Budget { + /// A budget from the command line's two options. + /// + /// The clock starts here rather than at the first item, so the time spent + /// loading and analyzing counts against it: a caller who said "two + /// seconds" meant two seconds, not two seconds of the last phase. + pub fn new(seconds: Option, items: Option) -> Budget { + Budget { + limit: seconds.map(Duration::from_secs_f64), + items, + started: Some(Instant::now()), + taken: 0, + } + } + + /// True when nothing is limited, so a caller can skip the bookkeeping. + pub fn unlimited(&self) -> bool { + self.limit.is_none() && self.items.is_none() + } + + /// Take one item, or report why it cannot be taken. + /// + /// Checked before the work rather than after, so the budget bounds what is + /// started and not merely what is finished. + pub fn take(&mut self) -> Stopped { + if self.items.is_some_and(|n| self.taken >= n) { + return Stopped::OutOfItems; + } + if let (Some(limit), Some(started)) = (self.limit, self.started) + && started.elapsed() >= limit + { + return Stopped::OutOfTime; + } + self.taken += 1; + Stopped::Finished + } + + /// How long the budget has been running. + pub fn elapsed(&self) -> Duration { + self.started.map(|s| s.elapsed()).unwrap_or_default() + } +} + +/// Say on stderr that a job stopped early, and how much is missing. +/// +/// stderr rather than the output, so a partial document is still a valid +/// document and the reason is still visible when the output is redirected. +pub fn report(b: &Budget, why: Stopped, done: usize, total: usize, what: &str) { + if why.complete() { + return; + } + eprintln!( + "e5r: {} after {:.1}s and {done} of {total} {what}; {} not done. \ + Raise --budget or --limit, or narrow the target.", + why.as_str(), + b.elapsed().as_secs_f64(), + total.saturating_sub(done) + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_unlimited_budget_never_stops() { + let mut b = Budget::new(None, None); + assert!(b.unlimited()); + for _ in 0..10_000 { + assert_eq!(b.take(), Stopped::Finished); + } + } + + #[test] + fn an_item_budget_stops_at_its_count() { + let mut b = Budget::new(None, Some(3)); + assert_eq!(b.take(), Stopped::Finished); + assert_eq!(b.take(), Stopped::Finished); + assert_eq!(b.take(), Stopped::Finished); + assert_eq!(b.take(), Stopped::OutOfItems); + // Still refused on a later call: a budget does not recover. + assert_eq!(b.take(), Stopped::OutOfItems); + } + + #[test] + fn a_zero_item_budget_does_nothing_rather_than_one_thing() { + let mut b = Budget::new(None, Some(0)); + assert_eq!(b.take(), Stopped::OutOfItems); + } + + #[test] + fn a_time_budget_stops_once_it_is_spent() { + let mut b = Budget::new(Some(0.0), None); + assert_eq!(b.take(), Stopped::OutOfTime); + } + + #[test] + fn the_item_limit_is_checked_before_the_clock() { + // Both exhausted: the item count is the more useful thing to report, + // because it is the one the caller can reason about. + let mut b = Budget::new(Some(0.0), Some(0)); + assert_eq!(b.take(), Stopped::OutOfItems); + } +} diff --git a/crates/e5r-cli/src/json.rs b/crates/e5r-cli/src/json.rs new file mode 100644 index 0000000..3b2b76f --- /dev/null +++ b/crates/e5r-cli/src/json.rs @@ -0,0 +1,830 @@ +//! The JSON surface. +//! +//! Versioned: `schema` names the shape, and a breaking change to any of these +//! bumps the major version. Agents and scripts read this, so it is an API. + +use e5r_analysis::{Function, Program, Xref}; +use e5r_arch::Insn; +use e5r_core::Addr; +use e5r_format::Object; +use serde::Serialize; + +use crate::exit; + +/// The schema version every document carries. +const SCHEMA: &str = "e5r/1"; + +/// Print a document and report success. +pub fn emit(w: &mut crate::out::Out, value: &T) -> Result { + let s = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?; + w.line(format_args!("{s}")); + Ok(exit::OK) +} + +/// Addresses serialize as hex strings: JSON numbers lose the top bits of a +/// 64-bit address in any consumer that parses them as doubles. +fn hex(a: Addr) -> String { + format!("{:#x}", a.get()) +} + +#[derive(Serialize)] +pub struct Info { + schema: &'static str, + format: String, + arch: String, + bits: u8, + endian: &'static str, + entry: Option, + image_base: String, + pic: bool, + segments: usize, + symbols: usize, + imports: usize, + exports: usize, + function_hints: usize, + metadata: std::collections::BTreeMap, + warnings: Vec, +} + +pub fn info(o: &Object) -> Info { + Info { + schema: SCHEMA, + format: o.format.to_string(), + arch: o.arch.to_string(), + bits: o.bits.bytes() as u8 * 8, + endian: match o.endian { + e5r_core::Endian::Little => "little", + e5r_core::Endian::Big => "big", + }, + entry: o.entry.map(hex), + image_base: hex(o.image_base), + pic: o.pic, + segments: o.memory.segments().len(), + symbols: o.symbols.len(), + imports: o.imports.len(), + exports: o.exports.len(), + function_hints: o.function_hints.len(), + metadata: o.metadata.clone(), + warnings: o.warnings.clone(), + } +} + +#[derive(Serialize)] +pub struct SectionOut { + name: String, + addr: String, + size: String, + file_offset: String, + file_size: String, + exec: bool, + write: bool, +} + +#[derive(Serialize)] +pub struct Listing { + schema: &'static str, + count: usize, + items: Vec, +} + +fn listing(items: Vec) -> Listing { + Listing { + schema: SCHEMA, + count: items.len(), + items, + } +} + +pub fn sections(o: &Object) -> Listing { + listing( + o.sections + .iter() + .map(|s| SectionOut { + name: s.name.clone(), + addr: hex(s.range.start()), + size: format!("{:#x}", s.range.len()), + file_offset: format!("{:#x}", s.file_offset), + file_size: format!("{:#x}", s.file_size), + exec: s.exec, + write: s.write, + }) + .collect(), + ) +} + +#[derive(Serialize)] +pub struct SymbolOut { + name: String, + addr: String, + size: String, + kind: String, + binding: String, + dynamic: bool, +} + +pub fn symbols(o: &Object) -> Listing { + listing( + o.symbols + .iter() + .map(|s| SymbolOut { + name: s.name.clone(), + addr: hex(s.addr), + size: format!("{:#x}", s.size), + kind: format!("{:?}", s.kind).to_lowercase(), + binding: format!("{:?}", s.binding).to_lowercase(), + dynamic: s.dynamic, + }) + .collect(), + ) +} + +#[derive(Serialize)] +pub struct ImportOut { + name: String, + library: Option, + thunk: Option, +} + +pub fn imports(o: &Object) -> Listing { + listing( + o.imports + .iter() + .map(|i| ImportOut { + name: i.name.clone(), + library: i.library.clone(), + thunk: i.thunk.map(hex), + }) + .collect(), + ) +} + +#[derive(Serialize)] +pub struct ExportOut { + name: String, + addr: String, +} + +pub fn exports(o: &Object) -> Listing { + listing( + o.exports + .iter() + .map(|e| ExportOut { + name: e.name.clone(), + addr: hex(e.addr), + }) + .collect(), + ) +} + +#[derive(Serialize)] +pub struct FunctionOut { + addr: String, + name: String, + named: bool, + size: String, + blocks: usize, + insns: u32, + complete: bool, + /// Why the walk stopped, so "incomplete" says what to look at: an + /// undecodable encoding is a decoder gap, a cap is a resource limit, and + /// `complete` here with `complete: false` above is an indirect branch + /// nothing resolved. + halt: &'static str, + indirect: bool, + strength: &'static str, + evidence: Vec, +} + +fn function_out(f: &Function) -> FunctionOut { + let mut evidence = vec![f.provenance.best.as_str().to_string()]; + evidence.extend( + f.provenance + .corroborating + .iter() + .map(|e| e.as_str().to_string()), + ); + FunctionOut { + addr: hex(f.entry), + name: f.display_name(), + named: f.name.is_some(), + size: format!("{:#x}", f.cfg.covered_bytes()), + blocks: f.cfg.blocks.len(), + insns: f.cfg.insns(), + complete: f.is_complete(), + halt: f.cfg.halt.as_str(), + indirect: f.cfg.has_indirect, + strength: crate::print::strength_name(f.provenance.strength()), + evidence, + } +} + +pub fn funcs(p: &Program) -> Listing { + listing(p.functions_by_address().map(function_out).collect()) +} + +#[derive(Serialize)] +pub struct Stats { + schema: &'static str, + functions: usize, + complete: usize, + capped: usize, + noreturn: usize, + blocks: usize, + tables: usize, + table_targets: usize, + indirect: usize, + data_regions: usize, + data_bytes: u64, + insns: u64, + xrefs: usize, + strings: usize, + rounds: usize, + by_strength: std::collections::BTreeMap, +} + +pub fn stats(p: &Program) -> Stats { + let s = p.stats(); + Stats { + schema: SCHEMA, + functions: s.functions, + complete: s.complete, + capped: s.capped, + noreturn: s.noreturn, + blocks: s.blocks, + tables: s.tables, + table_targets: s.table_targets, + indirect: s.indirect, + data_regions: s.data_regions, + data_bytes: s.data_bytes, + insns: s.insns, + xrefs: s.xrefs, + strings: s.strings, + rounds: p.rounds, + by_strength: p + .by_strength() + .into_iter() + .map(|(k, v)| (k.as_str().to_string(), v)) + .collect(), + } +} + +#[derive(Serialize)] +pub struct InsnOut { + addr: String, + len: u8, + text: String, + flow: String, + target: Option, +} + +#[derive(Serialize)] +pub struct DisasOut { + function: FunctionOut, + insns: Vec, +} + +fn insn_out(p: &Program, i: &Insn) -> InsnOut { + InsnOut { + addr: hex(i.addr), + len: i.len, + text: e5r_arch::format(&p.object.arch, i, false).replace('\t', " "), + flow: match i.flow { + e5r_arch::Flow::Next => "next", + e5r_arch::Flow::Branch(_) => "branch", + e5r_arch::Flow::CondBranch(_) => "cond_branch", + e5r_arch::Flow::IndirectBranch => "indirect_branch", + e5r_arch::Flow::Call(_) => "call", + e5r_arch::Flow::IndirectCall => "indirect_call", + e5r_arch::Flow::Return => "return", + e5r_arch::Flow::Trap => "trap", + e5r_arch::Flow::Syscall => "syscall", + } + .to_string(), + target: i.flow.target().map(hex), + } +} + +/// One variable a decompiled function declares. +/// +/// The name, the type and where the machine kept it, rather than a count. A +/// count cannot be checked against what the source declared, which is why +/// every consumer that tried to score our type recovery scored zero. +#[derive(Serialize)] +pub struct VariableOut { + name: String, + #[serde(rename = "type")] + ty: String, + size: u8, + role: &'static str, + storage: String, +} + +/// One decompiled function, with the numbers that say how well it went. +#[derive(Serialize)] +pub struct DecompileOut { + function: FunctionOut, + code: String, + gotos: usize, + locals: usize, + unmodelled: usize, + /// True when a type assertion in the log changed this signature. + asserted: bool, + /// What the assertion overrode, where it disagreed with the machine. + conflicts: Vec, + variables: Vec, +} + +pub fn decompiled(items: Vec<(&Function, &e5r_api::Decompiled)>) -> Listing { + listing( + items + .into_iter() + .map(|(f, d)| DecompileOut { + function: function_out(f), + code: d.text.clone(), + gotos: d.gotos, + locals: d.locals, + unmodelled: d.unmodelled, + asserted: d.asserted, + conflicts: d.conflicts.clone(), + variables: d.variables.iter().map(variable_out).collect(), + }) + .collect(), + ) +} + +fn variable_out(v: &e5r_decomp::expr::Variable) -> VariableOut { + use e5r_decomp::expr::{Home, Role}; + VariableOut { + name: v.name.clone(), + ty: v.ty.clone(), + size: v.size, + role: match v.role { + Role::Parameter => "parameter", + Role::Local => "local", + Role::Inherited => "inherited", + }, + storage: match v.home { + Home::Register(o) => format!("register+{o}"), + Home::Stack(o) => format!("stack{o:+}"), + Home::Anywhere => "anywhere".to_string(), + }, + } +} + +/// One pointer's inferred shape. +#[derive(Serialize)] +pub struct FieldOut { + offset: i64, + size: u8, +} + +/// What one function's pointers point at. +#[derive(Serialize)] +pub struct ShapeOut { + function: FunctionOut, + pointers: Vec, +} + +#[derive(Serialize)] +pub struct PointerOut { + argument: Option, + register: String, + size: Option, + written: bool, + fields: Vec, +} + +pub fn shapes(found: &[(&Function, Vec)]) -> Listing { + listing( + found + .iter() + .map(|(f, pointers)| ShapeOut { + function: function_out(f), + pointers: pointers + .iter() + .map(|p| PointerOut { + argument: p.argument, + register: format!("{:#x}", p.register), + size: p.size, + written: p.written, + fields: p + .fields + .iter() + .map(|(offset, size)| FieldOut { + offset: *offset, + size: *size, + }) + .collect(), + }) + .collect(), + }) + .collect(), + ) +} + +/// One function a signature library recognized. +#[derive(Serialize)] +pub struct IdentifiedOut { + addr: String, + name: String, + was: String, + r#match: String, + source: String, +} + +pub fn identified(found: &[e5r_api::Identified]) -> Listing { + listing( + found + .iter() + .map(|i| IdentifiedOut { + addr: hex(i.addr), + name: i.name.clone(), + was: i.was.clone(), + r#match: i.resolution.as_str().to_string(), + source: i.source.clone(), + }) + .collect(), + ) +} + +/// The answer to a query, as objects keyed by column name. +#[derive(Serialize)] +pub struct QueryOut { + schema: &'static str, + entity: String, + matched: usize, + count: usize, + items: Vec>, +} + +pub fn query(answer: &e5r_api::Answer) -> QueryOut { + let items = answer + .rows + .iter() + .map(|row| { + row.values + .iter() + .map(|(name, value)| { + use e5r_api::query::Field; + let v = match value { + Field::Number(n) => serde_json::Value::from(*n), + Field::Address(a) => serde_json::Value::from(hex(*a)), + Field::Text(t) => serde_json::Value::from(t.clone()), + Field::Bool(b) => serde_json::Value::from(*b), + Field::Missing => serde_json::Value::Null, + }; + (name.to_string(), v) + }) + .collect() + }) + .collect::>(); + QueryOut { + schema: SCHEMA, + entity: answer.entity.as_str().to_string(), + matched: answer.matched, + count: items.len(), + items, + } +} + +/// What one emulated run did. +#[derive(Serialize)] +pub struct RunOut { + function: FunctionOut, + stopped: String, + result: String, + insns: u64, + ops: u64, + unmodelled: Vec, + bytes_written: usize, +} + +pub fn run(f: &Function, r: &e5r_api::Run) -> RunOut { + RunOut { + function: function_out(f), + stopped: format!("{:?}", r.stop).to_lowercase(), + result: format!("{:#x}", r.result), + insns: r.insns, + ops: r.ops, + unmodelled: r.unlifted.iter().map(|a| hex(*a)).collect(), + bytes_written: r.written.len(), + } +} + +/// One virtual table. +#[derive(Serialize)] +pub struct VTableOut { + addr: String, + entry: String, + class: Option, + offset_to_top: i64, + typeinfo: Option, + evidence: String, + methods: Vec, +} + +#[derive(Serialize)] +pub struct MethodOut { + slot: usize, + addr: String, + name: Option, +} + +pub fn vtables(p: &Program, tables: &[e5r_api::VTable]) -> Listing { + listing( + tables + .iter() + .map(|t| VTableOut { + addr: hex(t.addr), + entry: hex(t.entry), + class: t.class.clone(), + offset_to_top: t.offset_to_top, + typeinfo: t.typeinfo.map(hex), + evidence: t.evidence.as_str().to_string(), + methods: t + .methods + .iter() + .enumerate() + .map(|(slot, addr)| MethodOut { + slot, + addr: hex(*addr), + name: p.function(*addr).map(|f| f.display_name()), + }) + .collect(), + }) + .collect(), + ) +} + +pub fn disas(p: &Program, fns: &[&Function]) -> Listing { + listing( + fns.iter() + .map(|f| DisasOut { + function: function_out(f), + insns: p.instructions(f).iter().map(|i| insn_out(p, i)).collect(), + }) + .collect(), + ) +} + +#[derive(Serialize)] +pub struct XrefOut { + from: String, + to: String, + kind: String, + from_function: Option, +} + +pub fn xrefs(p: &Program, refs: &[Xref]) -> Listing { + listing( + refs.iter() + .map(|x| XrefOut { + from: hex(x.from), + to: hex(x.to), + kind: format!("{:?}", x.kind).to_lowercase(), + from_function: p.function_at(x.from).map(|f| f.display_name()), + }) + .collect(), + ) +} + +/// A comparison of two builds. +#[derive(Serialize)] +pub struct DiffOut { + schema: &'static str, + matched: usize, + changed: usize, + identical: usize, + removed: Vec, + added: Vec, + /// Changed pairs, most changed first. + changes: Vec, +} + +use serde_json::{Value, json}; + +pub fn diff(d: &e5r_diff::Diff) -> DiffOut { + let pair = |m: &e5r_diff::Match| { + json!({ + "old": format!("{:#x}", m.old.get()), + "new": format!("{:#x}", m.new.get()), + "name": m.name, + "kind": m.kind.as_str(), + "similarity": m.similarity, + "old_insns": m.old_insns, + "new_insns": m.new_insns, + }) + }; + let side = + |(a, n): &(e5r_core::Addr, String)| json!({ "addr": format!("{:#x}", a.get()), "name": n }); + DiffOut { + schema: SCHEMA, + matched: d.matched.len(), + changed: d.changed_count(), + identical: d.identical_count(), + removed: d.removed.iter().map(side).collect(), + added: d.added.iter().map(side).collect(), + changes: d.changed().map(pair).collect(), + } +} + +#[derive(Serialize)] +pub struct OverlayOut { + offset: String, + size: u64, + entropy: f64, + content: &'static str, +} + +#[derive(Serialize)] +pub struct SectionEntropyOut { + name: String, + file_offset: String, + size: u64, + entropy: f64, + peak_entropy: f64, + peak_offset: Option, +} + +#[derive(Serialize)] +pub struct FindingOut { + kind: &'static str, + section: Option, + detail: String, + strength: &'static str, +} + +#[derive(Serialize)] +pub struct OverlayReport { + schema: &'static str, + described_end: String, + overlay: Option, + sections: Vec, + findings: Vec, + warnings: Vec, +} + +pub fn overlay(r: &e5r_format::overlay::Report) -> OverlayReport { + OverlayReport { + schema: SCHEMA, + described_end: format!("{:#x}", r.described_end), + overlay: r.overlay.as_ref().map(|o| OverlayOut { + offset: format!("{:#x}", o.offset), + size: o.size, + entropy: o.entropy, + content: o.content.as_str(), + }), + sections: r + .sections + .iter() + .map(|s| SectionEntropyOut { + name: s.name.clone(), + file_offset: format!("{:#x}", s.file_offset), + size: s.size, + entropy: s.entropy, + peak_entropy: s.peak().map(|p| p.entropy).unwrap_or(s.entropy), + peak_offset: s.peak().map(|p| format!("{:#x}", p.offset)), + }) + .collect(), + findings: r + .findings + .iter() + .map(|f| FindingOut { + kind: f.kind.as_str(), + section: f.section.clone(), + detail: f.detail.clone(), + strength: f.strength.as_str(), + }) + .collect(), + warnings: r.warnings.clone(), + } +} + +#[derive(Serialize)] +pub struct MemberOut { + name: String, + offset: u64, + size: u64, + mtime: u64, + symbols: Vec, +} + +#[derive(Serialize)] +pub struct ArchiveOut { + schema: &'static str, + flavor: &'static str, + thin: bool, + members: Vec, + warnings: Vec, +} + +pub fn archive(a: &e5r_format::archive::Archive) -> ArchiveOut { + ArchiveOut { + schema: SCHEMA, + flavor: a.flavor.as_str(), + thin: a.thin, + members: a + .members + .iter() + .enumerate() + .map(|(n, m)| MemberOut { + name: m.name.clone(), + offset: m.offset, + size: m.size, + mtime: m.mtime, + symbols: a.symbols_of(n).into_iter().map(str::to_string).collect(), + }) + .collect(), + warnings: a.warnings.clone(), + } +} + +#[derive(Serialize)] +pub struct BaseOut { + name: String, + offset: i64, + virtual_base: bool, + public: bool, +} + +#[derive(Serialize)] +pub struct ClassMemberOut { + address: String, + name: Option, + role: String, + slot: Option, + basis: &'static str, + strength: &'static str, + this: Option, +} + +#[derive(Serialize)] +pub struct ClassOut { + name: Option, + mangled: Option, + typeinfo: Option, + size: Option, + basis: &'static str, + strength: &'static str, + bases: Vec, + vtables: Vec, + members: Vec, +} + +#[derive(Serialize)] +pub struct ClassesOut { + schema: &'static str, + abi: &'static str, + rtti: &'static str, + tables: usize, + named_by_symbol: usize, + named_by_rtti: usize, + classes: Vec, +} + +pub fn classes(c: &e5r_api::classes::Classes) -> ClassesOut { + ClassesOut { + schema: SCHEMA, + abi: c.abi.as_str(), + rtti: c.rtti.as_str(), + tables: c.tables, + named_by_symbol: c.named_by_symbol, + named_by_rtti: c.named_by_rtti, + classes: c + .classes + .iter() + .map(|k| ClassOut { + name: k.name.clone(), + mangled: k.mangled.clone(), + typeinfo: k.typeinfo.map(hex), + size: k.size(), + basis: k.basis.as_str(), + strength: k.basis.strength().as_str(), + bases: k + .bases + .iter() + .map(|b| BaseOut { + name: b.name.clone(), + offset: b.offset, + virtual_base: b.is_virtual, + public: b.is_public, + }) + .collect(), + vtables: k.vtables.iter().copied().map(hex).collect(), + members: k + .members + .iter() + .map(|m| ClassMemberOut { + address: hex(m.addr), + name: m.name.clone(), + role: format!("{:?}", m.role).to_lowercase(), + slot: m.slot, + basis: m.basis.as_str(), + strength: m.basis.strength().as_str(), + this: m.this.as_ref().map(|t| t.declaration.clone()), + }) + .collect(), + }) + .collect(), + } +} diff --git a/crates/e5r-cli/src/main.rs b/crates/e5r-cli/src/main.rs new file mode 100644 index 0000000..511e0f5 --- /dev/null +++ b/crates/e5r-cli/src/main.rs @@ -0,0 +1,1067 @@ +//! The `e5r` command line. +//! +//! A thin client: it parses arguments, calls the library, and prints. Every +//! command takes `--json`, and the JSON is the same facts the text shows. + +// One unsafe block, in `map_file`: memory-mapping a file is inherently +// unsafe because another process can truncate it underneath us. Denied rather +// than forbidden so that one audited call can opt in with a written reason. +#![deny(unsafe_code)] + +mod addr; +mod annotate; +mod batch; +mod budget; +mod json; +mod out; +mod patch; +mod print; +mod progress; +mod repl; +mod shell; + +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::{Args, CommandFactory, Parser, Subcommand}; +use e5r_analysis::Options; +use e5r_core::{Addr, Arch, Caps}; +use e5r_format::LoadOptions; + +/// Exit codes, so a script can branch on the outcome. +mod exit { + /// Everything worked. + pub const OK: u8 = 0; + /// The command ran but found nothing matching. + pub const NOT_FOUND: u8 = 1; + /// The arguments did not make sense. clap exits with this itself; the + /// constant is here so the documented set is complete. + #[allow(dead_code)] + pub const USAGE: u8 = 2; + /// The file could not be read or parsed. + pub const BAD_INPUT: u8 = 3; +} + +#[derive(Parser)] +#[command( + name = "e5r", + version, + about = "Reverse engineering from the command line", + long_about = "Loads a binary, recovers functions, disassembles, and reports \ + what it found and why it believes it.\n\n\ + Every command takes --json. Exit codes: 0 ok, 1 nothing found, \ + 2 bad usage, 3 bad input." +)] +pub struct Cli { + #[command(subcommand)] + command: Command, +} + +/// Arguments shared by every command that opens a file. +#[derive(Args, Clone)] +pub struct Common { + /// The binary to analyze. + file: PathBuf, + /// Emit JSON instead of text. + #[arg(long, global = true)] + json: bool, + /// Threads for analysis. 0 or unset uses one per core. + #[arg(long, global = true)] + threads: Option, + /// Load a headerless image at this base address. + #[arg(long, value_parser = parse_u64)] + base: Option, + /// Architecture for a headerless image. + #[arg(long)] + arch: Option, + /// Skip the heuristic prologue scan, leaving only evidence-led discovery. + #[arg(long)] + no_scan: bool, + /// Annotation log to use. Defaults to the binary's path plus `.e5r`. + #[arg(long, global = true)] + db: Option, + /// When to colour the output. Piped output is never coloured under `auto`. + #[arg(long, global = true, value_name = "WHEN", default_value = "auto")] + color: out::When, + /// Do not page, even when a terminal is reading. + #[arg(long, global = true)] + no_pager: bool, + /// Stop after this many seconds and report what was finished. + #[arg(long, global = true, value_name = "SECONDS")] + budget: Option, + /// Stop after this many items and report what was finished. + #[arg(long, global = true, value_name = "N")] + limit: Option, + /// Show progress on stderr. Off when stderr is not a terminal. + #[arg(long, global = true)] + progress: bool, +} + +#[derive(Subcommand)] +pub enum Command { + /// Container, architecture, entry point, and what the loader noticed. + Info(Common), + /// Sections and their mapping. + Sections(Common), + /// Symbols the container names. + Symbols(Common), + /// Symbols needed from elsewhere. + Imports(Common), + /// Symbols offered to others. + Exports(Common), + /// Recovered functions, with the evidence for each. + Funcs(Common), + /// Disassemble a function, a range, or everything. + Disas { + #[command(flatten)] + common: Common, + /// A symbol name, an address, or `all`. Defaults to every function. + #[arg(default_value = "all")] + target: String, + /// Show the raw encoding beside each instruction. + #[arg(long)] + bytes: bool, + }, + /// References to or from an address. + Xrefs { + #[command(flatten)] + common: Common, + /// The address or symbol to ask about. + target: String, + /// Show what it points at rather than what points at it. + #[arg(long)] + from: bool, + }, + /// Strings found in the image. + Strings { + #[command(flatten)] + common: Common, + /// Shortest run to report. + #[arg(long, default_value_t = 4)] + min: usize, + /// Also scan executable memory, which is noisy. + #[arg(long)] + in_code: bool, + }, + /// Counts: functions, blocks, instructions, references, strings. + Stats(Common), + /// Compare two builds and say which functions changed. + /// + /// Matching runs strongest evidence first: identical bytes, then the + /// instruction shape, then names, then position in the call graph. + /// Changed functions are reported most changed first, which is the order a + /// patch diff is read in. + Diff { + #[command(flatten)] + common: Common, + /// The other build to compare against. + other: PathBuf, + /// Also list the pairs that did not change. + #[arg(long)] + all: bool, + }, + /// Decompile a function to pseudo-C. + /// + /// The output says what the machine does in C's notation; it does not claim + /// to be the source. Where the control flow does not fit a loop or a + /// branch, a labelled goto appears rather than a shape that is not there, + /// and the count is printed. + Decompile { + #[command(flatten)] + common: Common, + /// Address, symbol, or `all`. + target: String, + }, + /// Report what the pointers a function takes appear to point at. + /// + /// Inferred from the offsets the code touches through them, which is + /// evidence rather than a declaration: it says what was seen, not what the + /// type was. + Shapes { + #[command(flatten)] + common: Common, + /// Address, symbol, or `all`. + target: String, + }, + /// List virtual tables and the classes they belong to. + Vtables { + #[command(flatten)] + common: Common, + }, + /// Recover C++ classes, their hierarchy and their member functions. + /// + /// From the type information where the binary has it, and from the virtual + /// tables alone where it does not. Every claim says what it rests on: a + /// name read out of RTTI is proven, one inferred from what a function + /// writes is not, and the two are never printed as each other. + Classes { + #[command(flatten)] + common: Common, + /// Also list member functions and what each one touches through + /// `this`. This lifts every member function, which on a real C++ + /// library takes tens of seconds. + #[arg(long)] + members: bool, + }, + /// Run a function in the interpreter. + /// + /// The same machine the lifter's semantics gate uses, which is measured + /// against what real hardware does. Nothing escapes the process: memory is + /// a copy, a system call stops the run, and a budget bounds it. + Emulate { + #[command(flatten)] + common: Common, + /// Address or symbol of the function to run. + target: String, + /// Integer arguments, in the calling convention's order. + #[arg(value_parser = parse_u64)] + args: Vec, + /// How deep to follow calls. + #[arg(long, default_value_t = 64)] + depth: u32, + /// How many IR operations to allow. + /// + /// Not `--budget`, which is global and counts seconds. Two options + /// with one name and two types is a clap panic at parse time, which is + /// what this was: `e5r emulate` could not be run at all. + #[arg(long, default_value_t = 1 << 22)] + steps: u64, + }, + /// Ask a question about the program. + /// + /// `functions where insns > 100 and name ~ "crypt"`, and the same shape + /// for strings, symbols, imports, exports, sections and xrefs. + Query { + #[command(flatten)] + common: Common, + /// The query. + query: Vec, + }, + /// Build and apply function signatures. + /// + /// A signature identifies a function by what it is rather than where it + /// is, so a library built from a binary with symbols names the same code + /// in one without them. + Sig { + #[command(subcommand)] + what: SigCommand, + #[command(flatten)] + common: Common, + }, + /// Run several commands over one file and print one document. + /// + /// For a caller that wants every answer at once: each line of the script + /// is a command, and the results come back together with the command that + /// produced each one, so nothing has to be matched up afterwards. + Batch { + #[command(flatten)] + common: Common, + /// A file of commands, one per line; `-` reads standard input. + #[arg(short, long)] + script: Option, + /// A command to run, repeatable. + #[arg(short, long)] + command: Vec, + }, + /// Record, preview and apply byte patches. + /// + /// An edit is anchored to the code around it, so a patch written against + /// one build still lands on the right instruction in the next one. It + /// carries the bytes it expects to find, and applies as a whole set or not + /// at all. + Patch { + #[command(flatten)] + common: Common, + #[command(subcommand)] + what: PatchCommand, + }, + /// Save and reopen an analysis session. + /// + /// A project file names the binary by content as well as by path, so + /// opening one against the wrong build says so rather than producing + /// answers about bytes that are not there. + Project { + #[command(subcommand)] + what: ProjectCommand, + }, + /// Report an overlay, section entropy, and what they suggest. + /// + /// Findings are observations with a strength, never a verdict: a section + /// that is writable and executable is a fact, and what put it there is + /// not. A packer is named only when a section carries that packer's own + /// signature. + Overlay { + #[command(flatten)] + common: Common, + }, + /// List the members of an `ar` archive. + /// + /// An archive holds objects rather than being one, so it is not loaded: + /// picking a member silently would make every later answer about bytes + /// the caller did not choose. + Archive { + #[command(flatten)] + common: Common, + /// Also list the symbols each member defines. + #[arg(long)] + symbols: bool, + }, + /// Write a completion script for a shell. + /// + /// Generated from the command tree, so it cannot describe a command that + /// does not exist. + Completions { + /// Which shell. + shell: shell::Shell, + }, + /// Write the manual page, in roff. + Manpage, + /// Open an interactive session. + /// + /// One analysis, many questions. The commands are the ones below, spelled + /// the same way and without the file, so nothing has to be learned twice. + /// Analyzing a large binary takes seconds, and a session that paid that + /// per command would be unusable on exactly the binaries worth a session. + Repl { + #[command(flatten)] + common: Common, + }, + /// Read and write the annotation log. + /// + /// The log is a text file git can merge: every assertion is one line keyed + /// to a content anchor, so the work survives a rebuild and two analysts on + /// separate branches merge without conflict markers. + Annotate { + #[command(flatten)] + common: Common, + #[command(subcommand)] + what: Annotation, + }, +} + +/// What to do with a patch set. +#[derive(Subcommand)] +pub enum PatchCommand { + /// Capture an edit from the binary as it is now. + #[command(group = clap::ArgGroup::new("replacement").required(true).args(["bytes", "asm"]))] + Record { + /// Address or symbol to write at. + target: String, + /// The bytes to write, in hex. + #[arg(long)] + bytes: Option, + /// The instructions to write, assembled at the target address. + /// + /// Exclusive with `--bytes`: a patch whose two spellings disagree is + /// one nobody can review. + #[arg(long)] + asm: Option, + /// Pad the edit to this many bytes with no-ops. + /// + /// An instruction that encodes shorter than the one it replaces leaves + /// a hole, and the bytes after it are no longer the instructions they + /// were. + #[arg(long, value_name = "N")] + pad_to: Option, + /// Why. + #[arg(long, default_value = "")] + note: String, + /// The patch set to add to, created when it does not exist. + #[arg(short, long)] + out: PathBuf, + }, + /// List what a patch set says, without opening the binary. + Show { + /// The patch set. + set: PathBuf, + }, + /// Say where it would land and what it would overwrite. + Preview { + /// The patch set. + set: PathBuf, + }, + /// Write the patched binary. + Apply { + /// The patch set. + set: PathBuf, + /// Where to write. Defaults to refusing unless `--in-place`. + #[arg(short, long)] + out: Option, + /// Overwrite the binary itself. + #[arg(long)] + in_place: bool, + /// Write even where an edit matched on its address alone. + #[arg(long)] + allow_address_only: bool, + }, + /// Write the binary with the patch set taken back out. + Revert { + /// The patch set that was applied. + set: PathBuf, + /// Where to write. + #[arg(short, long)] + out: Option, + /// Overwrite the binary itself. + #[arg(long)] + in_place: bool, + /// Refuse where an edit matched on its address alone. + /// + /// Off by default, because a patched binary no longer holds the bytes + /// the anchor fingerprinted: that is what applying the patch did. The + /// expected bytes still have to match, and they are the exact bytes + /// the apply wrote. + #[arg(long)] + anchored_only: bool, + }, + /// Write the set that undoes this one, without applying anything. + Invert { + /// The patch set. + set: PathBuf, + /// Where to write it. + #[arg(short, long)] + out: PathBuf, + }, + /// Combine several sets into one. + Merge { + /// The sets, in the order they apply. + sets: Vec, + /// Where to write the result. + #[arg(short, long)] + out: PathBuf, + }, +} + +/// What to do with a project file. +#[derive(Subcommand)] +pub enum ProjectCommand { + /// Record what it takes to reopen this session. + New { + /// The binary. + binary: PathBuf, + /// Where to write the project. + #[arg(short, long)] + out: PathBuf, + /// An analysis option, as `key=value`. Repeatable. + #[arg(long = "set")] + settings: Vec, + /// The annotation log. Defaults to the binary's path plus `.e5r`. + #[arg(long)] + log: Option, + }, + /// Print a project file. + Show { + /// The project. + project: PathBuf, + }, + /// Say whether a binary is the one the project was made for. + Verify { + /// The project. + project: PathBuf, + /// The binary. Defaults to the path the project records. + #[arg(long)] + binary: Option, + }, + /// Attach a signature library, a patch set, or a log. + Add { + /// The project. + project: PathBuf, + /// A signature library. Repeatable. + #[arg(long)] + signatures: Vec, + /// A patch set. Repeatable. + #[arg(long = "patch")] + patches: Vec, + /// The annotation log. + #[arg(long)] + log: Option, + }, +} + +/// What to do with signatures. +#[derive(Subcommand)] +pub enum SigCommand { + /// Write a signature for every named function. + Create { + /// Where to write them; standard output when absent. + #[arg(short, long)] + out: Option, + }, + /// Name what a signature library recognizes. + Apply { + /// The signature file. + library: PathBuf, + }, +} + +/// What to do to the annotation log. +#[derive(Subcommand)] +pub enum Annotation { + /// Name a function or an address. + Name { + /// Address or symbol. + target: String, + /// The name. Omit to clear it. + value: Option, + }, + /// Attach a comment. + Comment { + /// Address or symbol. + target: String, + /// The text. Omit to clear it. + value: Option, + }, + /// Record a type or prototype. + Type { + /// Address or symbol. + target: String, + /// The type, as source text. Omit to clear it. + value: Option, + }, + /// Show everything the log says about this binary. + List, + /// Take back the most recent assertion. + Undo, + /// Put back the most recently undone assertion. + Redo, +} + +impl Command { + /// Colour and paging for this command, or `None` when its output is meant + /// for a program rather than for a person. + fn terminal_options(&self) -> Option<(out::When, bool)> { + match self { + // Both writers are redirected into a file the first time and + // never read on screen. + Command::Completions { .. } | Command::Manpage => None, + Command::Project { .. } => Some((out::When::Auto, false)), + _ => { + let c = self.common(); + if c.json { + return None; + } + Some((c.color, !c.no_pager)) + } + } + } + + fn common(&self) -> &Common { + match self { + Command::Info(c) + | Command::Sections(c) + | Command::Symbols(c) + | Command::Imports(c) + | Command::Exports(c) + | Command::Funcs(c) + | Command::Stats(c) => c, + Command::Annotate { common, .. } + | Command::Batch { common, .. } + | Command::Diff { common, .. } => common, + Command::Project { .. } | Command::Completions { .. } | Command::Manpage => { + unreachable!("handled before a file is opened") + } + Command::Vtables { common, .. } + | Command::Classes { common, .. } + | Command::Overlay { common, .. } + | Command::Archive { common, .. } + | Command::Repl { common } + | Command::Patch { common, .. } + | Command::Emulate { common, .. } + | Command::Query { common, .. } + | Command::Sig { common, .. } + | Command::Disas { common, .. } + | Command::Decompile { common, .. } + | Command::Shapes { common, .. } + | Command::Xrefs { common, .. } + | Command::Strings { common, .. } => common, + } + } +} + +fn parse_u64(s: &str) -> Result { + addr::parse_number(s).ok_or_else(|| format!("{s:?} is not a number")) +} + +fn main() -> ExitCode { + let cli = Cli::parse(); + let mut w = out::Out::new(); + // JSON is for a program, so it is never coloured and never paged whatever + // the terminal is. + if let Some(c) = cli.command.terminal_options() { + w.attach_terminal(c.0, c.1); + } + let result = run(&cli, &mut w); + // A reader that went away is the normal end of `... | head`, not a failure. + let piped_out = !w.finish(); + match result { + Ok(code) => ExitCode::from(if piped_out { exit::OK } else { code }), + Err(e) => { + eprintln!("e5r: {e}"); + ExitCode::from(exit::BAD_INPUT) + } + } +} + +pub fn run(cli: &Cli, w: &mut out::Out) -> Result { + // The two writers below have no input at all. + match &cli.command { + // A project names its own files; none of them is the binary the other + // commands open up front. + Command::Project { what } => return project(w, what), + Command::Completions { shell } => { + let script = shell::completions(&Cli::command(), *shell); + for line in script.lines() { + out::outln!(w, "{line}"); + } + return Ok(exit::OK); + } + Command::Manpage => { + let page = shell::manpage(&Cli::command()); + for line in page.lines() { + out::outln!(w, "{line}"); + } + return Ok(exit::OK); + } + _ => {} + } + let common = cli.command.common(); + + let file = + std::fs::File::open(&common.file).map_err(|e| format!("{}: {e}", common.file.display()))?; + // Memory-mapped: opening a 500 MB binary should not copy it. + let data = map_file(&file).map_err(|e| format!("{}: {e}", common.file.display()))?; + + if let Command::Archive { common, symbols } = &cli.command { + return print::archive(w, &data, *symbols, common.json); + } + + let arch = match &common.arch { + Some(a) => Some( + a.parse::() + .map_err(|e| format!("{e}; try x86-64, aarch64, arm or x86"))?, + ), + None => None, + }; + let load_opts = LoadOptions { + caps: Caps::default(), + base: common.base.map(Addr), + arch, + eh_frame: true, + debug_info: true, + }; + let mut object = e5r_format::load(&data, &load_opts).map_err(|e| e.to_string())?; + // A position independent library writes zero wherever a pointer goes and + // leaves a relocation saying what belongs there, so every virtual table + // and every type-information pointer in a shared object reads as zeroes + // until these are applied. Reading them as they sit does not fail, it + // quietly answers "there is nothing here". + e5r_api::apply_relative_relocations(&mut object); + // A PE keeps its debug information in a separate file, so the loader, + // which only sees bytes, cannot reach it. This is the first layer that + // can. + annotate::load_pdb(&mut object, &common.file); + let object = object; + + // Commands that only need the container skip analysis entirely, which is + // what makes `info` on a 500 MB binary instant. + match &cli.command { + Command::Info(c) => return print::info(w, &object, c.json), + Command::Sections(c) => return print::sections(w, &object, c.json), + Command::Symbols(c) => return print::symbols(w, &object, c.json), + Command::Imports(c) => return print::imports(w, &object, c.json), + Command::Exports(c) => return print::exports(w, &object, c.json), + Command::Overlay { common } => return print::overlay(w, &object, &data, common.json), + _ => {} + } + + let mut opts = Options { + threads: common.threads, + ..Options::default() + }; + if common.no_scan { + opts.scan_gaps = false; + } + if let Command::Strings { min, in_code, .. } = &cli.command { + opts.string_opts.min_len = *min; + opts.string_opts.in_code = *in_code; + // Nothing else is needed for a string scan. + opts.follow_calls = false; + opts.scan_gaps = false; + opts.xrefs = false; + } + if matches!(cli.command, Command::Funcs(_) | Command::Stats(_)) { + opts.strings = matches!(cli.command, Command::Stats(_)); + } + if matches!(cli.command, Command::Annotate { .. } | Command::Diff { .. }) { + opts.strings = false; + opts.xrefs = false; + } + + let program = e5r_analysis::analyze(object, &opts); + + let mut program = program; + if let Command::Repl { common } = &cli.command { + return repl::run(w, cli, common, &mut program, &data); + } + dispatch(cli, w, &mut program, &data, &load_opts, &opts) +} + +/// Run one command against a program that is already analyzed. +/// +/// Split out so the interactive session can answer a hundred commands from one +/// analysis. Everything before this point reads and analyzes the file, which on +/// a large binary is seconds and would be paid per line otherwise. +pub fn dispatch( + cli: &Cli, + w: &mut out::Out, + program: &mut e5r_analysis::Program, + data: &[u8], + load_opts: &LoadOptions, + opts: &Options, +) -> Result { + let common = cli.command.common(); + + // Names from the log override what the container said, because the point + // of writing one down was to overrule the engine. + let db = common + .db + .clone() + .unwrap_or_else(|| annotate::default_path(&common.file)); + let mut comments = std::collections::BTreeMap::new(); + let mut declared = std::collections::BTreeMap::new(); + if !matches!(cli.command, Command::Annotate { .. }) { + for (at, name, _) in annotate::names(program, &db) { + if let Some(f) = program.functions.get_mut(&at) { + f.name = Some(name); + } + } + comments = annotate::comments(program, &db); + declared = annotate::declarations(program, &db); + } + + // These need only the container, and a session reaches them through here + // as well, so they are answered in the same place rather than twice. + match &cli.command { + Command::Info(c) => return print::info(w, &program.object, c.json), + Command::Sections(c) => return print::sections(w, &program.object, c.json), + Command::Symbols(c) => return print::symbols(w, &program.object, c.json), + Command::Imports(c) => return print::imports(w, &program.object, c.json), + Command::Exports(c) => return print::exports(w, &program.object, c.json), + Command::Overlay { common } => { + return print::overlay(w, &program.object, data, common.json); + } + _ => {} + } + + match &cli.command { + Command::Funcs(c) => print::funcs(w, program, c.json), + Command::Stats(c) => print::stats(w, program, c.json), + Command::Strings { common, .. } => print::strings(w, program, common.json), + Command::Disas { + common, + target, + bytes, + } => print::disas( + w, + program, + target, + *bytes, + common.json, + print::Limits { + budget: budget::Budget::new(common.budget, common.limit), + progress: common.progress, + declared: &declared, + comments: &comments, + }, + ), + Command::Decompile { common, target } => print::decompile( + w, + program, + target, + common.json, + print::Limits { + budget: budget::Budget::new(common.budget, common.limit), + progress: common.progress, + declared: &declared, + comments: &comments, + }, + ), + Command::Shapes { common, target } => print::shapes(w, program, target, common.json), + Command::Emulate { + common, + target, + args, + depth, + steps, + } => print::emulate(w, program, target, args, *depth, *steps, common.json), + Command::Vtables { common } => print::vtables(w, program, common.json), + Command::Classes { common, members } => print::classes(w, program, *members, common.json), + Command::Batch { + common, + script, + command, + } => batch::run(w, cli, common, script.as_deref(), command), + Command::Query { common, query } => print::query(w, program, &query.join(" "), common.json), + Command::Patch { what, common } => { + let subject = patch::Subject { + program, + file: data, + binary: &common.file, + }; + match what { + PatchCommand::Record { + target, + bytes, + asm, + pad_to, + note, + out, + } => patch::record( + w, + &subject, + target, + patch::Replacement { + source: match (bytes, asm) { + (Some(b), _) => patch::Source::Bytes(patch::hex(b)?), + (_, Some(a)) => patch::Source::Asm(a), + _ => unreachable!("the argument group requires one"), + }, + pad_to: *pad_to, + }, + &whoami(), + note, + out, + ), + PatchCommand::Show { set } => patch::show(w, &patch::read(set)?), + PatchCommand::Preview { set } => patch::preview(w, &subject, &patch::read(set)?), + PatchCommand::Apply { + set, + out, + in_place, + allow_address_only, + } => patch::apply( + w, + &subject, + &patch::read(set)?, + out.as_deref(), + *in_place, + *allow_address_only, + ), + PatchCommand::Revert { + set, + out, + in_place, + anchored_only, + } => patch::apply( + w, + &subject, + &patch::read(set)?.revert(), + out.as_deref(), + *in_place, + !*anchored_only, + ), + PatchCommand::Invert { set, out } => patch::invert(w, &patch::read(set)?, out), + PatchCommand::Merge { sets, out } => patch::merge(w, sets, out), + } + } + Command::Sig { what, common } => match what { + SigCommand::Create { out } => { + let library = e5r_api::collect_signatures( + program, + &common + .file + .file_name() + .unwrap_or_default() + .to_string_lossy(), + ); + match out { + Some(path) => { + std::fs::write(path, library.to_text()) + .map_err(|e| format!("{}: {e}", path.display()))?; + eprintln!("{} signature(s) written", library.len()); + Ok(exit::OK) + } + None => { + for line in library.to_text().lines() { + out::outln!(w, "{line}"); + } + Ok(exit::OK) + } + } + } + SigCommand::Apply { library } => { + let text = std::fs::read_to_string(library) + .map_err(|e| format!("{}: {e}", library.display()))?; + let library = e5r_db::signature::Library::from_text(&text); + print::identified(w, program, &library, common.json) + } + }, + Command::Xrefs { + common, + target, + from, + } => print::xrefs(w, program, target, *from, common.json), + Command::Diff { common, other, all } => { + let other_data = std::fs::File::open(other) + .and_then(|f| map_file(&f)) + .map_err(|e| format!("{}: {e}", other.display()))?; + let other_obj = e5r_format::load(&other_data, load_opts).map_err(|e| e.to_string())?; + let other_prog = e5r_analysis::analyze(other_obj, opts); + print::diff(w, program, &other_prog, *all, common.json) + } + Command::Annotate { what, common } => { + let who = whoami(); + match what { + Annotation::Name { target, value } => annotate::set( + w, + program, + &db, + e5r_db::Field::Name, + target, + value.clone(), + &who, + ), + Annotation::Comment { target, value } => annotate::set( + w, + program, + &db, + e5r_db::Field::Comment, + target, + value.clone(), + &who, + ), + Annotation::Type { target, value } => annotate::set( + w, + program, + &db, + e5r_db::Field::Type, + target, + value.clone(), + &who, + ), + Annotation::List => annotate::list(w, program, &db, common.json), + Annotation::Undo => annotate::step(w, &db, false), + Annotation::Redo => annotate::step(w, &db, true), + } + } + _ => unreachable!("handled above"), + } +} + +/// The project commands, which open the files they name and nothing else. +fn project(w: &mut out::Out, what: &ProjectCommand) -> Result { + match what { + ProjectCommand::New { + binary, + out, + settings, + log, + } => { + let file = + std::fs::File::open(binary).map_err(|e| format!("{}: {e}", binary.display()))?; + let data = map_file(&file).map_err(|e| format!("{}: {e}", binary.display()))?; + let object = e5r_format::load(&data, &LoadOptions::default()) + .map_err(|e| format!("{}: {e}", binary.display()))?; + patch::project_new(w, binary, &object, &data, settings, log.as_deref(), out) + } + ProjectCommand::Show { project } => patch::project_show(w, &patch::project_read(project)?), + ProjectCommand::Verify { project, binary } => { + let proj = patch::project_read(project)?; + let path = binary + .clone() + .unwrap_or_else(|| PathBuf::from(&proj.binary.path)); + let file = + std::fs::File::open(&path).map_err(|e| format!("{}: {e}", path.display()))?; + let data = map_file(&file).map_err(|e| format!("{}: {e}", path.display()))?; + patch::project_verify(w, &proj, &path, &data) + } + ProjectCommand::Add { + project, + signatures, + patches, + log, + } => patch::project_add(w, project, signatures, patches, log.as_deref()), + } +} + +/// Who to record as the author of an assertion. +/// +/// The environment, then the OS user, then a placeholder. Never a hostname or +/// anything else that would leak more than a name into a file meant for a pull +/// request. +fn whoami() -> String { + std::env::var("E5R_AUTHOR") + .or_else(|_| std::env::var("USER")) + .or_else(|_| std::env::var("USERNAME")) + .unwrap_or_else(|_| "unknown".to_string()) +} + +/// Map the file read-only, so opening a 500 MB binary does not copy it. +/// +/// The obligation is that nothing truncates the file while the mapping lives. +/// Nothing here writes to it, and the mapping is dropped when the command +/// ends; a truncation racing the read is a corrupted read, which the loaders +/// already treat as ordinary hostile input. +#[allow(unsafe_code)] +fn map_file(file: &std::fs::File) -> std::io::Result { + unsafe { memmap2::Mmap::map(file) } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Every subcommand's parser is well formed. + /// + /// `clap` builds the parser at run time, so a definition that contradicts + /// itself is not a compile error: it is a panic the first time someone + /// runs the subcommand that contains it. `emulate` had one for its whole + /// life -- a global `--budget` counting seconds and a subcommand + /// `--budget` counting IR operations, two types under one name -- and + /// `e5r emulate` could not be run at all. Nothing noticed, because + /// nothing ever built the parser outside of running the program. + /// + /// `debug_assert` is clap's own consistency check, and it is the same one + /// that panics at parse time. Running it here turns "the first user to try + /// this subcommand" into "the test run". + #[test] + fn the_command_tree_is_consistent() { + Cli::command().debug_assert(); + } + + /// No subcommand names an option the global set already owns. + /// + /// The check above catches a redefinition with a different type, which is + /// the panic that happened. It does not catch one with the *same* type: + /// that parses, and quietly gives the subcommand's copy, so the same flag + /// before and after the subcommand would mean two things with no error + /// anywhere. A name is either global or it is not. + #[test] + fn no_subcommand_shadows_a_global_option() { + let cli = Cli::command(); + // The global options live on `Common`, which is flattened into every + // subcommand rather than declared on the root, so that is where they + // are visible. + let globals: std::collections::BTreeSet = cli + .get_subcommands() + .flat_map(|s| s.get_arguments()) + .filter(|a| a.is_global_set()) + .map(|a| a.get_id().to_string()) + .collect(); + assert!(!globals.is_empty(), "no global options were found at all"); + + let mut clashes = Vec::new(); + for sub in cli.get_subcommands() { + for arg in sub.get_arguments() { + let id = arg.get_id().to_string(); + if !arg.is_global_set() && globals.contains(&id) { + clashes.push(format!("{} --{id}", sub.get_name())); + } + } + } + assert!( + clashes.is_empty(), + "these subcommands redefine a global option: {}", + clashes.join(", ") + ); + } +} diff --git a/crates/e5r-cli/src/out.rs b/crates/e5r-cli/src/out.rs new file mode 100644 index 0000000..9ee705b --- /dev/null +++ b/crates/e5r-cli/src/out.rs @@ -0,0 +1,256 @@ +//! Buffered output that survives a closed pipe, pages when a person is +//! reading, and stays byte-identical when something else is. +//! +//! `println!` panics when the reader goes away, which is what happens every +//! time someone pipes a listing into `head`. Writing through a buffer and +//! dropping a broken-pipe error turns that into the normal end of a command. +//! Buffering also matters on its own: a libc listing is 400,000 lines. +//! +//! Colour and paging are decided once, from whether standard output is a +//! terminal. Piped output has no escape sequences and no truncation, so a +//! listing diffs against the same listing taken a week ago. + +use std::io::{BufWriter, IsTerminal, Stdout, Write}; +use std::process::{Child, ChildStdin, Command, Stdio}; + +/// When to colour. +#[derive(Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)] +pub enum When { + /// Colour a terminal, and nothing else. + #[default] + Auto, + /// Colour even when redirected, for a consumer that renders it. + Always, + /// Never. + Never, +} + +/// Where the output goes. +enum Sink { + /// Standard output, which is the usual case. + Stdout(BufWriter), + /// A pager, which owns the terminal until the reader quits. + Pager { + child: Child, + w: BufWriter, + }, + /// A string, for a command that runs another command and keeps what it + /// printed rather than letting it out. + Buffer(String), +} + +/// The output stream. +pub struct Out { + w: Sink, + /// Set once the reader has gone away; every later write is skipped. + closed: bool, + /// Whether to emit escape sequences. + colour: bool, + /// Terminal width, or `None` when the output is not a terminal, in which + /// case nothing is ever truncated. + width: Option, +} + +impl Out { + /// A 64 KiB buffered writer over stdout. + pub fn new() -> Out { + Out { + w: Sink::Stdout(BufWriter::with_capacity(64 * 1024, std::io::stdout())), + closed: false, + colour: false, + width: None, + } + } + + /// A stream that collects what is written instead of printing it. + pub fn buffer() -> Out { + Out { + w: Sink::Buffer(String::new()), + closed: false, + colour: false, + width: None, + } + } + + /// Decide colour and width, and page if a person is reading. + /// + /// `NO_COLOR` is honoured because it is the convention every other tool + /// follows, and a user who set it meant it. + pub fn attach_terminal(&mut self, when: When, paged: bool) { + let tty = std::io::stdout().is_terminal(); + self.colour = match when { + When::Always => true, + When::Never => false, + When::Auto => tty && std::env::var_os("NO_COLOR").is_none(), + }; + if !tty { + return; + } + self.width = terminal_width(); + if paged && matches!(self.w, Sink::Stdout(_)) { + if let Some((child, stdin)) = pager() { + self.w = Sink::Pager { + child, + w: BufWriter::with_capacity(64 * 1024, stdin), + }; + } + } + } + + /// True when escape sequences are wanted. + pub fn colour(&self) -> bool { + self.colour + } + + /// The width to fit a listing into, when there is one. + pub fn width(&self) -> Option { + self.width + } + + /// Wrap text in a colour, or return it unchanged. + pub fn paint(&self, role: Role, text: &str) -> String { + if !self.colour { + return text.to_string(); + } + format!("\x1b[{}m{text}\x1b[0m", role.code()) + } + + /// Write one line. + pub fn line(&mut self, args: std::fmt::Arguments<'_>) { + if self.closed { + return; + } + match &mut self.w { + Sink::Stdout(w) => { + if w.write_fmt(args).is_err() || w.write_all(b"\n").is_err() { + self.closed = true; + } + } + Sink::Pager { w, .. } => { + if w.write_fmt(args).is_err() || w.write_all(b"\n").is_err() { + self.closed = true; + } + } + Sink::Buffer(s) => { + let _ = std::fmt::Write::write_fmt(s, args); + s.push('\n'); + } + } + } + + /// What was collected, for a buffered stream. + pub fn take(self) -> String { + match self.w { + Sink::Buffer(s) => s, + _ => String::new(), + } + } + + /// Flush, reporting whether the reader was still there. + /// + /// The pager owns the terminal until the reader quits, so this waits for + /// it: returning first would let the shell prompt print underneath it. + pub fn finish(mut self) -> bool { + match &mut self.w { + Sink::Stdout(w) => !self.closed && w.flush().is_ok(), + Sink::Buffer(_) => !self.closed, + Sink::Pager { .. } => { + let Sink::Pager { mut child, mut w } = + std::mem::replace(&mut self.w, Sink::Buffer(String::new())) + else { + unreachable!("just matched") + }; + let flushed = w.flush().is_ok(); + // Closing the pipe is how the pager learns there is no more + // input; waiting is how the shell prompt stays below it. + drop(w); + let _ = child.wait(); + !self.closed && flushed + } + } + } +} + +/// What a piece of text is, which decides its colour. +#[derive(Clone, Copy)] +pub enum Role { + /// A column header. + Head, + /// An address. + Addr, + /// A symbol or function name. + Name, + /// How strongly something is believed. + Strong, + /// Believed, but not proven. + Weak, + /// Something the tool could not work out. + Unknown, +} + +impl Role { + /// The SGR parameters. Bold and the eight original colours only: a + /// 256-colour palette looks different on every terminal theme, and half of + /// them are unreadable on a light background. + fn code(self) -> &'static str { + match self { + Role::Head => "1", + Role::Addr => "36", + Role::Name => "32", + Role::Strong => "32", + Role::Weak => "33", + Role::Unknown => "31", + } + } +} + +/// The terminal's width. +/// +/// `COLUMNS` is not exported to children by most shells, so `stty` is asked +/// directly. It is one process at startup and only when a person is reading. +fn terminal_width() -> Option { + if let Some(n) = std::env::var("COLUMNS").ok().and_then(|v| v.parse().ok()) { + return Some(n); + } + let out = Command::new("stty") + .arg("size") + .stdin(Stdio::inherit()) + .output() + .ok()?; + let text = String::from_utf8_lossy(&out.stdout); + text.split_whitespace().nth(1)?.parse().ok() +} + +/// Start the pager, if there is one to start. +/// +/// `less -F` quits immediately when the output fits on one screen, so short +/// listings behave as if there were no pager at all. `-R` keeps the colours +/// and `-X` leaves the text on the screen after it exits. +fn pager() -> Option<(Child, ChildStdin)> { + let chosen = std::env::var("E5R_PAGER") + .or_else(|_| std::env::var("PAGER")) + .unwrap_or_else(|_| "less".to_string()); + if chosen.is_empty() || chosen == "cat" { + return None; + } + let mut parts = chosen.split_whitespace(); + let program = parts.next()?; + let mut cmd = Command::new(program); + let args: Vec<&str> = parts.collect(); + if args.is_empty() && program.ends_with("less") { + cmd.args(["-FRX"]); + } else { + cmd.args(args); + } + let mut child = cmd.stdin(Stdio::piped()).spawn().ok()?; + let stdin = child.stdin.take()?; + Some((child, stdin)) +} + +/// `println!` for [`Out`]. +macro_rules! outln { + ($o:expr) => { $o.line(format_args!("")) }; + ($o:expr, $($t:tt)*) => { $o.line(format_args!($($t)*)) }; +} + +pub(crate) use outln; diff --git a/crates/e5r-cli/src/patch.rs b/crates/e5r-cli/src/patch.rs new file mode 100644 index 0000000..33d3c3d --- /dev/null +++ b/crates/e5r-cli/src/patch.rs @@ -0,0 +1,469 @@ +//! The patch and project commands. +//! +//! Patching writes to a file, so everything here is arranged so that the +//! answer to "what would this do" comes before anything is written, and so +//! that a patch which no longer fits the binary says so rather than landing +//! somewhere approximate. + +use std::path::{Path, PathBuf}; + +use e5r_analysis::Program; +use e5r_core::Addr; +use e5r_db::patch::{Applied, Change, Conflict, Edit, Patch}; +use e5r_db::project::{Project, Verdict}; +use e5r_format::Object; + +use crate::addr; +use crate::annotate; +use crate::exit; +use crate::out::{Out, outln}; + +type R = Result; + +/// The binary a patch command is about: the analysis, the bytes on disk, and +/// where they came from. +pub struct Subject<'a> { + /// What the analyzer recovered, which is where the anchors come from. + pub program: &'a Program, + /// The file's bytes, exactly as they are on disk. + pub file: &'a [u8], + /// Where they were read from. + pub binary: &'a Path, +} + +/// Read a patch set, or fail with the path in the message. +pub fn read(path: &Path) -> Result { + let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?; + Patch::from_text(&text).map_err(|e| format!("{}: {e}", path.display())) +} + +/// Write a patch set. +fn write(path: &Path, p: &Patch) -> Result<(), String> { + std::fs::write(path, p.to_text()).map_err(|e| format!("{}: {e}", path.display())) +} + +/// Parse `de ad be ef` or `deadbeef` into bytes. +pub fn hex(s: &str) -> Result, String> { + let digits: String = s + .chars() + .filter(|c| !c.is_whitespace() && *c != '_') + .collect(); + if digits.is_empty() || digits.len() % 2 != 0 { + return Err(format!("{s:?} is not a whole number of hex bytes")); + } + (0..digits.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&digits[i..i + 2], 16).map_err(|_| format!("{s:?} is not hex"))) + .collect() +} + +/// The constant that turns an address into a file offset, for the section the +/// address is in. +/// +/// A patch is written to the file, and an address only becomes a file offset +/// through whichever section maps it. Sections rarely share one constant, so +/// this is checked per change rather than assumed. +fn file_base(o: &Object, at: Addr) -> Option { + o.sections + .iter() + .find(|s| s.file_size > 0 && !s.range.is_empty() && s.range.contains(at)) + .map(|s| s.range.start().get().wrapping_sub(s.file_offset)) +} + +/// Every change agrees on that constant, so the whole set can be applied to +/// the file as one image. +fn one_window(o: &Object, changes: &[Change], base: u64) -> Result<(), String> { + for c in changes { + match file_base(o, c.addr) { + Some(b) if b == base => {} + Some(b) => { + return Err(format!( + "{} is in a section mapped at a different file offset ({:#x} against {:#x}); \ + split the patch set by section", + c.addr, b, base + )); + } + None => return Err(format!("{} is not in any section with file bytes", c.addr)), + } + } + Ok(()) +} + +/// Where a patch set lands and what it holds. +fn placement( + p: &Patch, + o: &Object, + file: &[u8], + index: &e5r_db::AnchorIndex, +) -> Result<(u64, Vec), String> { + let first = p + .edits() + .first() + .ok_or_else(|| "the patch set is empty".to_string())?; + let base = file_base(o, first.addr()) + .ok_or_else(|| format!("{} is not in any section with file bytes", first.addr()))?; + let changes = p + .preview(file, Addr(base), Some(index)) + .map_err(|c| conflict(&c))?; + one_window(o, &changes, base)?; + Ok((base, changes)) +} + +/// A conflict, spelled for a terminal. +fn conflict(c: &Conflict) -> String { + format!("{c}") +} + +/// Print one change per line. +fn show_changes(w: &mut Out, changes: &[Change]) { + outln!( + w, + "{:<20}{:<20}{:<20}{}", + "address", + "before", + "after", + "found by" + ); + for c in changes { + outln!( + w, + "{:<20}{:<20}{:<20}{}", + c.addr.to_string(), + bytes(&c.before), + bytes(&c.after), + c.resolution.as_str() + ); + } +} + +fn bytes(b: &[u8]) -> String { + b.iter() + .map(|x| format!("{x:02x}")) + .collect::>() + .join("") +} + +/// Write the patched file, or the original path when asked to work in place. +fn emit( + out: Option<&Path>, + original: &Path, + image: &[u8], + in_place: bool, +) -> Result { + let dest = match (out, in_place) { + (Some(p), _) => p.to_path_buf(), + (None, true) => original.to_path_buf(), + (None, false) => { + return Err("give -o, or --in-place to overwrite the binary".to_string()); + } + }; + // Written beside the destination and renamed, so an interrupted write + // cannot leave a half-patched binary behind. + let tmp = dest.with_extension("e5r-patching"); + std::fs::write(&tmp, image).map_err(|e| format!("{}: {e}", tmp.display()))?; + if let Ok(meta) = std::fs::metadata(original) { + let _ = std::fs::set_permissions(&tmp, meta.permissions()); + } + std::fs::rename(&tmp, &dest).map_err(|e| format!("{}: {e}", dest.display()))?; + Ok(dest) +} + +/// Report what was written. +fn report(w: &mut Out, applied: &Applied, dest: &Path) { + show_changes(w, &applied.changes); + outln!( + w, + "{} byte(s) in {} change(s) written to {}", + applied.bytes, + applied.changes.len(), + dest.display() + ); +} + +/// `record`: capture an edit from the binary as it is now. +/// How the replacement was written. +/// +/// Assembly cannot be turned into bytes until the address is known: a branch +/// or a pc-relative operand encodes a displacement from where it sits, so +/// assembling at zero and relocating afterwards would be wrong. +pub enum Source<'a> { + /// Bytes, already given as hex. + Bytes(Vec), + /// Instructions, to assemble at the target address. + Asm(&'a str), +} + +/// What to write, and how long it has to be. +pub struct Replacement<'a> { + /// The replacement as the analyst wrote it. + pub source: Source<'a>, + /// Pad with no-ops to this length. An instruction that encodes shorter + /// than the one it replaces leaves the bytes after it meaning something + /// they did not mean before. + pub pad_to: Option, +} + +pub fn record( + w: &mut Out, + s: &Subject, + target: &str, + replace: Replacement<'_>, + who: &str, + note: &str, + out: &Path, +) -> R { + let (p, file, binary) = (s.program, s.file, s.binary); + let at = addr::resolve(p, target).ok_or_else(|| format!("{target}: no such address"))?; + let mut bytes = match replace.source { + Source::Bytes(b) => b, + Source::Asm(text) => { + e5r_asm::assemble_all(&p.object.arch, text, at).map_err(|e| format!("{target}: {e}"))? + } + }; + if let Some(n) = replace.pad_to { + if bytes.len() > n { + return Err(format!( + "the replacement is {} bytes, which does not fit in {n}", + bytes.len() + )); + } + bytes.extend(e5r_asm::pad(&p.object.arch, n - bytes.len()).map_err(|e| e.to_string())?); + } + let replace = &bytes[..]; + let base = file_base(&p.object, at) + .ok_or_else(|| format!("{at} is not in any section with file bytes"))?; + let mut set = match std::fs::read_to_string(out) { + Ok(text) => Patch::from_text(&text).map_err(|e| format!("{}: {e}", out.display()))?, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + Patch::new(&out.file_stem().unwrap_or_default().to_string_lossy()) + } + Err(e) => return Err(format!("{}: {e}", out.display())), + }; + // Anchored to the function it lands in when there is one, so the edit + // survives a rebuild that moves the code. Without a function the address + // is all there is, and the resolution reported on apply says so. + let anchor = match p.function_at(at) { + Some(f) => { + let off = at.get().wrapping_sub(f.entry.get()); + let off = u32::try_from(off).map_err(|_| "the offset is too large".to_string())?; + annotate::anchor_of(p, f).at_offset(off) + } + None => e5r_db::Anchor { + shape: 0, + bytes: 0, + insns: 0, + abs: at, + offset: 0, + }, + }; + if set.binary.is_none() { + set.binary = Some( + binary + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into(), + ); + } + let off = usize::try_from(at.get().wrapping_sub(base)) + .ok() + .filter(|o| o + replace.len() <= file.len()) + .ok_or_else(|| format!("{at} plus {} bytes is past the file", replace.len()))?; + let expect = file[off..off + replace.len()].to_vec(); + let before = set.len(); + set.push( + Edit::new(anchor, expect, replace.to_vec()) + .map_err(|e| e.to_string())? + .by(who) + .noted(note), + ); + + write(out, &set)?; + outln!( + w, + "{} edit(s) in {}, {} new", + set.len(), + out.display(), + set.len().saturating_sub(before) + ); + Ok(exit::OK) +} + +/// `show`: what a patch set says, without touching the binary. +pub fn show(w: &mut Out, set: &Patch) -> R { + outln!( + w, + "{}: {} edit(s){}", + set.name, + set.len(), + set.binary + .as_ref() + .map(|b| format!(", for {b}")) + .unwrap_or_default() + ); + for (n, e) in set.edits().iter().enumerate() { + outln!( + w, + "{n:>3} {:<20}{:<20}{:<20}{}{}", + e.addr().to_string(), + bytes(&e.expect), + bytes(&e.replace), + if e.who.is_empty() { + String::new() + } else { + format!("{} ", e.who) + }, + e.note + ); + } + if set.is_empty() { + return Ok(exit::NOT_FOUND); + } + Ok(exit::OK) +} + +/// `preview`: where it would land, and what it would overwrite. +pub fn preview(w: &mut Out, s: &Subject, set: &Patch) -> R { + let index = annotate::index(s.program); + let (_, changes) = placement(set, &s.program.object, s.file, &index)?; + show_changes(w, &changes); + Ok(exit::OK) +} + +/// `apply` and `revert`, which differ only in which set is used. +pub fn apply( + w: &mut Out, + s: &Subject, + set: &Patch, + out: Option<&Path>, + in_place: bool, + allow_address_only: bool, +) -> R { + let index = annotate::index(s.program); + let (base, _) = placement(set, &s.program.object, s.file, &index)?; + let mut image = s.file.to_vec(); + let applied = set + .apply(&mut image, Addr(base), Some(&index)) + .map_err(|c| conflict(&c))?; + if !applied.is_confident() && !allow_address_only { + return Err( + "some edits matched on the address alone, which means the code they were written \ + against is not there; pass --allow-address-only to write anyway" + .to_string(), + ); + } + let dest = emit(out, s.binary, &image, in_place)?; + report(w, &applied, &dest); + Ok(exit::OK) +} + +/// `invert`: write the set that undoes this one. +pub fn invert(w: &mut Out, set: &Patch, out: &Path) -> R { + let back = set.revert(); + write(out, &back)?; + outln!(w, "{} edit(s) written to {}", back.len(), out.display()); + Ok(exit::OK) +} + +/// `merge`: one set from several, with the later ones taking precedence. +pub fn merge(w: &mut Out, sets: &[PathBuf], out: &Path) -> R { + let mut all = Patch::new(&out.file_stem().unwrap_or_default().to_string_lossy()); + for path in sets { + let s = read(path)?; + if all.binary.is_none() { + all.binary = s.binary.clone(); + } + all.merge(&s); + } + write(out, &all)?; + outln!(w, "{} edit(s) written to {}", all.len(), out.display()); + Ok(exit::OK) +} + +/// `project new`: record what it takes to reopen this session. +pub fn project_new( + w: &mut Out, + binary: &Path, + o: &Object, + data: &[u8], + settings: &[String], + log: Option<&Path>, + out: &Path, +) -> R { + let mut proj = Project::for_binary(&binary.display().to_string(), data); + proj.load.base = Some(o.image_base); + proj.load.arch = Some(o.arch.clone()); + proj.load.readers.insert(o.format.to_string()); + for s in settings { + let (k, v) = s + .split_once('=') + .ok_or_else(|| format!("{s:?} is not key=value"))?; + proj.analysis.insert(k.to_string(), v.to_string()); + } + proj.log = log + .map(|p| p.display().to_string()) + .or_else(|| Some(annotate::default_path(binary).display().to_string())); + std::fs::write(out, proj.to_text()).map_err(|e| format!("{}: {e}", out.display()))?; + outln!(w, "{} written", out.display()); + Ok(exit::OK) +} + +/// Read a project file. +pub fn project_read(path: &Path) -> Result { + let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?; + Project::from_text(&text).map_err(|e| format!("{}: {e}", path.display())) +} + +/// `project show`. +pub fn project_show(w: &mut Out, proj: &Project) -> R { + for line in proj.to_text().lines() { + outln!(w, "{line}"); + } + Ok(exit::OK) +} + +/// `project verify`: are these the bytes the session was made for. +pub fn project_verify(w: &mut Out, proj: &Project, binary: &Path, data: &[u8]) -> R { + let verdict = proj.verify_at(&binary.display().to_string(), data); + outln!(w, "{verdict}"); + // A moved binary is still the right one, so it is not a failure. + Ok(match verdict { + Verdict::Different { .. } => exit::NOT_FOUND, + _ => exit::OK, + }) +} + +/// `project add`: attach a signature library, a patch set or a log. +pub fn project_add( + w: &mut Out, + path: &Path, + signatures: &[PathBuf], + patches: &[PathBuf], + log: Option<&Path>, +) -> R { + let mut proj = project_read(path)?; + for s in signatures { + let s = s.display().to_string(); + if !proj.signatures.contains(&s) { + proj.signatures.push(s); + } + } + for p in patches { + let p = p.display().to_string(); + if !proj.patches.contains(&p) { + proj.patches.push(p); + } + } + if let Some(l) = log { + proj.log = Some(l.display().to_string()); + } + std::fs::write(path, proj.to_text()).map_err(|e| format!("{}: {e}", path.display()))?; + outln!( + w, + "{}: {} signature file(s), {} patch set(s)", + path.display(), + proj.signatures.len(), + proj.patches.len() + ); + Ok(exit::OK) +} diff --git a/crates/e5r-cli/src/print.rs b/crates/e5r-cli/src/print.rs new file mode 100644 index 0000000..734127a --- /dev/null +++ b/crates/e5r-cli/src/print.rs @@ -0,0 +1,1108 @@ +//! Output. Text for a person, JSON for everything else. +//! +//! Both come from the same values, so `--json` never shows more or less than +//! the listing does. + +use e5r_analysis::{Program, XrefKind}; +use e5r_core::{Addr, Strength}; +use e5r_format::Object; + +use crate::addr; +use crate::budget::{Budget, Stopped}; +use crate::exit; +use crate::json; +use crate::out::{Out, Role, outln}; +use crate::progress::Progress; + +type R = Result; + +/// Container summary. +pub fn info(w: &mut Out, o: &Object, as_json: bool) -> R { + if as_json { + return json::emit(w, &json::info(o)); + } + outln!( + w, + "file {} {}, {}-bit, {:?}", + o.format, + o.arch, + o.bits, + o.endian + ); + if let Some(e) = o.entry { + outln!(w, "entry {e}"); + } + outln!(w, "base {}", o.image_base); + outln!(w, "pic {}", o.pic); + outln!( + w, + "mapped {} segments, {}", + o.memory.segments().len(), + o.memory + .bounds() + .map(|b| b.to_string()) + .unwrap_or_else(|| "nothing".into()) + ); + outln!( + w, + "symbols {} ({} dynamic)", + o.symbols.len(), + o.symbols.iter().filter(|s| s.dynamic).count() + ); + outln!(w, "imports {}", o.imports.len()); + outln!(w, "exports {}", o.exports.len()); + outln!(w, "hints {} function entries", o.function_hints.len()); + for (k, v) in &o.metadata { + // A key longer than the column still needs a space after it, or the + // value runs into the name. + outln!(w, "{k:<9} {}", v.replace('\n', " ")); + } + for warning in &o.warnings { + outln!(w, "warning {warning}"); + } + Ok(exit::OK) +} + +/// Sections and their mapping. +pub fn sections(w: &mut Out, o: &Object, as_json: bool) -> R { + if as_json { + return json::emit(w, &json::sections(o)); + } + if o.sections.is_empty() { + eprintln!("no sections"); + return Ok(exit::NOT_FOUND); + } + outln!( + w, + "{}", + head( + w, + &format!( + "{:<24}{:<20}{:>10} {:>10} perm", + "name", "address", "size", "file off" + ) + ) + ); + for s in &o.sections { + let addr = if s.range.is_empty() { + "unmapped".to_string() + } else { + s.range.start().to_string() + }; + outln!( + w, + "{:<24}{}{:>10x} {:>10x} {}{}{}", + s.name, + cell(w, Role::Addr, &addr, 20), + s.range.len(), + s.file_offset, + 'r', + if s.write { 'w' } else { '-' }, + if s.exec { 'x' } else { '-' }, + ); + } + Ok(exit::OK) +} + +/// Symbols. +pub fn symbols(w: &mut Out, o: &Object, as_json: bool) -> R { + if as_json { + return json::emit(w, &json::symbols(o)); + } + if o.symbols.is_empty() { + eprintln!("no symbols"); + return Ok(exit::NOT_FOUND); + } + for s in &o.symbols { + outln!( + w, + "{}{:>8x} {:<10}{:<8}{}", + cell(w, Role::Addr, &s.addr.to_string(), 20), + s.size, + format!("{:?}", s.kind).to_lowercase(), + if s.dynamic { "dynamic" } else { "static" }, + w.paint(Role::Name, &last(w, 48, &e5r_types::pretty(&s.name))) + ); + } + Ok(exit::OK) +} + +/// Imports. +pub fn imports(w: &mut Out, o: &Object, as_json: bool) -> R { + if as_json { + return json::emit(w, &json::imports(o)); + } + if o.imports.is_empty() { + eprintln!("no imports"); + return Ok(exit::NOT_FOUND); + } + for i in &o.imports { + let name = e5r_types::pretty(&i.name); + let lib = i + .library + .as_deref() + .map(|l| format!("{l} ")) + .unwrap_or_default(); + match i.thunk { + Some(t) => outln!(w, "{t} {lib}{name}"), + None => outln!(w, "{:<20}{lib}{name}", ""), + } + } + Ok(exit::OK) +} + +/// Exports. +pub fn exports(w: &mut Out, o: &Object, as_json: bool) -> R { + if as_json { + return json::emit(w, &json::exports(o)); + } + if o.exports.is_empty() { + eprintln!("no exports"); + return Ok(exit::NOT_FOUND); + } + for e in &o.exports { + outln!(w, "{} {}", e.addr, e5r_types::pretty(&e.name)); + } + Ok(exit::OK) +} + +/// Recovered functions, with the evidence for each. +pub fn funcs(w: &mut Out, p: &Program, as_json: bool) -> R { + if as_json { + return json::emit(w, &json::funcs(p)); + } + if p.functions.is_empty() { + eprintln!("no functions recovered"); + return Ok(exit::NOT_FOUND); + } + outln!( + w, + "{}", + head( + w, + &format!( + "{:<20}{:>8} {:>6} {:>6} {:<10}{:<28}{}", + "address", "size", "blocks", "insns", "strength", "evidence", "name" + ) + ) + ); + for f in p.functions_by_address() { + let ev = { + let mut s = f.provenance.best.to_string(); + for c in &f.provenance.corroborating { + s.push_str(", "); + s.push_str(c.as_str()); + } + s + }; + let strength = f.provenance.strength().to_string(); + let tail = if f.is_complete() { + "" + } else { + " [incomplete]" + }; + outln!( + w, + "{}{:>8x} {:>6} {:>6} {}{:<28}{}{}", + cell(w, Role::Addr, &f.entry.to_string(), 20), + f.cfg.covered_bytes(), + f.cfg.blocks.len(), + f.cfg.insns(), + cell(w, strength_role(&strength), &strength, 10), + truncate(&ev, 27), + w.paint(Role::Name, &last(w, 84 + tail.len(), &f.display_name())), + tail, + ); + } + Ok(exit::OK) +} + +fn truncate(s: &str, n: usize) -> String { + if s.len() <= n { + s.to_string() + } else { + // On a character boundary, because a name can be UTF-8 and cutting a + // multi-byte sequence in half would panic. + let mut cut = n - 1; + while cut > 0 && !s.is_char_boundary(cut) { + cut -= 1; + } + format!("{}…", &s[..cut]) + } +} + +/// What a listing command is allowed to spend, and what the log has told it. +/// +/// One struct rather than three parameters, because `disas` and `decompile` +/// take the same three and a caller that passes them in the wrong order to one +/// of them would not be caught by the types. +pub struct Limits<'a> { + /// Time and item budget. + pub budget: Budget, + /// Whether to draw a counter on stderr. + pub progress: bool, + /// Type assertions from the annotation log, by address. + pub declared: &'a std::collections::BTreeMap, + /// Comments from the annotation log, by address. + pub comments: &'a std::collections::BTreeMap, +} + +/// A fixed-width cell, padded before it is coloured so the escape sequences do +/// not count toward the width. +fn cell(w: &Out, role: Role, text: &str, width: usize) -> String { + w.paint(role, &format!("{text: String { + w.paint(Role::Head, text) +} + +/// Fit the last column of a listing to the terminal, and never when the output +/// is going somewhere else: a piped listing must diff against the same listing +/// taken a week ago. +fn last(w: &Out, used: usize, text: &str) -> String { + match w.width() { + Some(total) if total > used + 8 => truncate(text, total - used), + Some(_) => truncate(text, 8), + None => text.to_string(), + } +} + +/// The colour a strength is printed in. The ladder is the point: a reader +/// scanning a listing should see at a glance which rows are facts. +fn strength_role(s: &str) -> Role { + match s { + "proven" | "asserted" => Role::Strong, + "inferred" => Role::Weak, + _ => Role::Unknown, + } +} + +/// Counts. +pub fn stats(w: &mut Out, p: &Program, as_json: bool) -> R { + if as_json { + return json::emit(w, &json::stats(p)); + } + let s = p.stats(); + outln!(w, "functions {}", s.functions); + outln!( + w, + " complete {} ({:.1}%)", + s.complete, + pct(s.complete, s.functions) + ); + outln!(w, " capped {}", s.capped); + outln!(w, " noreturn {}", s.noreturn); + outln!(w, "blocks {}", s.blocks); + outln!( + w, + "jump tables {} resolved, {} targets; {} functions still indirect", + s.tables, + s.table_targets, + s.indirect + ); + outln!( + w, + "data {} regions, {} bytes proved not to be instructions", + s.data_regions, + s.data_bytes + ); + outln!(w, "insns {}", s.insns); + outln!(w, "xrefs {}", s.xrefs); + outln!(w, "strings {}", s.strings); + outln!(w, "rounds {}", p.rounds); + outln!(w, "evidence"); + for (k, n) in p.by_strength().iter().rev() { + outln!(w, " {:<9} {n}", k.to_string()); + } + Ok(exit::OK) +} + +fn pct(a: usize, b: usize) -> f64 { + if b == 0 { + 0.0 + } else { + a as f64 * 100.0 / b as f64 + } +} + +/// Strings. +pub fn strings(w: &mut Out, p: &Program, as_json: bool) -> R { + if as_json { + return json::emit(w, &p.strings); + } + if p.strings.is_empty() { + eprintln!("no strings"); + return Ok(exit::NOT_FOUND); + } + for s in &p.strings { + outln!( + w, + "{} {}", + w.paint(Role::Addr, &s.addr.to_string()), + last(w, 22, &format!("{:?}", s.text)) + ); + } + Ok(exit::OK) +} + +/// Disassembly. +pub fn disas( + w: &mut Out, + p: &Program, + target: &str, + show_bytes: bool, + as_json: bool, + limits: Limits<'_>, +) -> R { + let (mut budget, progress, comments) = (limits.budget, limits.progress, limits.comments); + let mut chosen: Vec<&e5r_analysis::Function> = if target == "all" { + p.functions_by_address().collect() + } else { + let Some(a) = addr::resolve(p, target) else { + return Err(format!("{target:?} is not an address, a symbol, or `all`")); + }; + match p.function(a).or_else(|| p.function_at(a)) { + Some(f) => vec![f], + None => { + eprintln!("no function covers {a}"); + return Ok(exit::NOT_FOUND); + } + } + }; + if chosen.is_empty() { + eprintln!("no functions recovered"); + return Ok(exit::NOT_FOUND); + } + + let wanted = chosen.len(); + if !budget.unlimited() { + let mut allowed = Vec::with_capacity(chosen.len()); + let mut why = Stopped::Finished; + for f in chosen { + why = budget.take(); + if !why.complete() { + break; + } + allowed.push(f); + } + crate::budget::report(&budget, why, allowed.len(), wanted, "function(s)"); + chosen = allowed; + } + + if as_json { + return json::emit(w, &json::disas(p, &chosen)); + } + + let mut bar = Progress::new("disassembled", chosen.len(), progress && !as_json); + for (n, f) in chosen.iter().enumerate() { + bar.step(); + if n > 0 { + outln!(w); + } + outln!( + w, + "{} <{}>: {} block(s), {}", + w.paint(Role::Addr, &f.entry.to_string()), + w.paint(Role::Name, &f.display_name()), + f.cfg.blocks.len(), + f.provenance + ); + let insns = p.instructions(f); + let mut prev_end: Option = None; + for i in &insns { + // A gap means the function is split; say so rather than implying + // the bytes between are its code. + if prev_end.is_some_and(|e| e != i.addr) { + outln!(w, " ..."); + } + prev_end = Some(i.next()); + let text = e5r_arch::format(&p.object.arch, i, false); + let raw = if show_bytes { + p.object + .memory + .slice(i.addr, i.len as u64) + .map(|b| b.iter().map(|x| format!("{x:02x}")).collect::()) + .unwrap_or_default() + } else { + String::new() + }; + let annot = annotate(p, i); + // An asserted comment comes last, after the tool's own note, so + // the two are never confused: everything before it was worked out + // and everything after it was written down by a person. + let said = match comments.get(&i.addr) { + Some(c) => w.paint(Role::Weak, &format!(" ; {c}")), + None => String::new(), + }; + outln!( + w, + " {}: {:<10}{}{}{said}", + w.paint(Role::Addr, &format!("{:>12x}", i.addr.get())), + raw, + text.replace('\t', " "), + w.paint(Role::Name, &annot) + ); + } + } + Ok(exit::OK) +} + +/// The `` a listing puts after a branch or a data reference. +fn annotate(p: &Program, i: &e5r_arch::Insn) -> String { + let mut out = String::new(); + if let Some(t) = i.flow.target() { + if let Some(n) = p.name_of(t) { + out = format!(" <{n}>"); + } + } + for x in p.xrefs.from(i.addr) { + if x.kind == XrefKind::Data || x.kind == XrefKind::Read || x.kind == XrefKind::Write { + if let Some(s) = p.strings.iter().find(|s| s.addr == x.to) { + out = format!(" ; {:?}", s.text); + } else if let Some(t) = p.text_at(x.to, 64) { + out = format!(" ; {t:?}"); + } else if let Some(n) = p.name_of(x.to) { + out = format!(" ; {n}"); + } + } + } + out +} + +/// References. +pub fn xrefs(w: &mut Out, p: &Program, target: &str, from: bool, as_json: bool) -> R { + let Some(a) = addr::resolve(p, target) else { + return Err(format!("{target:?} is not an address or a symbol")); + }; + let refs = if from { p.xrefs.from(a) } else { p.xrefs.to(a) }; + if as_json { + return json::emit(w, &json::xrefs(p, refs)); + } + if refs.is_empty() { + eprintln!("no references {} {a}", if from { "from" } else { "to" }); + return Ok(exit::NOT_FOUND); + } + for x in refs { + let other = if from { x.to } else { x.from }; + let name = p + .function_at(other) + .map(|f| format!(" in {}", f.display_name())) + .unwrap_or_default(); + outln!( + w, + "{:<10}{}{}", + format!("{:?}", x.kind).to_lowercase(), + other, + name + ); + } + Ok(exit::OK) +} + +/// The strength word, for JSON. +pub fn strength_name(s: Strength) -> &'static str { + s.as_str() +} + +/// A comparison of two builds. +pub fn diff(w: &mut Out, old: &Program, new: &Program, all: bool, as_json: bool) -> R { + let d = e5r_diff::compare(old, new); + if as_json { + return json::emit(w, &json::diff(&d)); + } + outln!( + w, + "{} matched ({} changed, {} identical), {} removed, {} added", + d.matched.len(), + d.changed_count(), + d.identical_count(), + d.removed.len(), + d.added.len() + ); + if d.matched.is_empty() && d.added.is_empty() && d.removed.is_empty() { + eprintln!("nothing to compare"); + return Ok(exit::NOT_FOUND); + } + outln!(w); + outln!( + w, + "{:<12}{:<20}{:<20}{:>7} {:>7} {}", + "similarity", + "old", + "new", + "old n", + "new n", + "name" + ); + for m in d.matched.iter().filter(|m| all || m.changed()) { + outln!( + w, + "{:<12.3}{:<20}{:<20}{:>7} {:>7} {} [{}]", + m.similarity, + m.old.to_string(), + m.new.to_string(), + m.old_insns, + m.new_insns, + m.name, + m.kind.as_str() + ); + } + for (a, n) in &d.removed { + outln!( + w, + "{:<12}{:<20}{:<20}{:>7} {:>7} {n}", + "removed", + a.to_string(), + "", + "", + "" + ); + } + for (a, n) in &d.added { + outln!( + w, + "{:<12}{:<20}{:<20}{:>7} {:>7} {n}", + "added", + "", + a.to_string(), + "", + "" + ); + } + Ok(exit::OK) +} + +/// Decompile one function, or every recovered function, to pseudo-C. +pub fn decompile(w: &mut Out, p: &Program, target: &str, as_json: bool, limits: Limits<'_>) -> R { + let (mut budget, progress, declared) = (limits.budget, limits.progress, limits.declared); + let chosen: Vec<&e5r_analysis::Function> = if target == "all" { + p.functions_by_address().collect() + } else { + let Some(a) = addr::resolve(p, target) else { + return Err(format!("{target:?} is not an address, a symbol, or `all`")); + }; + match p.function(a).or_else(|| p.function_at(a)) { + Some(f) => vec![f], + None => { + eprintln!("no function covers {a}"); + return Ok(exit::NOT_FOUND); + } + } + }; + if chosen.is_empty() { + eprintln!("no functions recovered"); + return Ok(exit::NOT_FOUND); + } + + // Decompiled in chunks so a budget can stop between them. One function at + // a time would lose the cross-function pass that settles call arity, and + // the whole list at once cannot be interrupted; a chunk is the compromise, + // and it is small enough that the budget overshoots by at most one chunk. + const CHUNK: usize = 64; + let wanted = chosen.len(); + let mut bar = Progress::new("decompiled", wanted, progress && !as_json); + let mut unit = e5r_api::Unit::default(); + let mut why = Stopped::Finished; + if budget.unlimited() { + unit = e5r_api::decompile_program_with(p, &chosen, declared); + bar.step(); + } else { + for group in chosen.chunks(CHUNK) { + let mut taking: Vec<&e5r_analysis::Function> = Vec::with_capacity(group.len()); + for f in group { + why = budget.take(); + if !why.complete() { + break; + } + taking.push(f); + } + if !taking.is_empty() { + let part = e5r_api::decompile_program_with(p, &taking, declared); + unit.declarations.extend(part.declarations); + unit.functions.extend(part.functions); + for _ in 0..taking.len() { + bar.step(); + } + } + if !why.complete() { + break; + } + } + // Declarations come from every chunk and repeat across them. + unit.declarations.sort(); + unit.declarations.dedup(); + } + bar.finish(); + let produced = unit.functions.len(); + if as_json { + let items = unit + .functions + .iter() + .map(|d| (p.function(d.addr).unwrap_or(chosen[0]), d)) + .collect(); + return json::emit(w, &json::decompiled(items)); + } + + if !unit.declarations.is_empty() { + outln!(w, "#include "); + for d in &unit.declarations { + outln!(w, "{d}"); + } + outln!(w); + } + crate::budget::report(&budget, why, produced, wanted, "function(s)"); + for (n, d) in unit.functions.iter().enumerate() { + if n > 0 { + outln!(w); + } + // What a reader needs to judge the output: where it came from, and how + // much of it the structuring and the lifter could not express. + outln!(w, "// {}", d.addr); + if d.gotos > 0 || d.unmodelled > 0 { + outln!( + w, + "// {} goto(s), {} unmodelled instruction(s)", + d.gotos, + d.unmodelled + ); + } + for line in d.text.lines() { + outln!(w, "{line}"); + } + } + Ok(exit::OK) +} + +/// Report what a function's pointers point at. +pub fn shapes(w: &mut Out, p: &Program, target: &str, as_json: bool) -> R { + let chosen: Vec<&e5r_analysis::Function> = if target == "all" { + p.functions_by_address().collect() + } else { + let Some(a) = addr::resolve(p, target) else { + return Err(format!("{target:?} is not an address, a symbol, or `all`")); + }; + match p.function(a).or_else(|| p.function_at(a)) { + Some(f) => vec![f], + None => { + eprintln!("no function covers {a}"); + return Ok(exit::NOT_FOUND); + } + } + }; + + let found: Vec<(&e5r_analysis::Function, Vec)> = chosen + .iter() + .map(|f| (*f, e5r_api::shapes_of(p, f))) + .filter(|(_, s)| !s.is_empty()) + .collect(); + + if as_json { + return json::emit(w, &json::shapes(&found)); + } + if found.is_empty() { + eprintln!("no pointers with a recoverable shape"); + return Ok(exit::NOT_FOUND); + } + for (n, (f, pointers)) in found.iter().enumerate() { + if n > 0 { + outln!(w); + } + outln!(w, "{} <{}>:", f.entry, f.display_name()); + for pointer in pointers { + let name = match pointer.argument { + Some(n) => format!("arg{n}"), + None => format!("reg{:x}", pointer.register), + }; + let size = match pointer.size { + Some(s) => format!("{s} bytes"), + None => "size unknown".to_string(), + }; + let access = if pointer.written { + "read and written" + } else { + "read only" + }; + outln!(w, " {name}: {size}, {access}"); + for (offset, width) in &pointer.fields { + outln!(w, " +{offset:<4} {width} byte(s)"); + } + } + } + Ok(exit::OK) +} + +/// Report what a signature library recognized. +pub fn identified( + w: &mut Out, + p: &Program, + library: &e5r_db::signature::Library, + as_json: bool, +) -> R { + let found = e5r_api::identify(p, library); + if as_json { + return json::emit(w, &json::identified(&found)); + } + if found.is_empty() { + eprintln!("nothing in {} signature(s) matched", library.len()); + return Ok(exit::NOT_FOUND); + } + outln!( + w, + "{}", + head( + w, + &format!("{:<20} {:<10} {:<28} {}", "address", "match", "name", "was") + ) + ); + for i in &found { + // The resolution is the point of this listing: `exact` and `shape` are + // evidence, `address` is a coincidence until someone checks it. + let role = match i.resolution.as_str() { + "exact" | "shape" => Role::Strong, + "address" => Role::Unknown, + _ => Role::Weak, + }; + outln!( + w, + "{} {} {} {}", + cell(w, Role::Addr, &i.addr.to_string(), 20), + cell(w, role, i.resolution.as_str(), 10), + cell(w, Role::Name, &i.name, 28), + i.was + ); + } + Ok(exit::OK) +} + +/// Answer a query. +pub fn query(w: &mut Out, p: &Program, text: &str, as_json: bool) -> R { + if text.trim().is_empty() { + return Err("a query is needed: try `functions where insns > 100`".into()); + } + let answer = e5r_api::query::run(p, text)?; + if as_json { + return json::emit(w, &json::query(&answer)); + } + if answer.rows.is_empty() { + eprintln!("nothing matched"); + return Ok(exit::NOT_FOUND); + } + + // Column widths from the data, so a table of addresses does not reserve + // room for the longest name a symbol could have. + let widths: Vec = answer + .columns + .iter() + .enumerate() + .map(|(n, c)| { + answer + .rows + .iter() + .map(|r| { + r.values + .get(n) + .map(|(_, v)| escape(&v.to_string()).len()) + .unwrap_or(0) + }) + .chain(std::iter::once(c.len())) + .max() + .unwrap_or(c.len()) + .min(60) + }) + .collect(); + + let mut header = String::new(); + for (c, width) in answer.columns.iter().zip(&widths) { + let _ = std::fmt::Write::write_fmt(&mut header, format_args!("{c: 60 { &text[..60] } else { &text }; + let _ = std::fmt::Write::write_fmt(&mut line, format_args!("{text: String { + let mut out = String::with_capacity(text.len()); + for c in text.chars() { + match c { + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if (c as u32) < 0x20 => out.push('.'), + c => out.push(c), + } + } + out +} + +/// Run a function and report what it did. +#[allow(clippy::too_many_arguments)] +pub fn emulate( + w: &mut Out, + p: &Program, + target: &str, + args: &[u64], + depth: u32, + budget: u64, + as_json: bool, +) -> R { + let Some(a) = addr::resolve(p, target) else { + return Err(format!("{target:?} is not an address or a symbol")); + }; + let Some(f) = p.function(a).or_else(|| p.function_at(a)) else { + eprintln!("no function covers {a}"); + return Ok(exit::NOT_FOUND); + }; + let setup = e5r_api::Setup { + arguments: args.to_vec(), + depth, + budget, + ..Default::default() + }; + let run = e5r_api::emulate::run(p, f, &setup); + + if as_json { + return json::emit(w, &json::run(f, &run)); + } + outln!(w, "{} <{}>", f.entry, f.display_name()); + outln!(w, " stopped {}", describe(&run.stop)); + outln!(w, " result {:#x} ({})", run.result, run.result as i64); + outln!( + w, + " executed {} instruction(s), {} operation(s)", + run.insns, + run.ops + ); + if !run.unlifted.is_empty() { + let shown: Vec = run.unlifted.iter().take(5).map(|a| a.to_string()).collect(); + outln!( + w, + " unmodelled {} instruction(s): {}", + run.unlifted.len(), + shown.join(", ") + ); + } + if !run.written.is_empty() { + outln!(w, " wrote {} byte(s) of memory", run.written.len()); + } + Ok(exit::OK) +} + +/// Why a run stopped, in words. +fn describe(stop: &e5r_ir::Stop) -> String { + use e5r_ir::Stop; + match stop { + Stop::Returned => "returned".into(), + Stop::Budget => "ran out of budget".into(), + Stop::Unimplemented(a) => { + format!("reached an instruction the lifter does not model at {a}") + } + Stop::NoCode(a) => format!("branched to {a}, where there is no code"), + Stop::Call(a) => format!("called {a}, and calls were not being followed"), + Stop::DivideByZero(a) => format!("divided by zero at {a}"), + } +} + +/// List the virtual tables. +pub fn vtables(w: &mut Out, p: &Program, as_json: bool) -> R { + let tables = e5r_api::vtables(p); + if as_json { + return json::emit(w, &json::vtables(p, &tables)); + } + if tables.is_empty() { + eprintln!("no virtual tables found"); + return Ok(exit::NOT_FOUND); + } + for (n, t) in tables.iter().enumerate() { + if n > 0 { + outln!(w); + } + outln!( + w, + "{} <{}> {} method(s), {}", + t.entry, + t.class.as_deref().unwrap_or("unnamed"), + t.methods.len(), + t.evidence.as_str() + ); + if t.offset_to_top != 0 { + outln!(w, " offset to top {}", t.offset_to_top); + } + for (slot, method) in t.methods.iter().enumerate() { + let name = p + .function(*method) + .map(|f| f.display_name()) + .unwrap_or_else(|| format!("{method}")); + outln!(w, " [{slot}] {method} {name}"); + } + } + Ok(exit::OK) +} + +/// Overlay, entropy and what they suggest. +pub fn overlay(w: &mut Out, o: &Object, data: &[u8], as_json: bool) -> R { + let report = e5r_format::overlay::analyze(o, data); + if as_json { + return json::emit(w, &json::overlay(&report)); + } + outln!(w, "described end {:#x}", report.described_end); + match &report.overlay { + Some(ov) => outln!( + w, + "overlay {:#x}, {} bytes, entropy {:.2}, looks like {}", + ov.offset, + ov.size, + ov.entropy, + ov.content.as_str() + ), + None => outln!(w, "overlay none"), + } + if !report.sections.is_empty() { + outln!(w, ""); + outln!( + w, + "{:<24}{:>10} {:>8} {:>8}", + "section", + "size", + "entropy", + "peak" + ); + for s in &report.sections { + let peak = s.peak().map(|p| p.entropy).unwrap_or(s.entropy); + outln!( + w, + "{:<24}{:>10x} {:>8.2} {:>8.2}", + s.name, + s.size, + s.entropy, + peak + ); + } + } + if !report.findings.is_empty() { + outln!(w, ""); + for f in &report.findings { + let where_ = f.section.as_deref().unwrap_or("image"); + outln!(w, "{:<10} {:<24} {}", f.strength.as_str(), where_, f.detail); + } + } + for warn in &report.warnings { + eprintln!("note: {warn}"); + } + // Nothing found is a real answer about a file, not a failed lookup. + Ok(exit::OK) +} + +/// Members of an `ar` archive, and the symbols each one defines. +pub fn archive(w: &mut Out, data: &[u8], symbols: bool, as_json: bool) -> R { + let ar = e5r_format::archive::open(data).map_err(|e| e.to_string())?; + if as_json { + return json::emit(w, &json::archive(&ar)); + } + outln!( + w, + "{} archive, {} member(s), {} indexed symbol(s){}", + ar.flavor.as_str(), + ar.members.len(), + ar.index.len(), + if ar.thin { ", thin" } else { "" } + ); + outln!(w, ""); + outln!(w, "{:<44}{:>10} {:>10}", "member", "offset", "size"); + for (n, m) in ar.members.iter().enumerate() { + outln!(w, "{:<44}{:>10x} {:>10x}", m.name, m.offset, m.size); + if symbols { + for s in ar.symbols_of(n) { + outln!(w, " {s}"); + } + } + } + for warn in &ar.warnings { + eprintln!("note: {warn}"); + } + if ar.members.is_empty() { + return Ok(exit::NOT_FOUND); + } + Ok(exit::OK) +} + +/// Recovered C++ classes, their hierarchy and their member functions. +pub fn classes(w: &mut Out, p: &Program, members: bool, as_json: bool) -> R { + let opts = e5r_api::classes::Options { + stores: true, + // Recovering what each member touches through `this` lifts every + // member function, which on a real C++ library is tens of seconds. It + // is worth it only when the caller asked to see them. + fields: members, + }; + let found = e5r_api::classes::classes_with(p, &opts); + if as_json { + return json::emit(w, &json::classes(&found)); + } + if found.classes.is_empty() { + eprintln!( + "no classes: {} virtual table(s), type information {}", + found.tables, + found.rtti.as_str() + ); + return Ok(exit::NOT_FOUND); + } + outln!( + w, + "{} class(es) from {} table(s); {} named by symbol, {} only by type information ({}, {})", + found.classes.len(), + found.tables, + found.named_by_symbol, + found.named_by_rtti, + found.abi.as_str(), + found.rtti.as_str() + ); + outln!(w, ""); + for c in &found.classes { + let name = c.display_name(); + outln!( + w, + "{} {}", + w.paint(Role::Name, &name), + w.paint(Role::Weak, c.basis.as_str()) + ); + for b in &c.bases { + outln!( + w, + " {} {} at {}{}", + if b.is_virtual { "virtual base" } else { "base" }, + w.paint(Role::Name, &b.name), + b.offset, + if b.is_public { "" } else { " (not public)" } + ); + } + if members { + for m in &c.members { + let what = m.name.clone().unwrap_or_else(|| m.addr.to_string()); + outln!( + w, + " {:<20}{:<24}{}", + format!("{:?}", m.role).to_lowercase(), + last(w, 48, &what), + w.paint(Role::Weak, m.basis.as_str()) + ); + } + } + } + Ok(exit::OK) +} diff --git a/crates/e5r-cli/src/progress.rs b/crates/e5r-cli/src/progress.rs new file mode 100644 index 0000000..f1130f2 --- /dev/null +++ b/crates/e5r-cli/src/progress.rs @@ -0,0 +1,134 @@ +//! Progress on stderr, for the jobs that are not instant. +//! +//! stderr rather than stdout, so a progress line never lands in a document +//! somebody is parsing, and only when stderr is a terminal, so a log file or a +//! pipe gets nothing. That is the whole interface contract: the output of a +//! command is exactly what it was without this file. +//! +//! The estimate is from the rate so far, which is right when the work per item +//! is even and wrong when it is not. It is printed as a time rather than a +//! percentage for that reason: a reader treats "12s left" as the guess it is. + +use std::io::{IsTerminal, Write}; +use std::time::{Duration, Instant}; + +/// How often to redraw. Fast enough to look live, slow enough that the +/// terminal is not the bottleneck on a 40,000 item job. +const TICK: Duration = Duration::from_millis(100); + +/// A counter that draws itself on stderr. +pub struct Progress { + what: &'static str, + total: usize, + done: usize, + started: Instant, + last_drawn: Instant, + /// False when stderr is not a terminal, in which case nothing is drawn. + live: bool, + /// Set once something has been drawn, so the line is only cleared if it + /// was ever written. + drawn: bool, +} + +impl Progress { + /// A counter over a known number of items. + /// + /// Silent when stderr is not a terminal, when the caller asked for JSON, + /// or when the job is small enough that a person would not wait for it. + pub fn new(what: &'static str, total: usize, wanted: bool) -> Progress { + let live = wanted && total > 64 && std::io::stderr().is_terminal(); + Progress { + what, + total, + done: 0, + started: Instant::now(), + // Back-dated so the first item draws immediately: a job that + // starts with a slow item should say so before it begins it. + last_drawn: Instant::now() - TICK, + live, + drawn: false, + } + } + + /// Count one item and redraw if it is time. + pub fn step(&mut self) { + self.done += 1; + if !self.live || self.last_drawn.elapsed() < TICK { + return; + } + self.last_drawn = Instant::now(); + self.draw(); + } + + fn draw(&mut self) { + let elapsed = self.started.elapsed().as_secs_f64(); + let left = if self.done > 0 && elapsed > 0.2 { + let per = elapsed / self.done as f64; + let remaining = self.total.saturating_sub(self.done) as f64 * per; + format!(", about {}s left", remaining.round() as u64) + } else { + String::new() + }; + let mut e = std::io::stderr(); + // Carriage return and clear to end of line, so the line is rewritten + // in place rather than scrolling the terminal. + let _ = write!( + e, + "\r\x1b[K{} {} of {}{left}", + self.what, self.done, self.total + ); + let _ = e.flush(); + self.drawn = true; + } + + /// Erase the line, so whatever prints next starts clean. + pub fn finish(mut self) { + if self.drawn { + let mut e = std::io::stderr(); + let _ = write!(e, "\r\x1b[K"); + let _ = e.flush(); + self.drawn = false; + } + } +} + +impl Drop for Progress { + /// A job that returns early still clears its line. + fn drop(&mut self) { + if self.drawn { + let mut e = std::io::stderr(); + let _ = write!(e, "\r\x1b[K"); + let _ = e.flush(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nothing_is_drawn_when_it_was_not_asked_for() { + let mut p = Progress::new("functions", 1000, false); + for _ in 0..1000 { + p.step(); + } + assert!(!p.drawn); + assert_eq!(p.done, 1000); + } + + #[test] + fn a_small_job_is_never_live() { + // Under the threshold, so it stays silent even on a terminal. + let p = Progress::new("functions", 10, true); + assert!(!p.live); + } + + #[test] + fn counting_is_independent_of_drawing() { + let mut p = Progress::new("functions", 100, false); + p.step(); + p.step(); + assert_eq!(p.done, 2); + } +} diff --git a/crates/e5r-cli/src/repl.rs b/crates/e5r-cli/src/repl.rs new file mode 100644 index 0000000..4a0a5c4 --- /dev/null +++ b/crates/e5r-cli/src/repl.rs @@ -0,0 +1,205 @@ +//! The interactive session. +//! +//! One analysis, many questions. The point is not to save typing: it is that +//! analyzing a large binary takes seconds, and a session that paid that per +//! command would be unusable on exactly the binaries where a session is worth +//! having. +//! +//! Commands are the ones the command line already has, spelled the same way, +//! so nothing has to be learned twice and anything read in the manual works +//! here. The binary is fixed for the session, so a command does not name it. + +use std::io::{BufRead, IsTerminal, Write}; + +use e5r_analysis::Program; +use e5r_core::Addr; + +use crate::out::{Out, outln}; +use crate::{Cli, Command, Common, exit}; + +/// What a short name stands for. Only for the commands typed a hundred times +/// an hour; everything else is spelled out, because a grammar of two-letter +/// pairs is what this is meant to be an alternative to. +const ALIASES: &[(&str, &str)] = &[ + ("d", "disas"), + ("dec", "decompile"), + ("f", "funcs"), + ("i", "info"), + ("s", "strings"), + ("x", "xrefs"), + ("q", "query"), + ("?", "help"), +]; + +/// Run the session. +pub fn run( + w: &mut Out, + cli: &Cli, + common: &Common, + program: &mut Program, + data: &[u8], +) -> Result { + let interactive = std::io::stdin().is_terminal(); + if interactive { + outln!( + w, + "{} function(s) in {}. `help` lists the commands, `quit` leaves.", + program.functions.len(), + common.file.display() + ); + } + // The prompt carries the current address, because the commands that take + // one are the commands that get typed repeatedly. + let mut here: Option = program.functions_by_address().next().map(|f| f.entry); + let stdin = std::io::stdin(); + let mut line = String::new(); + loop { + if interactive { + let at = here.map(|a| a.to_string()).unwrap_or_else(|| "?".into()); + let mut e = std::io::stderr(); + let _ = write!(e, "{at}> "); + let _ = e.flush(); + } + line.clear(); + if stdin + .lock() + .read_line(&mut line) + .map_err(|e| e.to_string())? + == 0 + { + break; // end of input, which is how a piped session ends + } + let text = line.split('#').next().unwrap_or("").trim(); + if text.is_empty() { + continue; + } + match step(w, cli, common, program, data, text, &mut here) { + Step::Continue => {} + Step::Leave => break, + } + } + if interactive { + outln!(w, ""); + } + Ok(exit::OK) +} + +/// Whether the session goes on. +enum Step { + Continue, + Leave, +} + +fn step( + w: &mut Out, + cli: &Cli, + common: &Common, + program: &mut Program, + data: &[u8], + text: &str, + here: &mut Option, +) -> Step { + let Some(words) = crate::batch::split(text) else { + eprintln!("e5r: a quote was never closed"); + return Step::Continue; + }; + let Some(first) = words.first().map(String::as_str) else { + return Step::Continue; + }; + match first { + "quit" | "exit" | "q!" => return Step::Leave, + "help" | "?" => { + help(w); + return Step::Continue; + } + // Moving is its own command because every listing command defaults to + // where you are, and typing the address into each one is the thing a + // session exists to avoid. + "seek" | "s@" => { + match words.get(1).and_then(|t| crate::addr::resolve(program, t)) { + Some(a) => *here = Some(a), + None => eprintln!("e5r: no such address"), + } + return Step::Continue; + } + _ => {} + } + + let name = ALIASES + .iter() + .find(|(short, _)| *short == first) + .map(|(_, long)| *long) + .unwrap_or(first); + + // The file comes from the session, not the line, so a command cannot be + // pointed at a different binary than the one that was analyzed. + let mut argv: Vec = vec!["e5r".into(), name.into(), common.file.display().to_string()]; + let mut rest: Vec = words[1..].to_vec(); + // A command that takes a target and was given none gets the current + // address, which is what makes seeking worth anything. + if rest.is_empty() + && matches!(name, "disas" | "decompile" | "shapes" | "xrefs") + && let Some(a) = here + { + rest.push(a.to_string()); + } + argv.extend(rest); + + let parsed = match ::try_parse_from(&argv) { + Ok(p) => p, + Err(e) => { + eprintln!("e5r: {e}"); + return Step::Continue; + } + }; + match &parsed.command { + // Both of these would analyze the file again, which is the cost this + // exists to avoid, and nesting a session inside one answers nothing. + Command::Repl { .. } | Command::Batch { .. } => { + eprintln!("e5r: not from a session"); + return Step::Continue; + } + _ => {} + } + + // The same analysis, borrowed, for every command: this is the whole point. + // Dispatch is exactly the command line's own path, so a session and a + // one-shot invocation cannot disagree about what a command does. + let opts = e5r_analysis::Options::default(); + let load = e5r_format::LoadOptions::default(); + if let Err(e) = crate::dispatch(&parsed, w, program, data, &load, &opts) { + eprintln!("e5r: {e}"); + } + let _ = cli; + Step::Continue +} + +fn help(w: &mut Out) { + outln!(w, "Commands are the ones e5r takes, without the file:"); + outln!(w, ""); + outln!( + w, + " info sections symbols imports exports funcs stats" + ); + outln!(w, " disas [where] decompile [where] shapes [where]"); + outln!( + w, + " xrefs [where] strings query " + ); + outln!(w, " classes vtables sig annotate overlay archive"); + outln!(w, ""); + outln!( + w, + " seek move, so the commands above need no address" + ); + outln!(w, " quit leave"); + outln!(w, ""); + outln!(w, "Short forms: d disas, dec decompile, f funcs, i info,"); + outln!(w, " s strings, x xrefs, q query, ? help."); + outln!(w, ""); + outln!( + w, + "`where` is an address, a symbol, `main+0x20`, or `all`. Anything the" + ); + outln!(w, "manual says works here, and --json works too."); +} diff --git a/crates/e5r-cli/src/shell.rs b/crates/e5r-cli/src/shell.rs new file mode 100644 index 0000000..8ca00fe --- /dev/null +++ b/crates/e5r-cli/src/shell.rs @@ -0,0 +1,270 @@ +//! Shell completions and a man page, written from the command tree itself. +//! +//! Generated rather than checked in, because a completion script that lists +//! yesterday's subcommands is worse than none: it tells the user a command +//! exists that does not. Everything here walks clap's own tree, so adding a +//! subcommand adds it to all four outputs and nothing has to be remembered. + +use std::fmt::Write as _; + +use clap::Command; + +/// The shells a script can be written for. +#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] +pub enum Shell { + /// GNU bash. + Bash, + /// Z shell. + Zsh, + /// fish. + Fish, +} + +/// A completion script for one shell. +pub fn completions(command: &Command, shell: Shell) -> String { + let name = command.get_name().to_string(); + match shell { + Shell::Bash => bash(command, &name), + Shell::Zsh => zsh(command, &name), + Shell::Fish => fish(command, &name), + } +} + +fn subcommands(command: &Command) -> Vec<&Command> { + command.get_subcommands().collect() +} + +/// Every option a subcommand takes, long form, including the global ones. +fn options(command: &Command, root: &Command) -> Vec { + let mut out: Vec = command + .get_arguments() + .filter_map(|a| a.get_long().map(|l| format!("--{l}"))) + .collect(); + out.extend( + root.get_arguments() + .filter(|a| a.is_global_set()) + .filter_map(|a| a.get_long().map(|l| format!("--{l}"))), + ); + out.push("--help".into()); + out.sort(); + out.dedup(); + out +} + +fn bash(root: &Command, name: &str) -> String { + let mut out = format!("# bash completion for {name}, generated by `{name} completions bash`\n"); + let names: Vec = subcommands(root) + .iter() + .map(|c| c.get_name().to_string()) + .collect(); + let _ = writeln!(out, "_{name}() {{"); + out.push_str(" local cur prev words cword\n"); + out.push_str(" cur=\"${COMP_WORDS[COMP_CWORD]}\"\n"); + out.push_str(" local subcommand=\"\"\n"); + out.push_str(" local i\n"); + out.push_str(" for ((i = 1; i < COMP_CWORD; i++)); do\n"); + out.push_str(" case \"${COMP_WORDS[i]}\" in\n"); + let _ = writeln!( + out, + " {}) subcommand=\"${{COMP_WORDS[i]}}\"; break ;;", + names.join("|") + ); + out.push_str(" esac\n done\n"); + out.push_str(" case \"$subcommand\" in\n"); + for sub in subcommands(root) { + let _ = writeln!( + out, + " {}) COMPREPLY=($(compgen -W \"{}\" -- \"$cur\")); \ + [ -z \"$cur\" ] || [ \"${{cur:0:1}}\" != \"-\" ] && \ + COMPREPLY+=($(compgen -f -- \"$cur\")) ;;", + sub.get_name(), + options(sub, root).join(" ") + ); + } + let _ = writeln!( + out, + " \"\") COMPREPLY=($(compgen -W \"{} --help --version\" -- \"$cur\")) ;;", + names.join(" ") + ); + out.push_str(" esac\n}\n"); + let _ = writeln!(out, "complete -F _{name} {name}"); + out +} + +fn zsh(root: &Command, name: &str) -> String { + let mut out = format!("#compdef {name}\n# generated by `{name} completions zsh`\n"); + let _ = writeln!(out, "_{name}() {{"); + out.push_str(" local -a commands\n commands=(\n"); + for sub in subcommands(root) { + let about = sub.get_about().map(|a| a.to_string()).unwrap_or_default(); + let about = about.lines().next().unwrap_or("").replace([':', '\''], " "); + let _ = writeln!(out, " '{}:{}'", sub.get_name(), about); + } + out.push_str(" )\n"); + out.push_str(" _arguments -C '1: :->command' '*:: :->argument'\n"); + out.push_str(" case $state in\n"); + let _ = writeln!( + out, + " command) _describe '{name} command' commands ;;" + ); + out.push_str(" argument)\n case $words[1] in\n"); + for sub in subcommands(root) { + let _ = writeln!( + out, + " {}) _arguments {} '*:file:_files' ;;", + sub.get_name(), + options(sub, root) + .iter() + .map(|o| format!("'{o}'")) + .collect::>() + .join(" ") + ); + } + out.push_str(" esac\n ;;\n esac\n}\n"); + let _ = writeln!(out, "_{name} \"$@\""); + out +} + +fn fish(root: &Command, name: &str) -> String { + let mut out = format!("# fish completion for {name}, generated by `{name} completions fish`\n"); + let _ = writeln!( + out, + "complete -c {name} -f -n '__fish_use_subcommand' -a '' -d ''" + ); + for sub in subcommands(root) { + let about = sub.get_about().map(|a| a.to_string()).unwrap_or_default(); + let about = about.lines().next().unwrap_or("").replace('\'', " "); + let _ = writeln!( + out, + "complete -c {name} -n '__fish_use_subcommand' -a '{}' -d '{about}'", + sub.get_name() + ); + for option in options(sub, root) { + let _ = writeln!( + out, + "complete -c {name} -n '__fish_seen_subcommand_from {}' -l '{}'", + sub.get_name(), + option.trim_start_matches('-') + ); + } + } + out +} + +/// A man page in roff, section one. +pub fn manpage(root: &Command) -> String { + let name = root.get_name().to_uppercase(); + let lower = root.get_name(); + let mut out = String::new(); + let _ = writeln!( + out, + ".TH {name} 1 \"\" \"{lower} {}\" \"Reverse engineering\"", + root.get_version().unwrap_or("") + ); + out.push_str(".SH NAME\n"); + let _ = writeln!( + out, + "{lower} \\- {}", + root.get_about().map(|a| a.to_string()).unwrap_or_default() + ); + out.push_str(".SH SYNOPSIS\n"); + let _ = writeln!(out, ".B {lower}\n[\\fICOMMAND\\fR] [\\fIOPTIONS\\fR]"); + + if let Some(long) = root.get_long_about() { + out.push_str(".SH DESCRIPTION\n"); + for line in long.to_string().lines() { + if line.trim().is_empty() { + out.push_str(".PP\n"); + } else { + let _ = writeln!(out, "{}", escape(line)); + } + } + } + + out.push_str(".SH COMMANDS\n"); + for sub in subcommands(root) { + let _ = writeln!(out, ".TP\n.B {}", sub.get_name()); + let about = sub + .get_long_about() + .or_else(|| sub.get_about()) + .map(|a| a.to_string()) + .unwrap_or_default(); + for line in about.lines() { + if line.trim().is_empty() { + out.push_str(".PP\n"); + } else { + let _ = writeln!(out, "{}", escape(line)); + } + } + let taken = options(sub, root); + if taken.len() > 1 { + let _ = writeln!(out, ".RS\n.PP\nOptions: {}\n.RE", taken.join(", ")); + } + } + + out.push_str(".SH EXIT STATUS\n"); + out.push_str(".TP\n.B 0\nThe command did what was asked.\n"); + out.push_str(".TP\n.B 1\nWhat was asked for was not found.\n"); + out.push_str(".TP\n.B 2\nThe command line was wrong.\n"); + out.push_str(".TP\n.B 3\nThe input could not be read.\n"); + out +} + +/// Roff takes a leading dot as a request, and a backslash as an escape. +fn escape(line: &str) -> String { + let line = line.replace('\\', "\\\\"); + if line.starts_with('.') || line.starts_with('\'') { + format!("\\&{line}") + } else { + line + } +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::CommandFactory; + + fn command() -> Command { + crate::Cli::command() + } + + #[test] + fn every_subcommand_appears_in_every_shell() { + let root = command(); + let names: Vec = root + .get_subcommands() + .map(|c| c.get_name().to_string()) + .collect(); + assert!(names.len() > 5, "only {} subcommands", names.len()); + for shell in [Shell::Bash, Shell::Zsh, Shell::Fish] { + let script = completions(&command(), shell); + for name in &names { + assert!( + script.contains(name.as_str()), + "{shell:?} completion does not mention {name}" + ); + } + } + } + + #[test] + fn the_man_page_names_every_command() { + let page = manpage(&command()); + for sub in command().get_subcommands() { + assert!( + page.contains(sub.get_name()), + "the man page does not mention {}", + sub.get_name() + ); + } + assert!(page.starts_with(".TH E5R 1")); + } + + #[test] + fn a_roff_request_in_the_text_is_escaped() { + // A line starting with a dot would be a formatting request. + assert_eq!(escape(".notarequest"), "\\&.notarequest"); + assert_eq!(escape("ordinary"), "ordinary"); + } +} diff --git a/crates/e5r-core/Cargo.toml b/crates/e5r-core/Cargo.toml new file mode 100644 index 0000000..cfeeee2 --- /dev/null +++ b/crates/e5r-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "e5r-core" +description = "e5r — addresses, memory maps, provenance, and the hostile-input reader." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +serde = { workspace = true } diff --git a/crates/e5r-core/src/addr.rs b/crates/e5r-core/src/addr.rs new file mode 100644 index 0000000..469cede --- /dev/null +++ b/crates/e5r-core/src/addr.rs @@ -0,0 +1,272 @@ +//! Addresses and ranges. 64-bit throughout, arithmetic always checked: +//! hostile files contain headers that wrap the address space. + +use std::fmt; + +use serde::{Deserialize, Serialize}; + +/// A virtual address. Displays as `0x` hex. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Addr(pub u64); + +impl Addr { + /// Address zero. Not null: firmware puts code there. + pub const ZERO: Addr = Addr(0); + /// The highest representable address. + pub const MAX: Addr = Addr(u64::MAX); + + #[inline] + /// An address at `v`. + pub const fn new(v: u64) -> Self { + Addr(v) + } + + #[inline] + /// The raw 64-bit value. + pub const fn get(self) -> u64 { + self.0 + } + + /// Add an offset, or `None` if the sum does not fit in 64 bits. + #[inline] + pub const fn checked_add(self, n: u64) -> Option { + match self.0.checked_add(n) { + Some(v) => Some(Addr(v)), + None => None, + } + } + + /// Subtract an offset, or `None` if the result would be negative. + #[inline] + pub const fn checked_sub(self, n: u64) -> Option { + match self.0.checked_sub(n) { + Some(v) => Some(Addr(v)), + None => None, + } + } + + /// Add a signed displacement, wrapping like the CPU does. + #[inline] + pub const fn wrapping_offset(self, delta: i64) -> Addr { + Addr(self.0.wrapping_add(delta as u64)) + } + + /// Distance from `self` to `other`, or `None` if `other` is below `self`. + #[inline] + pub const fn distance_to(self, other: Addr) -> Option { + other.0.checked_sub(self.0) + } + + /// Round down to a multiple of `align`. `align` must be a power of two. + #[inline] + pub const fn align_down(self, align: u64) -> Addr { + debug_assert!(align.is_power_of_two()); + Addr(self.0 & !(align - 1)) + } + + /// Round up to a multiple of `align`, or `None` on overflow. + #[inline] + pub fn align_up(self, align: u64) -> Option { + debug_assert!(align.is_power_of_two()); + self.0 + .checked_add(align - 1) + .map(|v| Addr(v & !(align - 1))) + } +} + +impl fmt::Debug for Addr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#x}", self.0) + } +} + +impl fmt::Display for Addr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#x}", self.0) + } +} + +impl fmt::LowerHex for Addr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +impl From for Addr { + fn from(v: u64) -> Self { + Addr(v) + } +} + +/// Half-open `start..end`. Cannot be built inverted, so callers never check. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct AddrRange { + start: Addr, + end: Addr, +} + +impl AddrRange { + /// Build from a start and an end, or `None` if `end < start`. + #[inline] + pub const fn new(start: Addr, end: Addr) -> Option { + if end.0 < start.0 { + None + } else { + Some(AddrRange { start, end }) + } + } + + /// Build from a start and a length, or `None` if the end would overflow. + #[inline] + pub const fn sized(start: Addr, len: u64) -> Option { + match start.checked_add(len) { + Some(end) => Some(AddrRange { start, end }), + None => None, + } + } + + /// An empty range at `at`. + #[inline] + pub const fn empty_at(at: Addr) -> AddrRange { + AddrRange { start: at, end: at } + } + + #[inline] + /// The first address in the range. + pub const fn start(self) -> Addr { + self.start + } + + #[inline] + /// One past the last address in the range. + pub const fn end(self) -> Addr { + self.end + } + + #[inline] + /// How many addresses the range covers. + pub const fn len(self) -> u64 { + self.end.0 - self.start.0 + } + + #[inline] + /// True when the range covers no addresses. + pub const fn is_empty(self) -> bool { + self.start.0 == self.end.0 + } + + #[inline] + /// True when `a` falls inside the range. + pub const fn contains(self, a: Addr) -> bool { + a.0 >= self.start.0 && a.0 < self.end.0 + } + + #[inline] + /// True when `other` lies entirely inside this range. + pub const fn contains_range(self, other: AddrRange) -> bool { + other.start.0 >= self.start.0 && other.end.0 <= self.end.0 + } + + /// True when the ranges share an address. Empty ranges overlap nothing. + #[inline] + pub const fn overlaps(self, other: AddrRange) -> bool { + !self.is_empty() + && !other.is_empty() + && self.start.0 < other.end.0 + && other.start.0 < self.end.0 + } + + /// The shared part of two ranges, or `None` when they are disjoint. + #[inline] + pub fn intersect(self, other: AddrRange) -> Option { + let start = Addr(self.start.0.max(other.start.0)); + let end = Addr(self.end.0.min(other.end.0)); + if start.0 < end.0 { + Some(AddrRange { start, end }) + } else { + None + } + } + + /// Iterate the addresses in the range. + pub fn iter(self) -> impl Iterator { + (self.start.0..self.end.0).map(Addr) + } +} + +impl fmt::Debug for AddrRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#x}..{:#x}", self.start.0, self.end.0) + } +} + +impl fmt::Display for AddrRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#x}..{:#x}", self.start.0, self.end.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checked_add_refuses_to_wrap() { + assert_eq!(Addr::MAX.checked_add(1), None); + assert_eq!(Addr(0x1000).checked_add(0x10), Some(Addr(0x1010))); + } + + #[test] + fn wrapping_offset_follows_the_machine() { + // A backward branch from the bottom of the address space is what the + // CPU computes, so it is what we report. + assert_eq!(Addr(4).wrapping_offset(-8), Addr(u64::MAX - 3)); + assert_eq!(Addr(0x1000).wrapping_offset(-0x10), Addr(0xff0)); + } + + #[test] + fn inverted_ranges_cannot_be_built() { + assert!(AddrRange::new(Addr(0x20), Addr(0x10)).is_none()); + assert!(AddrRange::new(Addr(0x10), Addr(0x10)).is_some()); + } + + #[test] + fn sized_range_refuses_to_wrap() { + // The hostile-section-header case: a start near the top plus a length + // that carries past the end of the address space. + assert!(AddrRange::sized(Addr(u64::MAX - 0x10), 0x20).is_none()); + assert_eq!( + AddrRange::sized(Addr(0x1000), 0x100).map(|r| r.end()), + Some(Addr(0x1100)) + ); + } + + #[test] + fn empty_ranges_overlap_nothing() { + let empty = AddrRange::empty_at(Addr(0x1000)); + let covering = AddrRange::sized(Addr(0), 0x2000).unwrap(); + assert!(!empty.overlaps(empty)); + assert!(!covering.overlaps(empty)); + assert!(covering.contains_range(empty)); + } + + #[test] + fn intersect_is_symmetric() { + let a = AddrRange::sized(Addr(0x1000), 0x100).unwrap(); + let b = AddrRange::sized(Addr(0x1080), 0x100).unwrap(); + let want = AddrRange::sized(Addr(0x1080), 0x80).unwrap(); + assert_eq!(a.intersect(b), Some(want)); + assert_eq!(b.intersect(a), Some(want)); + let c = AddrRange::sized(Addr(0x4000), 0x10).unwrap(); + assert_eq!(a.intersect(c), None); + } + + #[test] + fn alignment() { + assert_eq!(Addr(0x1234).align_down(0x1000), Addr(0x1000)); + assert_eq!(Addr(0x1234).align_up(0x1000), Some(Addr(0x2000))); + assert_eq!(Addr(0x2000).align_up(0x1000), Some(Addr(0x2000))); + assert_eq!(Addr::MAX.align_up(0x1000), None); + } +} diff --git a/crates/e5r-core/src/arch.rs b/crates/e5r-core/src/arch.rs new file mode 100644 index 0000000..7761f4a --- /dev/null +++ b/crates/e5r-core/src/arch.rs @@ -0,0 +1,188 @@ +//! Architectures, byte order, and pointer width. + +use std::fmt; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +/// Byte order of a container and of the machine it targets. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Endian { + /// Least significant byte first. + Little, + /// Most significant byte first. + Big, +} + +/// Pointer width. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Bits { + /// 16-bit pointers. + Bits16, + /// 32-bit pointers. + Bits32, + /// 64-bit pointers. + Bits64, +} + +impl Bits { + /// Pointer size in bytes. + pub const fn bytes(self) -> u64 { + match self { + Bits::Bits16 => 2, + Bits::Bits32 => 4, + Bits::Bits64 => 8, + } + } + + /// A mask covering a pointer of this width. + pub const fn mask(self) -> u64 { + match self { + Bits::Bits16 => 0xffff, + Bits::Bits32 => 0xffff_ffff, + Bits::Bits64 => u64::MAX, + } + } +} + +impl fmt::Display for Bits { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.bytes() * 8) + } +} + +/// An instruction set. Natively decoded ones get a variant; everything a +/// SLEIGH spec reaches arrives as [`Arch::Sleigh`], so breadth needs no edit here. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Arch { + /// 32-bit x86. + X86, + /// 64-bit x86, the `amd64` / `x86-64` naming both appear in the wild. + X86_64, + /// 32-bit ARM in the A32 instruction set, whose instructions are one + /// aligned word. + Arm, + /// 32-bit ARM in the T32 instruction set: the same registers and the same + /// calling convention, and instructions of two or four bytes. A separate + /// variant rather than a flag because the bytes never say which set an + /// address holds, exactly as x86's two modes need telling apart, and a + /// decoder that guesses produces plausible nonsense rather than nothing. + Thumb, + /// 64-bit ARM, A64. + AArch64, + /// Decoded by a SLEIGH language rather than by a hand-written decoder. + Sleigh(String), + /// Recognized container, unknown machine. Analysis stops at the container. + Unknown(u32), +} + +impl Arch { + /// The width this architecture's pointers have by default. + pub fn bits(&self) -> Bits { + match self { + Arch::X86 | Arch::Arm | Arch::Thumb => Bits::Bits32, + Arch::X86_64 | Arch::AArch64 => Bits::Bits64, + Arch::Sleigh(_) | Arch::Unknown(_) => Bits::Bits64, + } + } + + /// True when e5r has a native decoder for this architecture. + pub fn has_native_decoder(&self) -> bool { + matches!( + self, + Arch::X86 | Arch::X86_64 | Arch::AArch64 | Arch::Arm | Arch::Thumb + ) + } + + /// Instruction alignment. One byte is what makes x86 sweep ambiguous. + pub fn insn_alignment(&self) -> u64 { + match self { + Arch::X86 | Arch::X86_64 => 1, + Arch::Arm => 4, + Arch::Thumb => 2, + Arch::AArch64 => 4, + Arch::Sleigh(_) | Arch::Unknown(_) => 1, + } + } + + /// The longest instruction, used to bound a decode window. + pub fn max_insn_len(&self) -> u64 { + match self { + Arch::X86 | Arch::X86_64 => 15, + Arch::Arm | Arch::Thumb => 4, + Arch::AArch64 => 4, + Arch::Sleigh(_) | Arch::Unknown(_) => 16, + } + } + + /// The short name used on the command line and in JSON. + pub fn name(&self) -> &str { + match self { + Arch::X86 => "x86", + Arch::X86_64 => "x86-64", + Arch::Arm => "arm", + Arch::Thumb => "thumb", + Arch::AArch64 => "aarch64", + Arch::Sleigh(id) => id, + Arch::Unknown(_) => "unknown", + } + } +} + +impl fmt::Display for Arch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Arch::Unknown(m) => write!(f, "unknown({m:#x})"), + other => f.write_str(other.name()), + } + } +} + +impl FromStr for Arch { + type Err = String; + + fn from_str(s: &str) -> Result { + // Accept what readelf and objdump print, so a pasted command works. + Ok(match s.to_ascii_lowercase().replace('_', "-").as_str() { + "x86" | "i386" | "i686" | "x86-32" => Arch::X86, + "x86-64" | "amd64" | "x64" => Arch::X86_64, + "arm" | "armv7" => Arch::Arm, + "thumb" => Arch::Thumb, + "aarch64" | "arm64" | "armv8" => Arch::AArch64, + other => return Err(format!("unknown architecture {other:?}")), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_spellings_other_tools_print_all_parse() { + for s in ["x86-64", "amd64", "x64", "X86_64"] { + assert_eq!(s.parse::().unwrap(), Arch::X86_64, "{s}"); + } + for s in ["aarch64", "arm64", "ARMv8"] { + assert_eq!(s.parse::().unwrap(), Arch::AArch64, "{s}"); + } + assert!("vax".parse::().is_err()); + } + + #[test] + fn alignment_reflects_the_isa() { + // The reason linear sweep is hard on x86 and easy on AArch64. + assert_eq!(Arch::X86_64.insn_alignment(), 1); + assert_eq!(Arch::AArch64.insn_alignment(), 4); + } + + #[test] + fn sleigh_languages_do_not_need_an_enum_variant() { + let a = Arch::Sleigh("MIPS:BE:32:default".to_string()); + assert_eq!(a.name(), "MIPS:BE:32:default"); + assert!(!a.has_native_decoder()); + } +} diff --git a/crates/e5r-core/src/error.rs b/crates/e5r-core/src/error.rs new file mode 100644 index 0000000..540f3e9 --- /dev/null +++ b/crates/e5r-core/src/error.rs @@ -0,0 +1,220 @@ +//! Errors and resource caps. +//! +//! Parsing never panics. Variants carry enough to find the problem in a hex +//! editor: "invalid ELF" is useless, an offset and a length are a bug report. + +use std::fmt; + +use crate::addr::Addr; + +/// The workspace result type. +pub type Result = std::result::Result; + +/// What went wrong. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + /// A read ran past the end of the buffer. + OutOfBounds { + /// What the caller was trying to read. + what: &'static str, + /// Byte offset the read started at. + offset: u64, + /// How many bytes were wanted. + len: u64, + /// How many bytes there are. + available: u64, + }, + /// A field held a value the format does not allow. + BadField { + /// Which field, named as the specification names it. + field: &'static str, + /// What it held. + value: u64, + /// Why that is not acceptable. + reason: &'static str, + }, + /// The file is not in a format this loader recognizes. + NotRecognized { + /// What was expected, for the error message. + expected: &'static str, + }, + /// The format is recognized but this build cannot handle this variant. + Unsupported { + /// The feature that is missing. + what: String, + }, + /// An address has no bytes behind it in the memory map. + Unmapped { + /// The address that was not mapped. + addr: Addr, + }, + /// A count exceeded a cap. The cap is in the error so it can be raised. + CapExceeded { + /// What was being counted. + what: &'static str, + /// The count the file asked for. + requested: u64, + /// The limit in force. + limit: u64, + }, + /// Two facts that must agree do not. + Inconsistent { + /// A description of the disagreement. + detail: String, + }, + /// Something failed underneath, most often the filesystem. + Io { + /// What the operation was. + what: String, + /// The OS error message. + detail: String, + }, +} + +impl Error { + /// Build an [`Error::Unsupported`] without the caller writing `.to_string()`. + pub fn unsupported(what: impl Into) -> Error { + Error::Unsupported { what: what.into() } + } + + /// Build an [`Error::Inconsistent`]. + pub fn inconsistent(detail: impl Into) -> Error { + Error::Inconsistent { + detail: detail.into(), + } + } + + /// True for "not my format", the one error a format probe expects. + pub fn is_not_recognized(&self) -> bool { + matches!(self, Error::NotRecognized { .. }) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::OutOfBounds { + what, + offset, + len, + available, + } => write!( + f, + "{what}: read of {len:#x} bytes at offset {offset:#x} runs past the end \ + ({available:#x} bytes available)" + ), + Error::BadField { + field, + value, + reason, + } => write!(f, "{field} is {value:#x}, which {reason}"), + Error::NotRecognized { expected } => write!(f, "not a {expected}"), + Error::Unsupported { what } => write!(f, "unsupported: {what}"), + Error::Unmapped { addr } => write!(f, "address {addr} is not mapped"), + Error::CapExceeded { + what, + requested, + limit, + } => write!( + f, + "{what}: file asks for {requested}, limit is {limit}. Raise it if the file \ + is genuinely this large." + ), + Error::Inconsistent { detail } => write!(f, "inconsistent: {detail}"), + Error::Io { what, detail } => write!(f, "{what}: {detail}"), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io { + what: "io".to_string(), + detail: e.to_string(), + } + } +} + +/// Caps on counts the input file controls. A file claiming 4 billion sections +/// must not allocate until the process dies. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Caps { + /// Sections or segments in one container. + pub sections: u64, + /// Symbols in one table. + pub symbols: u64, + /// Relocations in one table. + pub relocations: u64, + /// Bytes in one string, before it is treated as unterminated. + pub string_len: u64, + /// Instructions decoded in one function before analysis gives up. + pub function_insns: u64, + /// Basic blocks in one function. + pub function_blocks: u64, + /// Entries in one recovered jump table. + pub jump_table_entries: u64, +} + +impl Default for Caps { + fn default() -> Self { + Caps { + sections: 1 << 16, + symbols: 1 << 24, + relocations: 1 << 24, + string_len: 1 << 16, + function_insns: 1 << 20, + function_blocks: 1 << 18, + jump_table_entries: 1 << 16, + } + } +} + +impl Caps { + /// Check a count against a cap, naming both in the error. + pub fn check(&self, what: &'static str, requested: u64, limit: u64) -> Result<()> { + if requested > limit { + Err(Error::CapExceeded { + what, + requested, + limit, + }) + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn messages_locate_the_problem() { + let e = Error::OutOfBounds { + what: "section header 12", + offset: 0x2a40, + len: 0x8000_0000, + available: 0x14c0, + }; + let s = e.to_string(); + assert!(s.contains("0x2a40"), "{s}"); + assert!(s.contains("0x80000000"), "{s}"); + assert!(s.contains("0x14c0"), "{s}"); + } + + #[test] + fn not_recognized_is_distinguishable() { + assert!(Error::NotRecognized { expected: "ELF" }.is_not_recognized()); + assert!(!Error::unsupported("PE32+ on ia64").is_not_recognized()); + } + + #[test] + fn cap_errors_name_the_limit() { + let caps = Caps::default(); + let e = caps.check("sections", 1 << 20, caps.sections).unwrap_err(); + assert!(e.to_string().contains("65536"), "{e}"); + assert!(caps.check("sections", 10, caps.sections).is_ok()); + } +} diff --git a/crates/e5r-core/src/lib.rs b/crates/e5r-core/src/lib.rs new file mode 100644 index 0000000..d5d0b7c --- /dev/null +++ b/crates/e5r-core/src/lib.rs @@ -0,0 +1,25 @@ +//! Addresses, memory maps, provenance, and the hostile-input reader. +//! +//! The bottom of the workspace: depends on nothing, everything depends on it. +//! +//! - [`Reader`] is the only way a loader gets bytes out of a file, and [`Caps`] +//! bounds anything the file gets to count. +//! - [`Addr`] has no `Add`: real files contain headers that wrap. +//! - [`Provenance`] travels with every recovered fact. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod addr; +pub mod arch; +pub mod error; +pub mod memory; +pub mod provenance; +pub mod reader; + +pub use addr::{Addr, AddrRange}; +pub use arch::{Arch, Bits, Endian}; +pub use error::{Caps, Error, Result}; +pub use memory::{MemoryMap, Perms, Segment}; +pub use provenance::{Evidence, Provenance, Strength}; +pub use reader::Reader; diff --git a/crates/e5r-core/src/memory.rs b/crates/e5r-core/src/memory.rs new file mode 100644 index 0000000..5932a80 --- /dev/null +++ b/crates/e5r-core/src/memory.rs @@ -0,0 +1,432 @@ +//! What bytes live at what addresses. A loader's job ends here; everything +//! above reads through this and never learns the container format. +//! +//! Two things it has to get right: `.bss` is mapped with no file bytes and must +//! read as zeros rather than as a neighbour's data, and segments overlap on +//! hostile input, resolved last-inserted-wins so the result is deterministic. + +use serde::{Deserialize, Serialize}; + +use crate::addr::{Addr, AddrRange}; +use crate::error::{Error, Result}; + +/// Access permissions on a mapped range. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +pub struct Perms { + /// The program may read these bytes. + pub read: bool, + /// The program may write these bytes. + pub write: bool, + /// The program may execute these bytes. + pub exec: bool, +} + +impl Perms { + /// Read only. + pub const R: Perms = Perms { + read: true, + write: false, + exec: false, + }; + /// Read and write. + pub const RW: Perms = Perms { + read: true, + write: true, + exec: false, + }; + /// Read and execute. + pub const RX: Perms = Perms { + read: true, + write: false, + exec: true, + }; + /// All three. + pub const RWX: Perms = Perms { + read: true, + write: true, + exec: true, + }; + + /// Build a permission set from three flags. + pub fn new(read: bool, write: bool, exec: bool) -> Perms { + Perms { read, write, exec } + } + + /// The `rwx` string. + pub fn as_str(self) -> &'static str { + match (self.read, self.write, self.exec) { + (false, false, false) => "---", + (true, false, false) => "r--", + (false, true, false) => "-w-", + (false, false, true) => "--x", + (true, true, false) => "rw-", + (true, false, true) => "r-x", + (false, true, true) => "-wx", + (true, true, true) => "rwx", + } + } +} + +impl std::fmt::Display for Perms { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// One mapped range of the address space. +#[derive(Debug, Clone)] +pub struct Segment { + /// Where it lives in memory. + pub range: AddrRange, + /// What the program may do with it. + pub perms: Perms, + /// Container's name for it, empty when it has none (ELF program headers). + pub name: String, + /// Offset in the file the bytes came from, for reporting and for patching. + pub file_offset: u64, + /// File bytes. Shorter than `range.len()` when the tail is zero-filled. + data: Vec, +} + +impl Segment { + /// Overwrite bytes in the segment. Returns false when the write would + /// leave the part that is backed by the file. + pub fn patch(&mut self, at: Addr, bytes: &[u8]) -> bool { + let Some(offset) = at.get().checked_sub(self.range.start().get()) else { + return false; + }; + let Ok(offset) = usize::try_from(offset) else { + return false; + }; + let Some(slot) = self.data.get_mut(offset..offset + bytes.len()) else { + return false; + }; + slot.copy_from_slice(bytes); + true + } + + /// A segment backed by file bytes. + pub fn new( + range: AddrRange, + perms: Perms, + name: impl Into, + file_offset: u64, + data: Vec, + ) -> Result { + if data.len() as u64 > range.len() { + return Err(Error::inconsistent(format!( + "segment {name} at {range} holds {:#x} file bytes but spans only {:#x}", + data.len(), + range.len(), + name = name.into() + ))); + } + Ok(Segment { + range, + perms, + name: name.into(), + file_offset, + data, + }) + } + + /// All zeros, no file bytes. `.bss`. + pub fn zeroed(range: AddrRange, perms: Perms, name: impl Into) -> Segment { + Segment { + range, + perms, + name: name.into(), + file_offset: 0, + data: Vec::new(), + } + } + + /// Bytes backed by the file; the rest read as zero. + pub fn file_len(&self) -> u64 { + self.data.len() as u64 + } + + /// True when part of this segment is zero-filled rather than file-backed. + pub fn has_zero_fill(&self) -> bool { + self.file_len() < self.range.len() + } + + /// Read `len` bytes, zero-filling the tail. `None` if not fully inside. + pub fn read(&self, addr: Addr, len: u64) -> Option> { + let want = AddrRange::sized(addr, len)?; + if !self.range.contains_range(want) { + return None; + } + let off = self.range.start().distance_to(addr)? as usize; + let len = len as usize; + let mut out = vec![0u8; len]; + if off < self.data.len() { + let take = len.min(self.data.len() - off); + out[..take].copy_from_slice(&self.data[off..off + take]); + } + Some(out) + } + + /// Borrow file-backed bytes. `None` if any would come from the zero tail. + pub fn slice(&self, addr: Addr, len: u64) -> Option<&[u8]> { + let off = self.range.start().distance_to(addr)?; + let end = off.checked_add(len)?; + if end > self.data.len() as u64 { + return None; + } + Some(&self.data[off as usize..end as usize]) + } + + /// All file-backed bytes from `addr` to the end of the segment. + pub fn slice_to_end(&self, addr: Addr) -> Option<&[u8]> { + let off = self.range.start().distance_to(addr)?; + if off >= self.data.len() as u64 { + return None; + } + Some(&self.data[off as usize..]) + } +} + +/// Every mapped range in one program, searchable by address. +#[derive(Debug, Clone, Default)] +pub struct MemoryMap { + /// Sorted by start address. Kept sorted by [`MemoryMap::add`] so lookup can + /// binary search. + segments: Vec, +} + +impl MemoryMap { + /// An empty map. + pub fn new() -> MemoryMap { + MemoryMap::default() + } + + /// Insert, keeping the list sorted. Overlaps are allowed: hostile files + /// have them and refusing would refuse the interesting binaries. + pub fn add(&mut self, seg: Segment) { + let at = self + .segments + .partition_point(|s| s.range.start() <= seg.range.start()); + self.segments.insert(at, seg); + } + + /// Every segment, sorted by start address. + /// Overwrite bytes wherever they are mapped, for a relocation the loader + /// has to apply itself. + pub fn patch(&mut self, at: Addr, bytes: &[u8]) -> bool { + self.segments + .iter_mut() + .find(|s| s.range.contains(at)) + .is_some_and(|s| s.patch(at, bytes)) + } + + /// The segments, in address order. + pub fn segments(&self) -> &[Segment] { + &self.segments + } + + /// True when nothing is mapped. + pub fn is_empty(&self) -> bool { + self.segments.is_empty() + } + + /// Lowest to highest mapped address. + pub fn bounds(&self) -> Option { + let lo = self.segments.first()?.range.start(); + let hi = self + .segments + .iter() + .map(|s| s.range.end()) + .max() + .unwrap_or(lo); + AddrRange::new(lo, hi) + } + + /// The segment containing `addr`, or `None`. + pub fn segment_at(&self, addr: Addr) -> Option<&Segment> { + // Candidates all start at or before addr; scan back from the partition + // point. Overlaps are rare, so this stays effectively O(log n). + let end = self.segments.partition_point(|s| s.range.start() <= addr); + self.segments[..end] + .iter() + .rev() + .find(|s| s.range.contains(addr)) + } + + /// True when the address is mapped at all. + pub fn is_mapped(&self, addr: Addr) -> bool { + self.segment_at(addr).is_some() + } + + /// True when the address is mapped and executable. + pub fn is_executable(&self, addr: Addr) -> bool { + self.segment_at(addr).is_some_and(|s| s.perms.exec) + } + + /// True when the address is mapped and writable. + pub fn is_writable(&self, addr: Addr) -> bool { + self.segment_at(addr).is_some_and(|s| s.perms.write) + } + + /// Read `len` bytes, zero-filling unbacked parts. + pub fn read(&self, addr: Addr, len: u64) -> Result> { + self.segment_at(addr) + .and_then(|s| s.read(addr, len)) + .ok_or(Error::Unmapped { addr }) + } + + /// Borrow file-backed bytes without copying, for a decoder's hot path. + pub fn slice(&self, addr: Addr, len: u64) -> Option<&[u8]> { + self.segment_at(addr)?.slice(addr, len) + } + + /// Up to `max` bytes, however many are actually there. A decoder wants a + /// truncated instruction at a section end, not an error. + pub fn decode_window(&self, addr: Addr, max: u64) -> Option<&[u8]> { + let s = self.segment_at(addr)?; + let all = s.slice_to_end(addr)?; + Some(&all[..all.len().min(max as usize)]) + } + + /// Read a pointer-sized value. + pub fn read_ptr(&self, addr: Addr, bytes: u64, little_endian: bool) -> Result { + let b = self.read(addr, bytes)?; + let mut v: u64 = 0; + if little_endian { + for (i, byte) in b.iter().enumerate() { + v |= (*byte as u64) << (8 * i); + } + } else { + for byte in b.iter() { + v = (v << 8) | *byte as u64; + } + } + Ok(v) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn seg(start: u64, len: u64, data: &[u8], perms: Perms, name: &str) -> Segment { + Segment::new( + AddrRange::sized(Addr(start), len).unwrap(), + perms, + name, + 0, + data.to_vec(), + ) + .unwrap() + } + + #[test] + fn bss_reads_as_zero_not_as_the_next_segment() { + // The bug this prevents: .data and .bss are adjacent in memory and + // their file bytes are not, so a naive implementation returns .data's + // neighbour bytes for a .bss read. + let mut m = MemoryMap::new(); + m.add(seg(0x1000, 0x10, &[0xaa; 0x10], Perms::RW, ".data")); + m.add(Segment::zeroed( + AddrRange::sized(Addr(0x1010), 0x20).unwrap(), + Perms::RW, + ".bss", + )); + assert_eq!(m.read(Addr(0x1010), 8).unwrap(), vec![0u8; 8]); + assert_eq!(m.read(Addr(0x1000), 4).unwrap(), vec![0xaa; 4]); + } + + #[test] + fn a_segment_that_is_part_file_part_zero_splits_correctly() { + let mut m = MemoryMap::new(); + // Memory size 0x10, file size 4: the classic ELF PT_LOAD with .bss. + m.add(seg(0x2000, 0x10, &[1, 2, 3, 4], Perms::RW, "load")); + assert_eq!( + m.read(Addr(0x2000), 8).unwrap(), + vec![1, 2, 3, 4, 0, 0, 0, 0] + ); + // Borrowing refuses to reach into the zero tail, so a decoder cannot + // silently decode zeros as instructions. + assert!(m.slice(Addr(0x2000), 8).is_none()); + assert_eq!(m.slice(Addr(0x2000), 4).unwrap(), &[1, 2, 3, 4]); + } + + #[test] + fn segment_declaring_more_file_bytes_than_it_spans_is_rejected() { + let r = Segment::new( + AddrRange::sized(Addr(0x1000), 4).unwrap(), + Perms::R, + "liar", + 0, + vec![0; 64], + ); + assert!(r.is_err()); + } + + #[test] + fn lookup_finds_the_right_segment_among_many() { + let mut m = MemoryMap::new(); + for i in 0..64u64 { + m.add(seg( + 0x1000 + i * 0x100, + 0x80, + &[i as u8; 0x80], + Perms::R, + "s", + )); + } + assert_eq!(m.read(Addr(0x1000 + 40 * 0x100), 1).unwrap(), vec![40]); + // The 0x80 gap between each pair is genuinely unmapped. + assert!(!m.is_mapped(Addr(0x1000 + 40 * 0x100 + 0x90))); + } + + #[test] + fn unmapped_reads_are_errors_not_zeros() { + let m = MemoryMap::new(); + assert!(matches!( + m.read(Addr(0x1000), 1), + Err(Error::Unmapped { .. }) + )); + } + + #[test] + fn decode_window_truncates_at_the_end_of_a_section() { + let mut m = MemoryMap::new(); + m.add(seg( + 0x1000, + 4, + &[0x90, 0x90, 0x90, 0x90], + Perms::RX, + ".text", + )); + // Asking for 15 bytes at the last byte yields the one byte there is, + // which is what a decoder needs to report a truncated instruction. + assert_eq!(m.decode_window(Addr(0x1003), 15).unwrap().len(), 1); + assert_eq!(m.decode_window(Addr(0x1000), 15).unwrap().len(), 4); + } + + #[test] + fn permissions_are_queryable_by_address() { + let mut m = MemoryMap::new(); + m.add(seg(0x1000, 0x10, &[0; 0x10], Perms::RX, ".text")); + m.add(seg(0x2000, 0x10, &[0; 0x10], Perms::RW, ".data")); + assert!(m.is_executable(Addr(0x1004))); + assert!(!m.is_executable(Addr(0x2004))); + assert!(m.is_writable(Addr(0x2004))); + assert!(!m.is_writable(Addr(0x1004))); + } + + #[test] + fn read_ptr_respects_byte_order() { + let mut m = MemoryMap::new(); + m.add(seg( + 0x1000, + 8, + &[0x78, 0x56, 0x34, 0x12, 0, 0, 0, 0], + Perms::R, + "d", + )); + assert_eq!(m.read_ptr(Addr(0x1000), 4, true).unwrap(), 0x1234_5678); + assert_eq!(m.read_ptr(Addr(0x1000), 4, false).unwrap(), 0x7856_3412); + } +} diff --git a/crates/e5r-core/src/provenance.rs b/crates/e5r-core/src/provenance.rs new file mode 100644 index 0000000..bead1b6 --- /dev/null +++ b/crates/e5r-core/src/provenance.rs @@ -0,0 +1,326 @@ +//! Where a fact came from (`ROADMAP.md` bet 4). +//! +//! A boundary from an `.eh_frame` FDE and one from a prologue pattern are not +//! the same claim. Every recovered fact carries one of these so the CLI can +//! answer "why do you think that?" without re-running analysis. + +use std::cmp::Ordering; +use std::fmt; + +use serde::{Deserialize, Serialize}; + +/// How strongly a fact is known. Ordered weakest to strongest. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Strength { + /// A pattern guess: prologue match, alignment, a plausible-looking string. + Heuristic, + /// Derived from something proven. A direct call's target is inferred: the + /// decode is proven, "therefore a function" is a step past it. + Inferred, + /// Stated by the file in a structure the format defines. + Proven, + /// Written in the annotation log. Outranks anything the engine derives. + Asserted, +} + +impl Strength { + /// True when the engine may overwrite a fact of this strength with one of + /// its own. It may never overwrite an assertion. + pub fn is_overridable(self) -> bool { + self != Strength::Asserted + } + + /// The lowercase word used in output and in JSON. + pub fn as_str(self) -> &'static str { + match self { + Strength::Heuristic => "heuristic", + Strength::Inferred => "inferred", + Strength::Proven => "proven", + Strength::Asserted => "asserted", + } + } +} + +impl fmt::Display for Strength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// The specific evidence behind a fact: the "why" the CLI prints. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Evidence { + /// An entry in a symbol table. + SymbolTable, + /// Debug information: what the compiler recorded about its own output. + DebugInfo, + /// A symbol with no declared type, sitting in executable memory. + CodeSymbol, + /// An entry in a dynamic symbol table. + DynamicSymbol, + /// The container's declared entry point. + EntryPoint, + /// An initializer or finalizer array. + InitArray, + /// A `.eh_frame` frame description entry. + EhFrame, + /// A PE `RUNTIME_FUNCTION` unwind record. + PeUnwind, + /// A table the loader validates indirect calls against: Control Flow + /// Guard's function table, or SafeSEH's handler table. The linker built it + /// from real function entries, and the loader enforces it at run time, so + /// an address in it is a function whatever else the image says. + GuardTable, + /// A Mach-O `LC_FUNCTION_STARTS` entry. + MachFunctionStarts, + /// Go's `pclntab`. + GoPclntab, + /// Objective-C runtime metadata: the class and method lists a binary + /// carries so the runtime can dispatch. + ObjcMetadata, + /// Swift's own metadata: type and protocol descriptors. + SwiftMetadata, + /// A Rust panic site, whose string carries the file and line it is in. + RustPanic, + /// A Windows program database, the compiler's own record of its output. + Pdb, + /// An export table entry. + Export, + /// An import thunk, PLT or IAT. + ImportThunk, + /// The target of a direct call found by decoding. + CallTarget, + /// The target of a direct branch found by decoding. + BranchTarget, + /// A recovered jump table entry. + JumpTable, + /// A function prologue byte pattern. + ProloguePattern, + /// Code found by sweeping a gap between known functions. + LinearSweep, + /// A pointer found in a data section that lands in executable memory. + DataPointer, + /// Written in the annotation log by a person or an agent. + Annotation, +} + +impl Evidence { + /// Strength this evidence carries. One place to decide, so two analyses + /// cannot disagree about how good a symbol table is. + pub fn strength(self) -> Strength { + match self { + Evidence::SymbolTable + | Evidence::DebugInfo + | Evidence::DynamicSymbol + | Evidence::EntryPoint + | Evidence::InitArray + | Evidence::EhFrame + | Evidence::PeUnwind + | Evidence::GuardTable + | Evidence::MachFunctionStarts + | Evidence::GoPclntab + | Evidence::Pdb + | Evidence::Export => Strength::Proven, + + // Metadata a runtime reads while the program runs: present because + // something uses it, so what it names is real, but the format is a + // convention rather than a declaration. + Evidence::ObjcMetadata + | Evidence::SwiftMetadata + | Evidence::RustPanic + | Evidence::CodeSymbol + | Evidence::ImportThunk + | Evidence::CallTarget + | Evidence::BranchTarget + | Evidence::JumpTable => Strength::Inferred, + + Evidence::ProloguePattern | Evidence::LinearSweep | Evidence::DataPointer => { + Strength::Heuristic + } + + Evidence::Annotation => Strength::Asserted, + } + } + + /// A short human-readable name. + pub fn as_str(self) -> &'static str { + match self { + Evidence::SymbolTable => "symbol table", + Evidence::DebugInfo => "debug info", + Evidence::CodeSymbol => "code symbol", + Evidence::DynamicSymbol => "dynamic symbol", + Evidence::EntryPoint => "entry point", + Evidence::InitArray => "init array", + Evidence::EhFrame => ".eh_frame FDE", + Evidence::PeUnwind => "PE unwind record", + Evidence::GuardTable => "PE guard table", + Evidence::MachFunctionStarts => "LC_FUNCTION_STARTS", + Evidence::GoPclntab => "Go pclntab", + Evidence::ObjcMetadata => "ObjC metadata", + Evidence::SwiftMetadata => "Swift metadata", + Evidence::RustPanic => "Rust panic site", + Evidence::Pdb => "program database", + Evidence::Export => "export table", + Evidence::ImportThunk => "import thunk", + Evidence::CallTarget => "direct call target", + Evidence::BranchTarget => "direct branch target", + Evidence::JumpTable => "jump table entry", + Evidence::ProloguePattern => "prologue pattern", + Evidence::LinearSweep => "linear sweep", + Evidence::DataPointer => "pointer in data", + Evidence::Annotation => "annotation", + } + } +} + +impl fmt::Display for Evidence { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Best evidence for a fact, plus everything else that agreed. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Provenance { + /// The strongest evidence seen, which sets the strength. + pub best: Evidence, + /// Everything else that pointed at the same fact, strongest first, with no + /// duplicates. + pub corroborating: Vec, +} + +impl Provenance { + /// A provenance with one piece of evidence. + pub fn new(e: Evidence) -> Provenance { + Provenance { + best: e, + corroborating: Vec::new(), + } + } + + /// The strength of the best evidence. + pub fn strength(&self) -> Strength { + self.best.strength() + } + + /// True when a piece of evidence is among what supports this. + pub fn has(&self, e: Evidence) -> bool { + self.best == e || self.corroborating.contains(&e) + } + + /// How many independent sources agree. + pub fn support(&self) -> usize { + 1 + self.corroborating.len() + } + + /// Record another source agreeing. Order-independent: the determinism gate + /// needs the same answer whatever order the work queue found things in. + pub fn add(&mut self, e: Evidence) { + if e == self.best || self.corroborating.contains(&e) { + return; + } + if e.strength() > self.best.strength() { + let old = std::mem::replace(&mut self.best, e); + self.corroborating.push(old); + } else { + self.corroborating.push(e); + } + self.corroborating.sort_by(|a, b| { + b.strength() + .cmp(&a.strength()) + .then_with(|| (*a as u8).cmp(&(*b as u8))) + }); + } + + /// Merge another provenance into this one. + pub fn merge(&mut self, other: &Provenance) { + self.add(other.best); + for e in &other.corroborating { + self.add(*e); + } + } + + /// Rank two provenances: stronger evidence first, then more corroboration. + pub fn rank(&self, other: &Provenance) -> Ordering { + other + .strength() + .cmp(&self.strength()) + .then_with(|| other.support().cmp(&self.support())) + } +} + +impl fmt::Display for Provenance { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} ({})", self.strength(), self.best)?; + for e in &self.corroborating { + write!(f, ", {e}")?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn assertions_outrank_everything_the_engine_knows() { + assert!(Strength::Asserted > Strength::Proven); + assert!(Strength::Proven > Strength::Inferred); + assert!(Strength::Inferred > Strength::Heuristic); + assert!(!Strength::Asserted.is_overridable()); + assert!(Strength::Proven.is_overridable()); + } + + #[test] + fn adding_stronger_evidence_promotes_it() { + let mut p = Provenance::new(Evidence::ProloguePattern); + assert_eq!(p.strength(), Strength::Heuristic); + p.add(Evidence::SymbolTable); + assert_eq!(p.best, Evidence::SymbolTable); + assert_eq!(p.strength(), Strength::Proven); + assert_eq!(p.corroborating, vec![Evidence::ProloguePattern]); + } + + #[test] + fn provenance_does_not_depend_on_arrival_order() { + // The determinism gate: analysis discovers evidence in whatever order + // the work queue hands it over, and the answer must not notice. + let order_a = [ + Evidence::ProloguePattern, + Evidence::EhFrame, + Evidence::CallTarget, + ]; + let order_b = [ + Evidence::CallTarget, + Evidence::ProloguePattern, + Evidence::EhFrame, + ]; + let build = |evs: &[Evidence]| { + let mut p = Provenance::new(evs[0]); + for e in &evs[1..] { + p.add(*e); + } + p + }; + assert_eq!(build(&order_a), build(&order_b)); + } + + #[test] + fn repeats_do_not_inflate_support() { + let mut p = Provenance::new(Evidence::SymbolTable); + p.add(Evidence::SymbolTable); + p.add(Evidence::EhFrame); + p.add(Evidence::EhFrame); + assert_eq!(p.support(), 2); + } + + #[test] + fn display_says_why() { + let mut p = Provenance::new(Evidence::EhFrame); + p.add(Evidence::ProloguePattern); + assert_eq!(p.to_string(), "proven (.eh_frame FDE), prologue pattern"); + } +} diff --git a/crates/e5r-core/src/reader.rs b/crates/e5r-core/src/reader.rs new file mode 100644 index 0000000..75d76cc --- /dev/null +++ b/crates/e5r-core/src/reader.rs @@ -0,0 +1,360 @@ +//! Bounds-checked cursor. Loaders read through this and never index a slice, +//! which is the whole no-panic story for parsing. +//! +//! Every read names what it is reading; the name travels into the error. +//! Endianness lives on the reader because a container decides it once. + +use crate::arch::Endian; +use crate::error::{Error, Result}; + +/// A cursor over bytes that cannot read past the end. +#[derive(Clone, Copy)] +pub struct Reader<'a> { + data: &'a [u8], + pos: usize, + endian: Endian, +} + +impl<'a> Reader<'a> { + /// A reader positioned at the start of `data`. + pub fn new(data: &'a [u8], endian: Endian) -> Reader<'a> { + Reader { + data, + pos: 0, + endian, + } + } + + /// A little-endian reader, for the case where the format fixes the order. + pub fn le(data: &'a [u8]) -> Reader<'a> { + Reader::new(data, Endian::Little) + } + + /// The whole buffer, ignoring the cursor. + pub fn data(&self) -> &'a [u8] { + self.data + } + + /// Total bytes available. + pub fn len(&self) -> usize { + self.data.len() + } + + /// True when there are no bytes at all. + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// The cursor position. + pub fn pos(&self) -> u64 { + self.pos as u64 + } + + /// Bytes between the cursor and the end. + pub fn remaining(&self) -> usize { + self.data.len() - self.pos + } + + /// The byte order later reads will use. + pub fn endian(&self) -> Endian { + self.endian + } + + /// Change the byte order for later reads. A container header decides this + /// once, so this is called once per file. + pub fn set_endian(&mut self, endian: Endian) { + self.endian = endian; + } + + /// Move the cursor to an absolute offset. + pub fn seek(&mut self, what: &'static str, offset: u64) -> Result<()> { + let off = usize::try_from(offset).map_err(|_| Error::OutOfBounds { + what, + offset, + len: 0, + available: self.data.len() as u64, + })?; + if off > self.data.len() { + return Err(Error::OutOfBounds { + what, + offset, + len: 0, + available: self.data.len() as u64, + }); + } + self.pos = off; + Ok(()) + } + + /// Advance the cursor without reading. + pub fn skip(&mut self, what: &'static str, n: u64) -> Result<()> { + let at = self.pos as u64; + self.seek( + what, + at.checked_add(n).ok_or(Error::OutOfBounds { + what, + offset: at, + len: n, + available: self.data.len() as u64, + })?, + ) + } + + /// A reader over a sub-range. A parser handed one cannot read outside it. + pub fn slice_at(&self, what: &'static str, offset: u64, len: u64) -> Result> { + Ok(Reader { + data: self.bytes_at(what, offset, len)?, + pos: 0, + endian: self.endian, + }) + } + + /// Borrow `len` bytes at an absolute offset without moving the cursor. + pub fn bytes_at(&self, what: &'static str, offset: u64, len: u64) -> Result<&'a [u8]> { + let available = self.data.len() as u64; + let end = offset.checked_add(len).ok_or(Error::OutOfBounds { + what, + offset, + len, + available, + })?; + if end > available { + return Err(Error::OutOfBounds { + what, + offset, + len, + available, + }); + } + // Both casts are safe: end <= data.len(), which is a usize. + Ok(&self.data[offset as usize..end as usize]) + } + + /// Read `len` bytes at the cursor and advance. + pub fn bytes(&mut self, what: &'static str, len: u64) -> Result<&'a [u8]> { + let out = self.bytes_at(what, self.pos as u64, len)?; + self.pos += out.len(); + Ok(out) + } + + /// Read a fixed-size array at the cursor and advance. + pub fn array(&mut self, what: &'static str) -> Result<[u8; N]> { + let b = self.bytes(what, N as u64)?; + let mut out = [0u8; N]; + out.copy_from_slice(b); + Ok(out) + } + + /// Read one byte. + pub fn u8(&mut self, what: &'static str) -> Result { + Ok(self.array::<1>(what)?[0]) + } + + /// Read one byte as signed. + pub fn i8(&mut self, what: &'static str) -> Result { + Ok(self.u8(what)? as i8) + } + + /// Read a 16-bit unsigned value in the reader's byte order. + pub fn u16(&mut self, what: &'static str) -> Result { + let b = self.array::<2>(what)?; + Ok(match self.endian { + Endian::Little => u16::from_le_bytes(b), + Endian::Big => u16::from_be_bytes(b), + }) + } + + /// Read a 32-bit unsigned value in the reader's byte order. + pub fn u32(&mut self, what: &'static str) -> Result { + let b = self.array::<4>(what)?; + Ok(match self.endian { + Endian::Little => u32::from_le_bytes(b), + Endian::Big => u32::from_be_bytes(b), + }) + } + + /// Read a 64-bit unsigned value in the reader's byte order. + pub fn u64(&mut self, what: &'static str) -> Result { + let b = self.array::<8>(what)?; + Ok(match self.endian { + Endian::Little => u64::from_le_bytes(b), + Endian::Big => u64::from_be_bytes(b), + }) + } + + /// Read a 16-bit signed value in the reader's byte order. + pub fn i16(&mut self, what: &'static str) -> Result { + Ok(self.u16(what)? as i16) + } + + /// Read a 32-bit signed value in the reader's byte order. + pub fn i32(&mut self, what: &'static str) -> Result { + Ok(self.u32(what)? as i32) + } + + /// Read a 64-bit signed value in the reader's byte order. + pub fn i64(&mut self, what: &'static str) -> Result { + Ok(self.u64(what)? as i64) + } + + /// Read 4 or 8 bytes, widened to `u64`. ELF and Mach-O differ only here, + /// so one method keeps their parsers from being two copies. + pub fn uword(&mut self, what: &'static str, wide: bool) -> Result { + if wide { + self.u64(what) + } else { + Ok(self.u32(what)? as u64) + } + } + + /// A NUL-terminated string at an absolute offset, terminator excluded. + /// Unterminated is an error, not a silent truncation. + pub fn cstr_at(&self, what: &'static str, offset: u64, max: u64) -> Result<&'a [u8]> { + let available = self.data.len() as u64; + if offset >= available { + return Err(Error::OutOfBounds { + what, + offset, + len: 1, + available, + }); + } + let start = offset as usize; + // Saturating: a caller asking for an unbounded string passes a huge + // maximum, and wrapping would make the limit smaller than the start. + let limit = (offset.saturating_add(max).min(available)) as usize; + match self.data[start..limit].iter().position(|&b| b == 0) { + Some(n) => Ok(&self.data[start..start + n]), + None => Err(Error::BadField { + field: what, + value: offset, + reason: "starts a string with no NUL terminator before the end of the table", + }), + } + } + + /// An unsigned LEB128 value at the cursor, as DWARF and Mach-O use. + pub fn uleb128(&mut self, what: &'static str) -> Result { + let mut out: u64 = 0; + let mut shift = 0u32; + loop { + let b = self.u8(what)?; + if shift < 64 { + out |= ((b & 0x7f) as u64) << shift; + } else if b & 0x7f != 0 { + return Err(Error::BadField { + field: what, + value: b as u64, + reason: "continues a LEB128 past 64 bits of significance", + }); + } + shift += 7; + if b & 0x80 == 0 { + return Ok(out); + } + if shift > 70 { + return Err(Error::BadField { + field: what, + value: shift as u64, + reason: "makes a LEB128 longer than any 64-bit value needs", + }); + } + } + } + + /// A signed LEB128 value at the cursor. + pub fn sleb128(&mut self, what: &'static str) -> Result { + let mut out: i64 = 0; + let mut shift = 0u32; + loop { + let b = self.u8(what)?; + if shift < 64 { + out |= ((b & 0x7f) as i64) << shift; + } + shift += 7; + if b & 0x80 == 0 { + if shift < 64 && b & 0x40 != 0 { + out |= -1i64 << shift; + } + return Ok(out); + } + if shift > 70 { + return Err(Error::BadField { + field: what, + value: shift as u64, + reason: "makes a LEB128 longer than any 64-bit value needs", + }); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn truncated_reads_error_rather_than_panic() { + let mut r = Reader::le(&[1, 2, 3]); + assert!(r.u32("header").is_err()); + // The cursor did not move, so a caller that recovers is not now + // misaligned. + assert_eq!(r.pos(), 0); + assert_eq!(r.u16("half").unwrap(), 0x0201); + } + + #[test] + fn offset_plus_len_cannot_wrap() { + let r = Reader::le(&[0u8; 16]); + let e = r.bytes_at("section", u64::MAX - 4, 16).unwrap_err(); + assert!(matches!(e, Error::OutOfBounds { .. })); + } + + #[test] + fn endianness_is_a_property_of_the_reader() { + let bytes = [0x12, 0x34]; + assert_eq!( + Reader::new(&bytes, Endian::Little).u16("x").unwrap(), + 0x3412 + ); + assert_eq!(Reader::new(&bytes, Endian::Big).u16("x").unwrap(), 0x1234); + } + + #[test] + fn slices_cannot_see_outside_themselves() { + let r = Reader::le(&[0, 1, 2, 3, 4, 5, 6, 7]); + let mut s = r.slice_at("body", 2, 3).unwrap(); + assert_eq!(s.len(), 3); + assert_eq!(s.bytes("all", 3).unwrap(), &[2, 3, 4]); + assert!(s.u8("past the end").is_err()); + } + + #[test] + fn unterminated_strings_are_an_error() { + let r = Reader::le(b"name\0tail"); + assert_eq!(r.cstr_at("strtab", 0, 64).unwrap(), b"name"); + assert!(r.cstr_at("strtab", 5, 64).is_err()); + let ok = Reader::le(b"abc\0"); + assert_eq!(ok.cstr_at("strtab", 0, 64).unwrap(), b"abc"); + } + + #[test] + fn leb128_round_trips_the_dwarf_examples() { + // Values from the DWARF 5 specification, appendix C. + assert_eq!(Reader::le(&[0x7f]).uleb128("v").unwrap(), 127); + assert_eq!(Reader::le(&[0x80, 0x01]).uleb128("v").unwrap(), 128); + assert_eq!( + Reader::le(&[0xe5, 0x8e, 0x26]).uleb128("v").unwrap(), + 624485 + ); + assert_eq!(Reader::le(&[0x7f]).sleb128("v").unwrap(), -1); + assert_eq!(Reader::le(&[0x80, 0x7f]).sleb128("v").unwrap(), -128); + assert_eq!(Reader::le(&[0x3f]).sleb128("v").unwrap(), 63); + } + + #[test] + fn leb128_refuses_to_run_forever() { + let forever = [0x80u8; 32]; + assert!(Reader::le(&forever).uleb128("v").is_err()); + } +} diff --git a/crates/e5r-db/Cargo.toml b/crates/e5r-db/Cargo.toml new file mode 100644 index 0000000..2602696 --- /dev/null +++ b/crates/e5r-db/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "e5r-db" +description = "e5r — content anchors and the git-mergeable annotation log." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +e5r-core = { workspace = true } +e5r-arch = { workspace = true } +e5r-types = { workspace = true } +serde = { workspace = true } + +# The signature gate measures against a real statically linked binary and +# builds its library out of the distribution's own static archives, so it needs +# a loader and an analysis. Neither of those depends on this crate, so the edge +# only exists for the tests. +[dev-dependencies] +e5r-format = { workspace = true } +e5r-analysis = { workspace = true } diff --git a/crates/e5r-db/src/anchor.rs b/crates/e5r-db/src/anchor.rs new file mode 100644 index 0000000..f9bdfeb --- /dev/null +++ b/crates/e5r-db/src/anchor.rs @@ -0,0 +1,520 @@ +//! Content anchors: naming a location so the name survives a rebuild. +//! +//! Keying an annotation to an absolute address means a rebase, a relink, or a +//! recompile that shifts everything by a few bytes throws away the work. An +//! anchor is layered instead, most durable first: +//! +//! - `shape` fingerprints the instruction stream as `(length, flow class)` +//! pairs. Branch targets are deliberately excluded, which is what makes it +//! invariant to where the code is loaded. +//! - `bytes` fingerprints the raw body. It captures embedded addresses, so it +//! matches only the identical binary, and is the fast exact path. +//! - `abs` is the entry address, a last-resort tiebreak. +//! +//! Resolution reports which layer matched, so a confident hit is +//! distinguishable from a best-effort one. + +use std::fmt; + +use e5r_arch::{Extend, Flow, Insn, Mem, Operand, Reg, Shift}; +use e5r_core::Addr; + +/// FNV-1a 64-bit, chosen for being byte-order fixed and reproducible across +/// platforms. Anchoring needs a stable identity, not collision resistance. +#[derive(Clone, Copy)] +pub struct Fnv(u64); + +impl Default for Fnv { + fn default() -> Self { + Fnv::new() + } +} + +impl Fnv { + const OFFSET: u64 = 0xcbf2_9ce4_8422_2325; + const PRIME: u64 = 0x0000_0100_0000_01b3; + + /// A fresh hash. + pub fn new() -> Fnv { + Fnv(Fnv::OFFSET) + } + + /// Fold one byte. + pub fn byte(&mut self, b: u8) { + self.0 ^= b as u64; + self.0 = self.0.wrapping_mul(Fnv::PRIME); + } + + /// Fold a slice. + pub fn slice(&mut self, s: &[u8]) { + for &b in s { + self.byte(b); + } + } + + /// Fold a 64-bit value little-endian, so the result is byte-order fixed. + pub fn u64(&mut self, v: u64) { + self.slice(&v.to_le_bytes()); + } + + /// The digest. + pub fn finish(self) -> u64 { + self.0 + } +} + +/// How an instruction transfers control, coarse enough to survive a rebuild. +fn flow_class(f: Flow) -> u8 { + match f { + Flow::Next => 0, + Flow::Branch(_) => 1, + Flow::CondBranch(_) => 2, + Flow::IndirectBranch => 3, + Flow::Call(_) => 4, + Flow::IndirectCall => 5, + Flow::Return => 6, + Flow::Trap => 7, + Flow::Syscall => 8, + } +} + +/// Fingerprint one instruction by everything about it a relink cannot change. +/// +/// [`Anchor::function`] folds `(length, flow class)` per instruction. That is +/// enough to identify a whole stream, and cheap enough to compute for every +/// function in a binary, but it is far too coarse to align two streams +/// *against* each other: at that granularity a third of a function's +/// instructions share a token, and an alignment anchored on a token dozens of +/// instructions carry is anchored on nothing. +/// +/// So this folds the mnemonic and the operands too, and leaves out only what +/// moves when an instruction is inserted ahead of it: the absolute address a +/// branch or a pc-relative computation resolves to, and the displacement of a +/// pc-relative memory operand. Two instructions with the same token are the +/// same instruction wherever the linker put them; whether their targets agree +/// is the separate question the caller asks next. +pub fn shape_token(i: &Insn) -> u64 { + let mut h = Fnv::new(); + h.slice(i.mnemonic.as_bytes()); + h.byte(0); + h.slice(i.prefix.unwrap_or("").as_bytes()); + h.byte(0); + h.byte(i.len); + h.byte(flow_class(i.flow)); + h.byte(i.operands().len() as u8); + for op in i.operands() { + fold_operand(&mut h, op); + } + h.finish() +} + +fn fold_reg(h: &mut Fnv, r: &Reg) { + h.byte(r.class as u8); + h.byte(r.num); + h.byte(r.width as u8); +} + +fn fold_shift(h: &mut Fnv, s: Shift, amount: u8) { + h.byte(s as u8); + h.byte(amount); +} + +fn fold_extend(h: &mut Fnv, e: Extend, amount: u8) { + h.byte(e as u8); + h.byte(amount); +} + +fn fold_mem(h: &mut Fnv, m: &Mem) { + match m.seg { + Some(r) => { + h.byte(1); + fold_reg(h, &r); + } + None => h.byte(0), + } + match m.base { + Some(r) => { + h.byte(1); + fold_reg(h, &r); + } + None => h.byte(0), + } + match m.index { + Some((r, e, s)) => { + h.byte(1); + fold_reg(h, &r); + fold_extend(h, e, s); + } + None => h.byte(0), + } + h.byte(m.mode as u8); + h.u64(m.size); + // A pc-relative displacement is an address wearing a disguise: it changes + // when anything ahead of the instruction moves, so it is left out for the + // same reason a branch target is. + if m.is_pc_relative() { + h.byte(0xff); + } else { + h.u64(m.disp as u64); + } +} + +fn fold_operand(h: &mut Fnv, op: &Operand) { + match op { + Operand::Reg(r) => { + h.byte(0); + fold_reg(h, r); + } + Operand::Shifted(r, s, a) => { + h.byte(1); + fold_reg(h, r); + fold_shift(h, *s, *a); + } + Operand::Extended(r, e, a) => { + h.byte(2); + fold_reg(h, r); + fold_extend(h, *e, *a); + } + Operand::Imm(v) => { + h.byte(3); + h.u64(*v as u64); + } + Operand::UImm(v) => { + h.byte(4); + h.u64(*v); + } + Operand::Count(v) => { + h.byte(5); + h.u64(*v as u64); + } + Operand::ShiftOp(s, a) => { + h.byte(6); + fold_shift(h, *s, *a); + } + Operand::Name(n) => { + h.byte(7); + h.slice(n.as_bytes()); + } + Operand::FpImm(v) => { + h.byte(8); + h.u64(*v); + } + Operand::Vector(n, l) => { + h.byte(9); + h.byte(*n); + h.slice(l.as_str().as_bytes()); + } + Operand::VectorLane(n, w, i) => { + h.byte(10); + h.byte(*n); + h.byte(*w as u8); + h.byte(*i); + } + Operand::VectorList(n, c, l) => { + h.byte(11); + h.byte(*n); + h.byte(*c); + h.slice(l.as_str().as_bytes()); + } + // Excluded on purpose: this is the address the instruction names, and + // an insertion earlier in the function is exactly what changes it. + Operand::Addr(_) => h.byte(12), + Operand::Mem(m) => { + h.byte(13); + fold_mem(h, m); + } + Operand::Cond(c) => { + h.byte(14); + h.byte(c.0); + } + Operand::Sys(v) => { + h.byte(15); + h.u64(*v as u64); + } + } +} + +/// Every absolute address one instruction names, in a fixed order. +/// +/// The order is a function of the operand kinds and the flow class alone, both +/// of which [`shape_token`] folds in, so two instructions with the same token +/// produce lists of the same length and a caller can compare them position by +/// position. +pub fn addresses(i: &Insn) -> Vec { + let mut out = Vec::new(); + for op in i.operands() { + match op { + Operand::Addr(a) => out.push(*a), + Operand::Mem(m) => { + if let Some(a) = m.pc_target(i.end()) { + out.push(a); + } + } + _ => {} + } + } + out.extend(i.flow.target()); + out +} + +/// A stable identity for one location. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Anchor { + /// Rebase-invariant fingerprint of the instruction stream's shape. + pub shape: u64, + /// Exact fingerprint of the raw bytes, for the same-binary fast path. + pub bytes: u64, + /// How many instructions the shape covers. + pub insns: u32, + /// Entry address, a hint rather than an identity. + pub abs: Addr, + /// Byte offset from the anchored entry, so a comment can sit on one + /// instruction rather than on the whole function. + pub offset: u32, +} + +impl Anchor { + /// Anchor a function from its instructions, in address order, and its bytes. + pub fn function(entry: Addr, insns: &[Insn], body: &[u8]) -> Anchor { + let mut shape = Fnv::new(); + for i in insns { + shape.byte(i.len); + shape.byte(flow_class(i.flow)); + } + let mut bytes = Fnv::new(); + bytes.slice(body); + Anchor { + shape: shape.finish(), + bytes: bytes.finish(), + insns: insns.len() as u32, + abs: entry, + offset: 0, + } + } + + /// The same anchor, pointing at a byte offset inside it. + pub fn at_offset(self, offset: u32) -> Anchor { + Anchor { offset, ..self } + } + + /// The identity fields, excluding the address hint. + pub fn identity(&self) -> (u64, u64, u32, u32) { + (self.shape, self.bytes, self.insns, self.offset) + } + + /// The address this anchor points at, entry plus offset. + pub fn target(&self) -> Addr { + self.abs.wrapping_offset(self.offset as i64) + } +} + +impl fmt::Display for Anchor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:016x}/{:016x}+{}", self.shape, self.bytes, self.offset) + } +} + +/// How an anchor was matched when it was resolved. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Resolution { + /// The raw bytes matched: the same function in the same binary. + Exact, + /// The instruction shape matched uniquely: the same function, rebuilt. + Shape, + /// Only the address matched, and nothing corroborates it. + Address, + /// The shape matched more than one candidate; the address chose between + /// them, so the answer is a guess. + Ambiguous, +} + +impl Resolution { + /// The word shown in output. + pub fn as_str(self) -> &'static str { + match self { + Resolution::Exact => "exact", + Resolution::Shape => "shape", + Resolution::Address => "address", + Resolution::Ambiguous => "ambiguous", + } + } + + /// True when the match is strong enough to apply without a warning. + pub fn is_confident(self) -> bool { + matches!(self, Resolution::Exact | Resolution::Shape) + } +} + +impl fmt::Display for Resolution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Anchors for one analyzed binary, searchable by any layer. +#[derive(Debug, Clone, Default)] +pub struct AnchorIndex { + /// Sorted by `bytes`. + by_bytes: Vec<(u64, Anchor)>, + /// Sorted by `shape`. + by_shape: Vec<(u64, Anchor)>, + /// Sorted by `abs`. + by_abs: Vec<(Addr, Anchor)>, +} + +impl AnchorIndex { + /// Build from every anchored location in a program. + pub fn build(anchors: impl IntoIterator) -> AnchorIndex { + let all: Vec = anchors.into_iter().collect(); + let mut by_bytes: Vec<(u64, Anchor)> = all.iter().map(|a| (a.bytes, *a)).collect(); + let mut by_shape: Vec<(u64, Anchor)> = all.iter().map(|a| (a.shape, *a)).collect(); + let mut by_abs: Vec<(Addr, Anchor)> = all.iter().map(|a| (a.abs, *a)).collect(); + by_bytes.sort_unstable(); + by_shape.sort_unstable(); + by_abs.sort_unstable(); + AnchorIndex { + by_bytes, + by_shape, + by_abs, + } + } + + /// How many anchors are indexed. + pub fn len(&self) -> usize { + self.by_bytes.len() + } + + /// True when nothing is indexed. + pub fn is_empty(&self) -> bool { + self.by_bytes.is_empty() + } + + /// Find where an anchor points in this binary, and say how sure that is. + pub fn resolve(&self, want: &Anchor) -> Option<(Anchor, Resolution)> { + // The same bytes in the same binary: unambiguous. + if let Some(a) = lookup(&self.by_bytes, want.bytes).first() { + return Some((**a, Resolution::Exact)); + } + // The same shape, rebuilt. Unique means confident; several means the + // address is doing the choosing and the caller should be told. + let by_shape = lookup(&self.by_shape, want.shape); + match by_shape.len() { + 0 => {} + 1 => return Some((*by_shape[0], Resolution::Shape)), + _ => { + let pick = by_shape + .iter() + .find(|a| a.abs == want.abs) + .copied() + .unwrap_or(by_shape[0]); + return Some((*pick, Resolution::Ambiguous)); + } + } + // Nothing but the address is left. + lookup(&self.by_abs, want.abs) + .first() + .map(|a| (**a, Resolution::Address)) + } +} + +/// Every value with the given key in a sorted list. +fn lookup(v: &[(K, Anchor)], key: K) -> Vec<&Anchor> { + let lo = v.partition_point(|(k, _)| *k < key); + let hi = v.partition_point(|(k, _)| *k <= key); + v[lo..hi].iter().map(|(_, a)| a).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use e5r_arch::aarch64; + + fn insns_of(words: &[u32], base: u64) -> Vec { + words + .iter() + .enumerate() + .filter_map(|(n, w)| aarch64::decode_word(*w, Addr(base + n as u64 * 4))) + .collect() + } + + fn body_of(words: &[u32]) -> Vec { + words.iter().flat_map(|w| w.to_le_bytes()).collect() + } + + #[test] + fn shape_survives_a_relink() { + // The same function at two addresses, with its internal branch + // displacement unchanged. Bytes differ only where the pc-relative + // target lands, and shape ignores targets entirely. + let words = [0xf100_041f, 0x5400_0040, 0xd280_0000, 0xd65f_03c0]; + let a = Anchor::function(Addr(0x1000), &insns_of(&words, 0x1000), &body_of(&words)); + let b = Anchor::function(Addr(0x9000), &insns_of(&words, 0x9000), &body_of(&words)); + assert_eq!(a.shape, b.shape); + assert_ne!(a.abs, b.abs); + } + + #[test] + fn shape_changes_when_the_code_changes() { + let one = [0xd280_0000u32, 0xd65f_03c0]; + let two = [0xd280_0000u32, 0xd280_0020, 0xd65f_03c0]; + let a = Anchor::function(Addr(0x1000), &insns_of(&one, 0x1000), &body_of(&one)); + let b = Anchor::function(Addr(0x1000), &insns_of(&two, 0x1000), &body_of(&two)); + assert_ne!(a.shape, b.shape); + } + + #[test] + fn exact_bytes_win_over_shape() { + let words = [0xd280_0000u32, 0xd65f_03c0]; + let here = Anchor::function(Addr(0x1000), &insns_of(&words, 0x1000), &body_of(&words)); + let elsewhere = Anchor::function(Addr(0x9000), &insns_of(&words, 0x9000), &body_of(&words)); + let ix = AnchorIndex::build([here, elsewhere]); + let (found, how) = ix.resolve(&here).unwrap(); + assert_eq!(how, Resolution::Exact); + assert_eq!(found.abs, Addr(0x1000)); + } + + #[test] + fn a_shared_shape_is_reported_as_ambiguous() { + // Two identical functions at different addresses with different bytes: + // shape alone cannot choose, so the answer says so. + let mut a = Anchor::function(Addr(0x1000), &[], b"aaaa"); + let mut b = Anchor::function(Addr(0x2000), &[], b"bbbb"); + a.shape = 0x1234; + b.shape = 0x1234; + let ix = AnchorIndex::build([a, b]); + let want = Anchor { bytes: 0xdead, ..a }; + let (_, how) = ix.resolve(&want).unwrap(); + assert_eq!(how, Resolution::Ambiguous); + assert!(!how.is_confident()); + } + + #[test] + fn the_address_is_the_last_resort() { + let a = Anchor::function(Addr(0x1000), &[], b"aaaa"); + let ix = AnchorIndex::build([a]); + let want = Anchor { + shape: 0xdead, + bytes: 0xbeef, + ..a + }; + let (found, how) = ix.resolve(&want).unwrap(); + assert_eq!(how, Resolution::Address); + assert_eq!(found.abs, Addr(0x1000)); + assert!(!how.is_confident()); + } + + #[test] + fn nothing_matches_nothing() { + let ix = AnchorIndex::build([]); + assert!(ix.resolve(&Anchor::function(Addr(1), &[], b"x")).is_none()); + } + + #[test] + fn the_fingerprint_is_platform_fixed() { + // A literal, so a change in the fold is a test failure rather than a + // silent invalidation of every annotation file in existence. + let mut h = Fnv::new(); + h.slice(b"e5r"); + assert_eq!(h.finish(), 0xc3e1_1718_f11f_f4a7); + } +} diff --git a/crates/e5r-db/src/lib.rs b/crates/e5r-db/src/lib.rs new file mode 100644 index 0000000..6732cdf --- /dev/null +++ b/crates/e5r-db/src/lib.rs @@ -0,0 +1,18 @@ +//! Content anchors and the git-mergeable annotation log. +//! +//! What an analyst writes down is the only knowledge in a program that cannot +//! be recomputed, so it is the only thing worth storing. Boundaries, blocks and +//! cross references stay out of the file: regenerating them can never conflict +//! because they are not there to conflict. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod anchor; +pub mod log; +pub mod patch; +pub mod project; +pub mod signature; + +pub use anchor::{Anchor, AnchorIndex, Fnv, Resolution}; +pub use log::{Assertion, Declared, Field, Log, declared}; diff --git a/crates/e5r-db/src/log.rs b/crates/e5r-db/src/log.rs new file mode 100644 index 0000000..4a9d573 --- /dev/null +++ b/crates/e5r-db/src/log.rs @@ -0,0 +1,836 @@ +//! The annotation log: an append-only list of assertions that git can merge. +//! +//! Each assertion is one line. The current state is a fold over the log, and +//! the fold depends only on each record's `(seq, id)`, never on where the line +//! sits in the file. Two analysts on separate branches therefore merge with +//! `git merge` alone: their assertions are distinct lines and the union folds +//! to a well-defined state whichever landed first. +//! +//! Lines are written sorted by content id rather than appended. Appending puts +//! every new line at the end of the file, which is exactly where git's +//! line-based merge conflicts; scattering them by a content hash spreads two +//! branches' additions across the file instead. +//! +//! Scattering is not enough on its own: two insertions into the same gap still +//! collide. The file is therefore declared `merge=union` in `.gitattributes`, +//! git's built-in driver that keeps both sides of a conflicting hunk. For an +//! append-only log whose fold ignores order, the union of both sides *is* the +//! correct merge, and duplicate lines are dropped on read. The header names +//! the rule so anyone who opens the file knows it needs to be there. + +use std::collections::BTreeMap; +use std::fmt::Write as _; + +use e5r_core::{Addr, Error, Result}; +use e5r_types::cdecl; +use e5r_types::ctype::{Signature, TypeId, Types}; + +use crate::anchor::{Anchor, AnchorIndex, Fnv, Resolution}; + +/// Magic on the first line. +const TAG: &str = "e5r-annotations"; +/// On-disk format version. A reader refuses a version it does not know rather +/// than guessing at a layout. +const VERSION: u32 = 1; + +/// Which single-valued attribute of a location an assertion sets. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Field { + /// The name. + Name, + /// The type or prototype, as source text. + Type, + /// A free comment. + Comment, +} + +impl Field { + /// The on-disk keyword. + pub fn as_str(self) -> &'static str { + match self { + Field::Name => "name", + Field::Type => "type", + Field::Comment => "comment", + } + } + + fn tag(self) -> u8 { + match self { + Field::Name => 0, + Field::Type => 1, + Field::Comment => 2, + } + } + + /// Parse an on-disk keyword. + pub fn parse(s: &str) -> Option { + Some(match s { + "name" => Field::Name, + "type" => Field::Type, + "comment" => Field::Comment, + _ => return None, + }) + } + + /// Check a value before it is written. + /// + /// The log stores a type as the source text a person typed, and this is + /// the reason that is safe rather than sloppy. Two properties have to hold + /// together and they pull in opposite directions: + /// + /// * **Reviewable.** The file is read in a pull request. A serialized type + /// graph is not something anybody reviews, and a parser that improves + /// would have to rewrite history to keep up with itself. Text does not. + /// * **Not write-only.** A typed declaration that is never parsed is a + /// comment. Until this existed, `annotate type` wrote a line nothing + /// read, which is the failure this whole path exists to fix. + /// + /// So: parse on read, and validate on write. A typo is rejected at the + /// moment it is typed, which is the only moment the person who made it is + /// still there to fix it. Reading deliberately does not validate: a log + /// written by a newer build must still fold, and refusing a whole file + /// because one type no longer parses would throw away the names and + /// comments beside it. + pub fn validate(self, value: &str) -> std::result::Result<(), String> { + match self { + Field::Comment => Ok(()), + Field::Name => { + if value.trim().is_empty() { + return Err("a name cannot be blank; clear it instead".into()); + } + // A demangled C++ name is full of spaces and punctuation, so + // the only thing refused is what would not survive a listing. + match value.chars().find(|c| c.is_control()) { + Some(c) => Err(format!("a name cannot contain {c:?}")), + None => Ok(()), + } + } + Field::Type => { + let mut types = Types::new(); + declared(&mut types, value) + .map(|_| ()) + .map_err(|e| e.to_string()) + } + } + } +} + +/// What a `type` assertion says, once it has been parsed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Declared { + /// A function declaration: a prototype to lay over what recovery found. + Function { + /// The name the declaration gave, when it gave one. + name: Option, + /// What it takes and returns. + signature: Signature, + /// The function type itself. + ty: TypeId, + }, + /// A type for a data object, or a bare type name. + Object { + /// The name the declaration gave, when it gave one. + name: Option, + /// The type. + ty: TypeId, + }, +} + +impl Declared { + /// The type, whichever kind it is. + pub fn ty(&self) -> TypeId { + match self { + Declared::Function { ty, .. } | Declared::Object { ty, .. } => *ty, + } + } + + /// The name the declaration gave, when it gave one. + pub fn name(&self) -> Option<&str> { + match self { + Declared::Function { name, .. } | Declared::Object { name, .. } => name.as_deref(), + } + } +} + +/// Read a `type` assertion back into the type model. +/// +/// Accepts both of the things an analyst writes: a declaration with a name, +/// which is what a prototype is, and a bare type name, which is what a data +/// object gets. Anything else comes back as an error naming what was not +/// understood, because a type the reader will believe and that is wrong is +/// worse than no type at all. +pub fn declared( + types: &mut Types, + value: &str, +) -> std::result::Result { + match cdecl::declaration(types, value) { + Ok(d) => { + let resolved = types.resolve(d.ty); + match types.get(resolved) { + Some(e5r_types::ctype::Type::Function(sig)) => Ok(Declared::Function { + name: d.name, + signature: sig.clone(), + ty: d.ty, + }), + _ => Ok(Declared::Object { + name: d.name, + ty: d.ty, + }), + } + } + // A bare type name declares nothing, which the declaration grammar + // refuses and an annotation on a data object is written as. + Err(first) => match cdecl::type_name(types, value) { + Ok(ty) => Ok(Declared::Object { name: None, ty }), + Err(second) => Err(if second.offset > first.offset { + second + } else { + first + }), + }, + } +} + +impl std::fmt::Display for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// One assertion: a single edit to one field of one location. +/// +/// Immutable once made. A later assertion to the same `(target, field)` +/// supersedes it under the fold; a `value` of `None` is a tombstone that +/// clears the field. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Assertion { + /// Where it applies. + pub target: Anchor, + /// Which attribute it sets. + pub field: Field, + /// What it sets, or `None` to clear. + pub value: Option, + /// Assignment order: the last-writer-wins clock. + pub seq: u64, + /// Content id, the tiebreak when two branches share a `seq` and the sort + /// key that scatters new lines through the file. + pub id: u64, + /// Who wrote it. + pub who: String, +} + +impl Assertion { + /// Derive the content id from every other field. + fn content_id(target: &Anchor, field: Field, value: Option<&str>, seq: u64, who: &str) -> u64 { + let mut h = Fnv::new(); + let (shape, bytes, insns, offset) = target.identity(); + h.u64(shape); + h.u64(bytes); + h.u64(insns as u64); + h.u64(offset as u64); + h.u64(target.abs.get()); + // Domain separators, so a name and a comment with the same text cannot + // collide. + h.byte(0xff); + h.byte(field.tag()); + h.u64(seq); + h.byte(0xfe); + h.slice(who.as_bytes()); + h.byte(0xfd); + match value { + Some(v) => { + h.byte(1); + h.slice(v.as_bytes()); + } + None => h.byte(0), + } + h.finish() + } + + /// The key a fold groups by. + fn key(&self) -> (u64, u64, u32, u32, Field) { + let (shape, bytes, insns, offset) = self.target.identity(); + (shape, bytes, insns, offset, self.field) + } +} + +/// Everything an analyst has written about one binary. +#[derive(Debug, Clone, Default)] +pub struct Log { + /// Sorted by `id`, which is the on-disk order. + records: Vec, + /// Next sequence number to hand out. + next_seq: u64, + /// Undone assertions, newest last. Cleared by any fresh assertion, which + /// is the editor model everyone already knows. + redo: Vec, + /// The binary this log is about, as a hex digest, when known. + pub binary: Option, +} + +impl Log { + /// An empty log. + pub fn new() -> Log { + Log::default() + } + + /// How many assertions it holds. + pub fn len(&self) -> usize { + self.records.len() + } + + /// True when nothing has been written. + pub fn is_empty(&self) -> bool { + self.records.is_empty() + } + + /// Every assertion, in file order. + pub fn records(&self) -> &[Assertion] { + &self.records + } + + /// Record an assertion. `None` clears the field. + pub fn assert( + &mut self, + target: Anchor, + field: Field, + value: Option, + who: &str, + ) -> &Assertion { + let seq = self.next_seq; + self.next_seq += 1; + let id = Assertion::content_id(&target, field, value.as_deref(), seq, who); + let a = Assertion { + target, + field, + value, + seq, + id, + who: who.to_string(), + }; + self.redo.clear(); + let at = self + .records + .partition_point(|r| (r.id, r.seq) < (a.id, a.seq)); + self.records.insert(at, a); + &self.records[at] + } + + /// Record an assertion, refusing a value the field cannot hold. + /// + /// The checked form of [`Log::assert`], and the one every command that + /// takes a value from a person should use. See [`Field::validate`] for why + /// writing checks and reading does not. + pub fn assert_checked( + &mut self, + target: Anchor, + field: Field, + value: Option, + who: &str, + ) -> Result<&Assertion> { + if let Some(v) = &value { + field.validate(v).map_err(|detail| Error::Inconsistent { + detail: format!("{field} assertion is not valid: {detail}"), + })?; + } + Ok(self.assert(target, field, value, who)) + } + + /// Undo the most recent assertion, returning it. + pub fn undo(&mut self) -> Option { + let at = self + .records + .iter() + .enumerate() + .max_by_key(|(_, r)| (r.seq, r.id)) + .map(|(i, _)| i)?; + let a = self.records.remove(at); + self.redo.push(a.clone()); + Some(a) + } + + /// Put back the most recently undone assertion. + pub fn redo(&mut self) -> Option { + let a = self.redo.pop()?; + let at = self + .records + .partition_point(|r| (r.id, r.seq) < (a.id, a.seq)); + self.records.insert(at, a.clone()); + Some(a) + } + + /// The current state: the winning assertion for every `(location, field)`. + /// + /// Order-independent by construction, which is what lets two branches merge + /// by concatenating lines. + pub fn fold(&self) -> BTreeMap<(u64, u64, u32, u32, Field), &Assertion> { + let mut out: BTreeMap<(u64, u64, u32, u32, Field), &Assertion> = BTreeMap::new(); + for r in &self.records { + match out.get(&r.key()) { + // Last writer wins; a shared seq breaks on the content id, so + // two branches that both wrote at seq 7 still agree. + Some(prev) if (prev.seq, prev.id) >= (r.seq, r.id) => {} + _ => { + out.insert(r.key(), r); + } + } + } + out.retain(|_, r| r.value.is_some()); + out + } + + /// Resolve the folded state against a freshly analyzed binary. + /// + /// Returns one entry per assertion that still points somewhere, with the + /// address it landed on and how sure the match is. + pub fn apply(&self, index: &AnchorIndex) -> Vec { + let mut out = Vec::new(); + for r in self.fold().values() { + if let Some((found, how)) = index.resolve(&r.target) { + out.push(Applied { + addr: found.target().wrapping_offset(r.target.offset as i64), + field: r.field, + value: r.value.clone().unwrap_or_default(), + resolution: how, + who: r.who.clone(), + }); + } + } + out.sort_by_key(|a| (a.addr, a.field)); + out + } + + /// Merge another log into this one. Used to fold in a file that changed + /// under us; the result does not depend on which side is `other`. + pub fn merge(&mut self, other: &Log) { + for r in &other.records { + if self.records.iter().any(|e| e.id == r.id && e.seq == r.seq) { + continue; + } + let at = self + .records + .partition_point(|e| (e.id, e.seq) < (r.id, r.seq)); + self.records.insert(at, r.clone()); + } + self.next_seq = self + .records + .iter() + .map(|r| r.seq + 1) + .max() + .unwrap_or(0) + .max(self.next_seq); + } + + /// Serialize, sorted by content id so new lines scatter through the file. + pub fn to_text(&self) -> String { + let mut s = String::with_capacity(128 + self.records.len() * 96); + let _ = writeln!(s, "{TAG} {VERSION}"); + if let Some(b) = &self.binary { + let _ = writeln!(s, "binary {b}"); + } + let _ = writeln!( + s, + "# One assertion per line, sorted by content id. Order does not affect the" + ); + let _ = writeln!( + s, + "# result, so the union of two branches is the correct merge. Put this in" + ); + let _ = writeln!(s, "# .gitattributes so git does that for you:"); + let _ = writeln!(s, "# *.e5r merge=union"); + let _ = writeln!(s, "# Do not sort or reflow by hand."); + for r in &self.records { + let _ = writeln!( + s, + "{} shape={:016x} bytes={:016x} insns={} abs={:x} off={} seq={} id={:016x} by={} value={}", + r.field, + r.target.shape, + r.target.bytes, + r.target.insns, + r.target.abs.get(), + r.target.offset, + r.seq, + r.id, + escape(&r.who), + match &r.value { + Some(v) => escape(v), + None => "-".to_string(), + } + ); + } + s + } + + /// Parse a serialized log. + pub fn from_text(text: &str) -> Result { + let mut lines = text.lines(); + let header = lines.next().unwrap_or_default(); + let mut parts = header.split_whitespace(); + if parts.next() != Some(TAG) { + return Err(Error::NotRecognized { + expected: "e5r annotation log", + }); + } + let version: u32 = parts + .next() + .and_then(|v| v.parse().ok()) + .ok_or(Error::BadField { + field: "format version", + value: 0, + reason: "is missing from the header line", + })?; + if version > VERSION { + return Err(Error::unsupported(format!( + "annotation format version {version}; this build knows {VERSION}" + ))); + } + + let mut log = Log::new(); + for (n, line) in lines.enumerate() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some(rest) = line.strip_prefix("binary ") { + log.binary = Some(rest.trim().to_string()); + continue; + } + match parse_record(line) { + Some(r) => log.records.push(r), + None => { + return Err(Error::inconsistent(format!( + "annotation line {} is malformed: {line:?}", + n + 2 + ))); + } + } + } + log.records.sort_by_key(|r| (r.id, r.seq)); + log.records.dedup_by_key(|r| (r.id, r.seq)); + log.next_seq = log.records.iter().map(|r| r.seq + 1).max().unwrap_or(0); + Ok(log) + } +} + +/// One folded assertion placed on a freshly analyzed binary. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Applied { + /// Where it landed. + pub addr: Addr, + /// Which field it sets. + pub field: Field, + /// The value. + pub value: String, + /// How confident the match is. + pub resolution: Resolution, + /// Who wrote it. + pub who: String, +} + +/// Quote a value so it survives a round trip through a whitespace-split line. +fn escape(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + out.push('"'); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\t' => out.push_str("\\t"), + c => out.push(c), + } + } + out.push('"'); + out +} + +fn unescape(s: &str) -> Option { + let body = s.strip_prefix('"')?.strip_suffix('"')?; + let mut out = String::with_capacity(body.len()); + let mut chars = body.chars(); + while let Some(c) = chars.next() { + if c != '\\' { + out.push(c); + continue; + } + match chars.next()? { + 'n' => out.push('\n'), + 't' => out.push('\t'), + '"' => out.push('"'), + '\\' => out.push('\\'), + other => out.push(other), + } + } + Some(out) +} + +fn parse_record(line: &str) -> Option { + let (field_word, rest) = line.split_once(' ')?; + let field = Field::parse(field_word)?; + + let mut shape = None; + let mut bytes = None; + let mut insns = None; + let mut abs = None; + let mut offset = 0u32; + let mut seq = None; + let mut id = None; + let mut who = String::new(); + let mut value: Option> = None; + + // `value=` and `by=` are quoted and may hold spaces, so they are taken + // whole rather than split on whitespace. + let mut cursor = rest; + while !cursor.is_empty() { + let cursor_trim = cursor.trim_start(); + if cursor_trim.is_empty() { + break; + } + let (key, after) = cursor_trim.split_once('=')?; + if after.starts_with('"') { + let end = find_quote_end(after)?; + let (quoted, tail) = after.split_at(end + 1); + let v = unescape(quoted)?; + match key { + "by" => who = v, + "value" => value = Some(Some(v)), + _ => return None, + } + cursor = tail; + continue; + } + let (v, tail) = match after.find(' ') { + Some(i) => after.split_at(i), + None => (after, ""), + }; + match key { + "shape" => shape = u64::from_str_radix(v, 16).ok(), + "bytes" => bytes = u64::from_str_radix(v, 16).ok(), + "insns" => insns = v.parse().ok(), + "abs" => abs = u64::from_str_radix(v, 16).ok(), + "off" => offset = v.parse().ok()?, + "seq" => seq = v.parse().ok(), + "id" => id = u64::from_str_radix(v, 16).ok(), + "value" if v == "-" => value = Some(None), + _ => return None, + } + cursor = tail; + } + + Some(Assertion { + target: Anchor { + shape: shape?, + bytes: bytes?, + insns: insns?, + abs: Addr(abs?), + offset, + }, + field, + value: value?, + seq: seq?, + id: id?, + who, + }) +} + +/// Index of the closing quote of a quoted value starting at index 0. +fn find_quote_end(s: &str) -> Option { + let b = s.as_bytes(); + let mut i = 1; + while i < b.len() { + match b[i] { + b'\\' => i += 2, + b'"' => return Some(i), + _ => i += 1, + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + fn anchor(shape: u64, bytes: u64) -> Anchor { + Anchor { + shape, + bytes, + insns: 4, + abs: Addr(0x1000), + offset: 0, + } + } + + #[test] + fn the_last_writer_wins() { + let mut log = Log::new(); + let a = anchor(1, 2); + log.assert(a, Field::Name, Some("first".into()), "alice"); + log.assert(a, Field::Name, Some("second".into()), "alice"); + let folded = log.fold(); + assert_eq!(folded.len(), 1); + assert_eq!( + folded.values().next().unwrap().value.as_deref(), + Some("second") + ); + } + + #[test] + fn a_tombstone_clears_the_field() { + let mut log = Log::new(); + let a = anchor(1, 2); + log.assert(a, Field::Comment, Some("note".into()), "alice"); + log.assert(a, Field::Comment, None, "alice"); + assert!(log.fold().is_empty()); + // The tombstone is still a record; clearing is not forgetting. + assert_eq!(log.len(), 2); + } + + #[test] + fn fields_are_independent() { + let mut log = Log::new(); + let a = anchor(1, 2); + log.assert(a, Field::Name, Some("f".into()), "alice"); + log.assert(a, Field::Comment, Some("c".into()), "alice"); + log.assert(a, Field::Type, Some("int f(void)".into()), "alice"); + assert_eq!(log.fold().len(), 3); + } + + #[test] + fn round_trips_through_text() { + let mut log = Log::new(); + log.binary = Some("sha256:abcd".into()); + log.assert( + anchor(1, 2), + Field::Name, + Some("parse_header".into()), + "alice", + ); + log.assert( + anchor(3, 4), + Field::Comment, + Some("handles the \"quoted\" case\nand a newline".into()), + "bob", + ); + log.assert(anchor(5, 6), Field::Name, None, "carol"); + let text = log.to_text(); + let back = Log::from_text(&text).unwrap(); + assert_eq!(back.records, log.records); + assert_eq!(back.binary, log.binary); + assert_eq!(back.to_text(), text); + } + + #[test] + fn two_branches_merge_to_the_same_state_either_way() { + // The property the whole design exists for. + let mut base = Log::new(); + base.assert(anchor(1, 2), Field::Name, Some("shared".into()), "alice"); + + let mut left = base.clone(); + left.assert(anchor(3, 4), Field::Name, Some("from_left".into()), "alice"); + let mut right = base.clone(); + right.assert(anchor(5, 6), Field::Name, Some("from_right".into()), "bob"); + + let mut a = left.clone(); + a.merge(&right); + let mut b = right.clone(); + b.merge(&left); + assert_eq!(a.to_text(), b.to_text()); + assert_eq!(a.fold().len(), 3); + } + + #[test] + fn concatenating_two_files_is_a_valid_merge() { + // What git actually does to non-overlapping line insertions. + let mut left = Log::new(); + left.assert(anchor(3, 4), Field::Name, Some("left".into()), "alice"); + let mut right = Log::new(); + right.assert(anchor(5, 6), Field::Name, Some("right".into()), "bob"); + + let lt = left.to_text(); + let rt = right.to_text(); + let lines: Vec<&str> = lt + .lines() + .chain(rt.lines().filter(|l| !l.starts_with(TAG))) + .collect(); + let merged = Log::from_text(&lines.join("\n")).unwrap(); + assert_eq!(merged.fold().len(), 2); + } + + #[test] + fn a_shared_sequence_number_breaks_on_content() { + // Two branches both writing their first assertion get seq 0. The fold + // must still pick one, and pick the same one on both machines. + let a = anchor(1, 2); + let mut left = Log::new(); + left.assert(a, Field::Name, Some("left".into()), "alice"); + let mut right = Log::new(); + right.assert(a, Field::Name, Some("right".into()), "bob"); + + let mut m1 = left.clone(); + m1.merge(&right); + let mut m2 = right.clone(); + m2.merge(&left); + let v1 = m1.fold().values().next().unwrap().value.clone(); + let v2 = m2.fold().values().next().unwrap().value.clone(); + assert_eq!(v1, v2); + } + + #[test] + fn undo_and_redo_walk_the_log() { + let mut log = Log::new(); + let a = anchor(1, 2); + log.assert(a, Field::Name, Some("one".into()), "alice"); + log.assert(a, Field::Name, Some("two".into()), "alice"); + assert_eq!(log.undo().unwrap().value.as_deref(), Some("two")); + assert_eq!( + log.fold().values().next().unwrap().value.as_deref(), + Some("one") + ); + assert_eq!(log.redo().unwrap().value.as_deref(), Some("two")); + assert_eq!( + log.fold().values().next().unwrap().value.as_deref(), + Some("two") + ); + // A fresh assertion drops the redo stack, as an editor does. + log.undo(); + log.assert(a, Field::Name, Some("three".into()), "alice"); + assert!(log.redo().is_none()); + } + + #[test] + fn a_newer_format_version_is_refused_rather_than_guessed_at() { + let text = format!("{TAG} 99\n"); + assert!(Log::from_text(&text).is_err()); + assert!(Log::from_text("not an annotation file\n").is_err()); + } + + #[test] + fn a_malformed_line_names_itself() { + let text = format!("{TAG} 1\nname shape=zz\n"); + let e = Log::from_text(&text).unwrap_err().to_string(); + assert!(e.contains("line 2"), "{e}"); + } + + #[test] + fn on_disk_order_is_by_content_so_appends_scatter() { + // The property that keeps git from conflicting: consecutive writes do + // not land next to each other in the file. + let mut log = Log::new(); + for i in 0..64u64 { + log.assert( + anchor(i, i * 7), + Field::Name, + Some(format!("f{i}")), + "alice", + ); + } + let ids: Vec = log.records().iter().map(|r| r.id).collect(); + let mut sorted = ids.clone(); + sorted.sort_unstable(); + assert_eq!(ids, sorted); + // The last assertion made is not the last line. + let last_seq_pos = log.records().iter().position(|r| r.seq == 63).unwrap(); + assert!(last_seq_pos < 63, "newest assertion landed at the end"); + } +} diff --git a/crates/e5r-db/src/patch.rs b/crates/e5r-db/src/patch.rs new file mode 100644 index 0000000..3eac01e --- /dev/null +++ b/crates/e5r-db/src/patch.rs @@ -0,0 +1,818 @@ +//! Patch sets: byte edits that carry the bytes they expect to overwrite. +//! +//! "Write 90 at 4011a2" is neither auditable nor safe: applied to a binary it +//! was not made against it corrupts the file and says nothing. Every edit here +//! records the bytes it expects to find, so the wrong binary is a reported +//! conflict instead of a wreck, and recording them is also what makes a patch +//! set invertible. +//! +//! Edits are anchored by content plus an offset, exactly as annotations are, +//! so a patch made before a relink still lands on the right instruction; the +//! result says which layer matched rather than pretending an address hit is as +//! good as a content hit. The file is sorted by content id and merges under +//! the same union rule as the annotation log. + +use std::fmt; +use std::fmt::Write as _; + +use e5r_core::{Addr, Error, Result}; + +use crate::anchor::{Anchor, AnchorIndex, Fnv, Resolution}; + +/// Magic on the first line. +const TAG: &str = "e5r-patches"; +/// On-disk format version. A reader refuses a version it does not know. +const VERSION: u32 = 1; + +/// One byte edit: where it goes, what must be there, and what to write. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Edit { + /// Where it applies, anchored by content with an offset inside. + pub target: Anchor, + /// What the image must hold for this to be the patch it claims to be. + pub expect: Vec, + /// What to write. Always as long as `expect`: a different length would + /// move every byte after it and invalidate the rest of the patch set. + pub replace: Vec, + /// Who made it. + pub who: String, + /// Why, in one line. + pub note: String, +} + +impl Edit { + /// An edit on an anchored location. + pub fn new(target: Anchor, expect: Vec, replace: Vec) -> Result { + if expect.is_empty() { + return Err(Error::inconsistent("a patch edit writes no bytes")); + } + if expect.len() != replace.len() { + return Err(Error::inconsistent(format!( + "edit at {} replaces {} bytes with {}; an in-place patch cannot change length", + target.target(), + expect.len(), + replace.len() + ))); + } + Ok(Edit { + target, + expect, + replace, + who: String::new(), + note: String::new(), + }) + } + + /// An edit on a bare address, for a location no anchor covers. + pub fn at(addr: Addr, expect: Vec, replace: Vec) -> Result { + let target = Anchor { + shape: 0, + bytes: 0, + insns: 0, + abs: addr, + offset: 0, + }; + Edit::new(target, expect, replace) + } + + /// Attribute it. + pub fn by(self, who: &str) -> Edit { + Edit { + who: who.to_string(), + ..self + } + } + + /// Explain it. + pub fn noted(self, note: &str) -> Edit { + Edit { + note: note.to_string(), + ..self + } + } + + /// The address recorded when the edit was made. + pub fn addr(&self) -> Addr { + self.target.target() + } + + /// The same edit backwards. + pub fn inverse(&self) -> Edit { + Edit { + target: self.target, + expect: self.replace.clone(), + replace: self.expect.clone(), + who: self.who.clone(), + note: self.note.clone(), + } + } + + /// Content id: the sort key that scatters new lines through the file. + pub fn id(&self) -> u64 { + let mut h = Fnv::new(); + let (shape, bytes, insns, offset) = self.target.identity(); + h.u64(shape); + h.u64(bytes); + h.u64(insns as u64); + h.u64(offset as u64); + h.u64(self.target.abs.get()); + // Domain separators, so the fields cannot run into each other. + h.byte(0xff); + h.slice(&self.expect); + h.byte(0xfe); + h.slice(&self.replace); + h.byte(0xfd); + h.slice(self.who.as_bytes()); + h.byte(0xfc); + h.slice(self.note.as_bytes()); + h.finish() + } + + /// True when the edit carries a content anchor rather than an address + /// alone; a zeroed anchor would match whatever happened to hash to zero. + fn is_anchored(&self) -> bool { + self.target.shape != 0 || self.target.bytes != 0 + } +} + +/// One edit placed on an image, before anything is written. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Change { + /// Which edit, by index into the patch set. + pub edit: usize, + /// Where it lands in this image. + pub addr: Addr, + /// What is there now. + pub before: Vec, + /// What would be there after. + pub after: Vec, + /// How the location was found. + pub resolution: Resolution, +} + +/// What applying a patch set did. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Applied { + /// Every change made, in address order. + pub changes: Vec, + /// How many bytes were written. + pub bytes: usize, +} + +impl Applied { + /// True when every edit landed on a match stronger than the address. + pub fn is_confident(&self) -> bool { + self.changes.iter().all(|c| c.resolution.is_confident()) + } +} + +/// Why a patch set would not apply. Nothing is ever written when one is +/// returned: the whole set lands or none of it does. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Conflict { + /// The image does not hold what the edit was made against. + Mismatch { + /// Which edit, by index into the patch set. + edit: usize, + /// Where it was checked. + addr: Addr, + /// What the patch expected there. + expected: Vec, + /// What is actually there. + found: Vec, + }, + /// The edit lands outside the image it was given. + OutOfRange { + /// Which edit. + edit: usize, + /// Where it wanted to write. + addr: Addr, + /// How many bytes. + len: usize, + /// How big the image is. + image: usize, + }, + /// Two edits write the same byte, so the result would depend on order. + Overlap { + /// The earlier edit. + first: usize, + /// The one that runs into it. + second: usize, + /// Where they meet. + addr: Addr, + }, +} + +impl fmt::Display for Conflict { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Conflict::Mismatch { + edit, + addr, + expected, + found, + } => write!( + f, + "edit {edit} at {addr} expected {} but found {}; this patch was made against a \ + different binary", + hex(expected), + hex(found) + ), + Conflict::OutOfRange { + edit, + addr, + len, + image, + } => write!( + f, + "edit {edit} writes {len} bytes at {addr}, which is outside the {image}-byte image" + ), + Conflict::Overlap { + first, + second, + addr, + } => write!(f, "edits {first} and {second} both write {addr}"), + } + } +} + +impl std::error::Error for Conflict {} + +/// A named set of edits, applied and reverted as a unit. +#[derive(Debug, Clone, Default)] +pub struct Patch { + /// Sorted by content id, which is the on-disk order. + edits: Vec, + /// What the set is called. + pub name: String, + /// The binary it was made against, as a hex digest, when known. + pub binary: Option, +} + +impl Patch { + /// An empty set. + pub fn new(name: &str) -> Patch { + Patch { + edits: Vec::new(), + name: name.to_string(), + binary: None, + } + } + + /// How many edits it holds. + pub fn len(&self) -> usize { + self.edits.len() + } + + /// True when it holds none. + pub fn is_empty(&self) -> bool { + self.edits.is_empty() + } + + /// Every edit, in file order. + pub fn edits(&self) -> &[Edit] { + &self.edits + } + + /// Add an edit, keeping the file order. A duplicate is dropped, because + /// the same edit twice would be an overlap with itself. + pub fn push(&mut self, edit: Edit) { + let id = edit.id(); + let at = self.edits.partition_point(|e| e.id() < id); + if self.edits.get(at) == Some(&edit) { + return; + } + self.edits.insert(at, edit); + } + + /// Capture an edit from the image, so `expect` cannot be typed wrong. + /// + /// `base` is the address the first byte of `image` is loaded at. + pub fn record( + &mut self, + target: Anchor, + image: &[u8], + base: Addr, + replace: Vec, + ) -> Result<()> { + let addr = target.target(); + let off = base + .distance_to(addr) + .and_then(|d| usize::try_from(d).ok()) + .ok_or(Error::Unmapped { addr })?; + let end = off + .checked_add(replace.len()) + .filter(|e| *e <= image.len()) + .ok_or(Error::OutOfBounds { + what: "patch edit", + offset: off as u64, + len: replace.len() as u64, + available: image.len() as u64, + })?; + self.push(Edit::new(target, image[off..end].to_vec(), replace)?); + Ok(()) + } + + /// What would change, without writing anything. + /// + /// `base` is the address the first byte of `image` is loaded at, and + /// `index` anchors the edits against this image; without one every edit + /// falls back to the address it was recorded at. + pub fn preview( + &self, + image: &[u8], + base: Addr, + index: Option<&AnchorIndex>, + ) -> std::result::Result, Conflict> { + let mut changes: Vec = Vec::with_capacity(self.edits.len()); + for (n, e) in self.edits.iter().enumerate() { + let (addr, resolution) = place(e, index); + let out_of_range = || Conflict::OutOfRange { + edit: n, + addr, + len: e.expect.len(), + image: image.len(), + }; + let off = base + .distance_to(addr) + .and_then(|d| usize::try_from(d).ok()) + .ok_or_else(out_of_range)?; + let end = off + .checked_add(e.expect.len()) + .filter(|end| *end <= image.len()) + .ok_or_else(out_of_range)?; + let found = &image[off..end]; + if found != e.expect { + return Err(Conflict::Mismatch { + edit: n, + addr, + expected: e.expect.clone(), + found: found.to_vec(), + }); + } + changes.push(Change { + edit: n, + addr, + before: found.to_vec(), + after: e.replace.clone(), + resolution, + }); + } + changes.sort_by_key(|c| (c.addr, c.edit)); + for w in changes.windows(2) { + let end = w[0].addr.get().saturating_add(w[0].before.len() as u64); + if end > w[1].addr.get() { + return Err(Conflict::Overlap { + first: w[0].edit, + second: w[1].edit, + addr: w[1].addr, + }); + } + } + Ok(changes) + } + + /// Apply every edit, or none of them. + pub fn apply( + &self, + image: &mut [u8], + base: Addr, + index: Option<&AnchorIndex>, + ) -> std::result::Result { + let changes = self.preview(image, base, index)?; + let mut bytes = 0; + for c in &changes { + // The preview proved the range and the expected bytes, so this + // cannot be the write that discovers a problem half way through. + let off = base.distance_to(c.addr).unwrap_or(0) as usize; + image[off..off + c.after.len()].copy_from_slice(&c.after); + bytes += c.after.len(); + } + Ok(Applied { changes, bytes }) + } + + /// The patch set that undoes this one. + /// + /// The anchors are kept as they are: against an image this patch has + /// already been applied to their byte fingerprint no longer matches, so + /// the inverse resolves by shape or by address, which is exactly what the + /// resolution in the result reports. + pub fn revert(&self) -> Patch { + let mut out = Patch::new(&self.name); + out.binary = self.binary.clone(); + for e in &self.edits { + out.push(e.inverse()); + } + out + } + + /// Merge another set in. The result does not depend on which side is + /// `other`, so two branches converge. + pub fn merge(&mut self, other: &Patch) { + for e in &other.edits { + self.push(e.clone()); + } + } + + /// Serialize, sorted by content id so new lines scatter through the file. + pub fn to_text(&self) -> String { + let mut s = String::with_capacity(128 + self.edits.len() * 96); + let _ = writeln!(s, "{TAG} {VERSION}"); + let _ = writeln!(s, "name {}", quote(&self.name)); + if let Some(b) = &self.binary { + let _ = writeln!(s, "binary {b}"); + } + let _ = writeln!( + s, + "# One edit per line, sorted by content id. Every edit records the bytes it" + ); + let _ = writeln!( + s, + "# expects to overwrite, so applying to the wrong binary fails instead of" + ); + let _ = writeln!( + s, + "# corrupting it. Order does not affect the result, so the union of two" + ); + let _ = writeln!(s, "# branches is the correct merge:"); + let _ = writeln!(s, "# *.e5r-patch merge=union"); + for e in &self.edits { + let _ = writeln!( + s, + "edit shape={:016x} bytes={:016x} insns={} abs={:x} off={} expect={} replace={} \ + by={} note={}", + e.target.shape, + e.target.bytes, + e.target.insns, + e.target.abs.get(), + e.target.offset, + hex(&e.expect), + hex(&e.replace), + quote(&e.who), + quote(&e.note), + ); + } + s + } + + /// Parse a serialized patch set. + pub fn from_text(text: &str) -> Result { + let mut lines = text.lines(); + let header = lines.next().unwrap_or_default(); + let mut parts = header.split_whitespace(); + if parts.next() != Some(TAG) { + return Err(Error::NotRecognized { + expected: "e5r patch set", + }); + } + let version: u32 = parts + .next() + .and_then(|v| v.parse().ok()) + .ok_or(Error::BadField { + field: "format version", + value: 0, + reason: "is missing from the header line", + })?; + if version > VERSION { + return Err(Error::unsupported(format!( + "patch format version {version}; this build knows {VERSION}" + ))); + } + + let mut patch = Patch::new(""); + for (n, line) in lines.enumerate() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some(rest) = line.strip_prefix("name ") { + patch.name = unquote(rest.trim()).ok_or_else(|| malformed(n, line))?; + continue; + } + if let Some(rest) = line.strip_prefix("binary ") { + patch.binary = Some(rest.trim().to_string()); + continue; + } + patch.push(parse_edit(line).ok_or_else(|| malformed(n, line))?); + } + Ok(patch) + } +} + +/// Where an edit lands in this image, and on what evidence. +fn place(e: &Edit, index: Option<&AnchorIndex>) -> (Addr, Resolution) { + if e.is_anchored() { + if let Some((found, how)) = index.and_then(|ix| ix.resolve(&e.target)) { + return (found.target().wrapping_offset(e.target.offset as i64), how); + } + } + (e.addr(), Resolution::Address) +} + +fn malformed(n: usize, line: &str) -> Error { + Error::inconsistent(format!("patch line {} is malformed: {line:?}", n + 2)) +} + +fn parse_edit(line: &str) -> Option { + let rest = line.strip_prefix("edit ")?; + // The two free-text fields are quoted and last; the head is bare tokens, + // so the first ` by="` in the line is the one that ends it. + let head_end = rest.find(" by=\"")?; + let (head, tail) = rest.split_at(head_end); + let who_q = &tail[4..]; + let end = find_quote_end(who_q)?; + let who = unquote(&who_q[..=end])?; + let note = unquote(who_q[end + 1..].strip_prefix(" note=")?)?; + + let mut shape = None; + let mut bytes = None; + let mut insns = None; + let mut abs = None; + let mut offset = 0u32; + let mut expect = None; + let mut replace = None; + for field in head.split_whitespace() { + let (key, v) = field.split_once('=')?; + match key { + "shape" => shape = u64::from_str_radix(v, 16).ok(), + "bytes" => bytes = u64::from_str_radix(v, 16).ok(), + "insns" => insns = v.parse().ok(), + "abs" => abs = u64::from_str_radix(v, 16).ok(), + "off" => offset = v.parse().ok()?, + "expect" => expect = unhex(v), + "replace" => replace = unhex(v), + _ => return None, + } + } + let target = Anchor { + shape: shape?, + bytes: bytes?, + insns: insns?, + abs: Addr(abs?), + offset, + }; + Some( + Edit::new(target, expect?, replace?) + .ok()? + .by(&who) + .noted(¬e), + ) +} + +/// Lowercase hex, the form a hex editor shows. +fn hex(b: &[u8]) -> String { + let mut s = String::with_capacity(b.len() * 2); + for byte in b { + let _ = write!(s, "{byte:02x}"); + } + s +} + +fn unhex(s: &str) -> Option> { + if s.is_empty() || s.len() % 2 != 0 { + return None; + } + (0..s.len() / 2) + .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()) + .collect() +} + +/// Quote a value so it survives a round trip through a whitespace-split line. +fn quote(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + out.push('"'); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\t' => out.push_str("\\t"), + c => out.push(c), + } + } + out.push('"'); + out +} + +fn unquote(s: &str) -> Option { + let body = s.strip_prefix('"')?.strip_suffix('"')?; + let mut out = String::with_capacity(body.len()); + let mut chars = body.chars(); + while let Some(c) = chars.next() { + if c != '\\' { + out.push(c); + continue; + } + match chars.next()? { + 'n' => out.push('\n'), + 't' => out.push('\t'), + '"' => out.push('"'), + '\\' => out.push('\\'), + other => out.push(other), + } + } + Some(out) +} + +/// Index of the closing quote of a quoted value starting at index 0. +fn find_quote_end(s: &str) -> Option { + let b = s.as_bytes(); + let mut i = 1; + while i < b.len() { + match b[i] { + b'\\' => i += 2, + b'"' => return Some(i), + _ => i += 1, + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + fn anchor(shape: u64, bytes: u64, abs: u64) -> Anchor { + Anchor { + shape, + bytes, + insns: 4, + abs: Addr(abs), + offset: 0, + } + } + + fn edit(abs: u64, expect: &[u8], replace: &[u8]) -> Edit { + Edit::new( + anchor(abs ^ 0x51, abs ^ 0x77, abs), + expect.to_vec(), + replace.to_vec(), + ) + .unwrap() + } + + #[test] + fn an_edit_cannot_change_the_length_of_the_image() { + assert!(Edit::at(Addr(0x1000), vec![0x90], vec![0x90, 0x90]).is_err()); + assert!(Edit::at(Addr(0x1000), vec![], vec![]).is_err()); + } + + #[test] + fn applying_to_the_wrong_bytes_changes_nothing() { + let mut patch = Patch::new("nop the check"); + patch.push(edit(0x1000, &[0x74, 0x0c], &[0x90, 0x90])); + patch.push(edit(0x1010, &[0xcc], &[0x90])); + let mut image = vec![0u8; 0x20]; + image[0] = 0x74; + image[1] = 0x0c; + // One edit does not match, so neither is written. + let e = patch.apply(&mut image, Addr(0x1000), None).unwrap_err(); + assert!( + matches!( + e, + Conflict::Mismatch { + addr: Addr(0x1010), + .. + } + ), + "{e}" + ); + assert_eq!(image[0..2], [0x74, 0x0c]); + } + + #[test] + fn applying_then_reverting_restores_the_image() { + let mut image: Vec = (0..64u8).collect(); + let original = image.clone(); + let mut patch = Patch::new("p"); + patch.push(edit(0x1004, &[4, 5, 6], &[0x90, 0x90, 0x90])); + patch.push(edit(0x1020, &[32], &[0xcc])); + let applied = patch.apply(&mut image, Addr(0x1000), None).unwrap(); + assert_eq!(applied.bytes, 4); + assert_ne!(image, original); + patch + .revert() + .apply(&mut image, Addr(0x1000), None) + .unwrap(); + assert_eq!(image, original); + } + + #[test] + fn a_preview_writes_nothing() { + let image: Vec = (0..16u8).collect(); + let mut patch = Patch::new("p"); + patch.push(edit(0x1002, &[2], &[0xff])); + let changes = patch.preview(&image, Addr(0x1000), None).unwrap(); + assert_eq!(changes.len(), 1); + assert_eq!(changes[0].addr, Addr(0x1002)); + assert_eq!(changes[0].before, vec![2]); + assert_eq!(changes[0].after, vec![0xff]); + assert_eq!(image[2], 2); + } + + #[test] + fn overlapping_edits_are_refused_rather_than_ordered() { + let mut image = vec![0u8; 16]; + let mut patch = Patch::new("p"); + patch.push(edit(0x1000, &[0, 0, 0, 0], &[1, 1, 1, 1])); + patch.push(edit(0x1002, &[0, 0], &[2, 2])); + let e = patch.apply(&mut image, Addr(0x1000), None).unwrap_err(); + assert!(matches!(e, Conflict::Overlap { .. }), "{e}"); + assert_eq!(image, vec![0u8; 16]); + } + + #[test] + fn an_edit_outside_the_image_is_a_conflict() { + let mut image = vec![0u8; 4]; + let mut patch = Patch::new("p"); + patch.push(edit(0x2000, &[0], &[1])); + let e = patch.apply(&mut image, Addr(0x1000), None).unwrap_err(); + assert!(matches!(e, Conflict::OutOfRange { .. }), "{e}"); + } + + #[test] + fn the_text_form_round_trips() { + let mut patch = Patch::new("skip the \"licence\" check"); + patch.binary = Some("fnv:1234".into()); + patch.push( + edit(0x1000, &[0x74, 0x0c], &[0x90, 0x90]) + .by("alice") + .noted("a note with spaces\nand a newline"), + ); + patch.push(edit(0x1010, &[0xcc], &[0x90]).by("bob")); + let text = patch.to_text(); + let back = Patch::from_text(&text).unwrap(); + assert_eq!(back.edits, patch.edits); + assert_eq!(back.name, patch.name); + assert_eq!(back.binary, patch.binary); + assert_eq!(back.to_text(), text); + } + + #[test] + fn a_newer_format_version_is_refused_rather_than_guessed_at() { + assert!(Patch::from_text(&format!("{TAG} 99\n")).is_err()); + assert!(Patch::from_text("something else\n").is_err()); + let e = Patch::from_text(&format!("{TAG} 1\nedit shape=zz\n")) + .unwrap_err() + .to_string(); + assert!(e.contains("line 2"), "{e}"); + } + + #[test] + fn on_disk_order_is_by_content_so_appends_scatter() { + let mut patch = Patch::new("p"); + for i in 0..64u64 { + patch.push(edit(0x1000 + i * 8, &[i as u8], &[0x90])); + } + let ids: Vec = patch.edits().iter().map(|e| e.id()).collect(); + let mut sorted = ids.clone(); + sorted.sort_unstable(); + assert_eq!(ids, sorted); + } + + #[test] + fn merging_two_branches_converges() { + let mut left = Patch::new("p"); + left.push(edit(0x1000, &[1], &[0x90])); + let mut right = Patch::new("p"); + right.push(edit(0x2000, &[2], &[0x90])); + let mut a = left.clone(); + a.merge(&right); + let mut b = right.clone(); + b.merge(&left); + assert_eq!(a.to_text(), b.to_text()); + assert_eq!(a.len(), 2); + // The same edit from both sides is one edit, not two. + a.merge(&left); + assert_eq!(a.len(), 2); + } + + #[test] + fn an_address_only_edit_ignores_the_index() { + // A zeroed anchor is not a fingerprint, so it must not be looked up. + let a = anchor(0, 0, 0x1000); + let ix = AnchorIndex::build([a]); + let e = Edit::at(Addr(0x1004), vec![4], vec![0x90]).unwrap(); + let mut patch = Patch::new("p"); + patch.push(e); + let image: Vec = (0..16u8).collect(); + let changes = patch.preview(&image, Addr(0x1000), Some(&ix)).unwrap(); + assert_eq!(changes[0].addr, Addr(0x1004)); + assert_eq!(changes[0].resolution, Resolution::Address); + } + + #[test] + fn recording_captures_what_is_there() { + let image: Vec = (0..16u8).collect(); + let mut patch = Patch::new("p"); + patch + .record(anchor(1, 2, 0x1004), &image, Addr(0x1000), vec![0x90]) + .unwrap(); + assert_eq!(patch.edits()[0].expect, vec![4]); + assert!( + patch + .record(anchor(1, 2, 0x9000), &image, Addr(0x1000), vec![0x90]) + .is_err() + ); + } +} diff --git a/crates/e5r-db/src/project.rs b/crates/e5r-db/src/project.rs new file mode 100644 index 0000000..42e57a4 --- /dev/null +++ b/crates/e5r-db/src/project.rs @@ -0,0 +1,412 @@ +//! The project file: everything needed to reopen an analysis session. +//! +//! A session is worth reopening only if it reopens on the same binary with the +//! same configuration; a project that silently attaches to a different build +//! is worse than no project, because every annotation in it is then a lie. So +//! the file records the binary by content as well as by path, and opening one +//! starts with a verdict on whether the bytes are the bytes it was made for. +//! +//! What it holds is references, not copies: the annotation log, the signature +//! libraries and the patch sets stay in their own files, which are the ones +//! that merge. The project is `key = value` lines sorted by key, so a diff +//! shows what changed about a session rather than a reordering of it. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::fmt::Write as _; + +use e5r_core::{Addr, Arch, Error, Result}; + +use crate::anchor::Fnv; + +/// Magic on the first line. +const TAG: &str = "e5r-project"; +/// On-disk format version. A reader refuses a version it does not know rather +/// than guessing at a layout. +pub const VERSION: u32 = 1; + +/// Content digest of a binary. +/// +/// FNV-1a, the same fold the anchors use: a project needs a stable identity +/// for a file, not a defence against someone forging one. +pub fn digest(data: &[u8]) -> u64 { + let mut h = Fnv::new(); + h.u64(data.len() as u64); + h.slice(data); + h.finish() +} + +/// The binary a project is about. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Binary { + /// Where it was when the project was made. + pub path: String, + /// How big it is, checked before the digest because it is free. + pub size: u64, + /// Its content digest. + pub hash: u64, +} + +/// How the binary was loaded. Mirrors the loader's options rather than sharing +/// them, because the loader sits above this crate. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Load { + /// Base address for a raw image, or a rebase for a relocatable one. + pub base: Option, + /// Architecture override. It names a decoder, so a name no native decoder + /// answers to is kept as a SLEIGH language id. + pub arch: Option, + /// Which optional readers were enabled, by name. A set: they are flags, + /// and two of them in the other order is the same configuration. + pub readers: BTreeSet, +} + +/// A saved analysis session. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Project { + /// The binary, by path and by content. + pub binary: Binary, + /// The load configuration. + pub load: Load, + /// Analysis options, free-form because the analyzer that owns their names + /// sits above this crate and a project must not go stale when it grows one. + pub analysis: BTreeMap, + /// The annotation log, if the session has one. + pub log: Option, + /// Signature libraries, in precedence order. + pub signatures: Vec, + /// Patch sets, in the order they apply. + pub patches: Vec, +} + +/// Whether a binary is the one a project was made for. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Verdict { + /// The same bytes at the same path. + Same, + /// The same bytes somewhere else: the session is valid and the path is + /// stale, which is what a moved or re-fetched binary looks like. + Moved { + /// Where the project says it lives. + recorded: String, + /// Where these bytes came from. + found: String, + }, + /// Not the binary the project was made for. + Different { + /// The size the project recorded. + recorded_size: u64, + /// The size of the bytes given. + found_size: u64, + /// The digest the project recorded. + recorded_hash: u64, + /// The digest of the bytes given. + found_hash: u64, + }, +} + +impl Verdict { + /// True when the session can be opened on these bytes. + pub fn is_usable(&self) -> bool { + !matches!(self, Verdict::Different { .. }) + } +} + +impl fmt::Display for Verdict { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Verdict::Same => f.write_str("the binary this project was made for"), + Verdict::Moved { recorded, found } => write!( + f, + "the same binary, moved: the project records {recorded}, this is {found}" + ), + Verdict::Different { + recorded_size, + found_size, + recorded_hash, + found_hash, + } => write!( + f, + "a different binary: the project was made for {recorded_size} bytes \ + ({recorded_hash:016x}), this is {found_size} bytes ({found_hash:016x})" + ), + } + } +} + +impl Project { + /// A project for a binary that has just been read. + pub fn for_binary(path: &str, data: &[u8]) -> Project { + Project { + binary: Binary { + path: path.to_string(), + size: data.len() as u64, + hash: digest(data), + }, + ..Project::default() + } + } + + /// Whether these bytes are the binary the project was made for. + /// + /// Content only, so it can never report a move; use [`Project::verify_at`] + /// when the caller knows where the bytes came from, which is the only way + /// a binary that is identical but relocated is distinguishable from one + /// that never moved. + pub fn verify(&self, data: &[u8]) -> Verdict { + self.verify_at(&self.binary.path, data) + } + + /// The same check when the path the bytes were read from is known. + pub fn verify_at(&self, path: &str, data: &[u8]) -> Verdict { + let found_size = data.len() as u64; + let found_hash = digest(data); + if found_size != self.binary.size || found_hash != self.binary.hash { + return Verdict::Different { + recorded_size: self.binary.size, + found_size, + recorded_hash: self.binary.hash, + found_hash, + }; + } + if path != self.binary.path { + return Verdict::Moved { + recorded: self.binary.path.clone(), + found: path.to_string(), + }; + } + Verdict::Same + } + + /// Serialize: one `key = value` per line, sorted by key. + pub fn to_text(&self) -> String { + let mut lines: Vec<(String, String)> = vec![ + ("binary.path".into(), self.binary.path.clone()), + ("binary.size".into(), self.binary.size.to_string()), + ("binary.hash".into(), format!("{:016x}", self.binary.hash)), + ]; + if let Some(base) = self.load.base { + lines.push(("load.base".into(), format!("{:x}", base.get()))); + } + if let Some(arch) = &self.load.arch { + lines.push(("load.arch".into(), arch.name().to_string())); + } + for r in &self.load.readers { + lines.push(("load.reader".into(), r.clone())); + } + for (k, v) in &self.analysis { + lines.push((format!("analysis.{k}"), v.clone())); + } + if let Some(log) = &self.log { + lines.push(("log".into(), log.clone())); + } + for s in &self.signatures { + lines.push(("signatures".into(), s.clone())); + } + for p in &self.patches { + lines.push(("patches".into(), p.clone())); + } + // Stable sort on the key alone, so lists keep the order they were + // declared in while the file still reviews as a sorted diff. + lines.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut s = String::with_capacity(128 + lines.len() * 48); + let _ = writeln!(s, "{TAG} {VERSION}"); + let _ = writeln!( + s, + "# One key = value per line, sorted by key. Everything the session needs to" + ); + let _ = writeln!( + s, + "# reopen on the same binary; the annotations, signatures and patches live in" + ); + let _ = writeln!(s, "# the files named here."); + for (k, v) in lines { + let _ = writeln!(s, "{k} = {v}"); + } + s + } + + /// Parse a serialized project. + pub fn from_text(text: &str) -> Result { + let mut lines = text.lines(); + let header = lines.next().unwrap_or_default(); + let mut parts = header.split_whitespace(); + if parts.next() != Some(TAG) { + return Err(Error::NotRecognized { + expected: "e5r project", + }); + } + let version: u32 = parts + .next() + .and_then(|v| v.parse().ok()) + .ok_or(Error::BadField { + field: "format version", + value: 0, + reason: "is missing from the header line", + })?; + if version > VERSION { + return Err(Error::unsupported(format!( + "project format version {version}; this build knows {VERSION}" + ))); + } + + let mut p = Project::default(); + let mut seen = BTreeSet::new(); + for (n, line) in lines.enumerate() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let (key, value) = line + .split_once(" = ") + .ok_or_else(|| malformed(n, line, "is not `key = value`"))?; + let (key, value) = (key.trim(), value.trim()); + let number = |radix: u32| { + u64::from_str_radix(value, radix) + .map_err(|_| malformed(n, line, "does not hold a number")) + }; + match key { + "binary.path" => p.binary.path = value.to_string(), + "binary.size" => p.binary.size = number(10)?, + "binary.hash" => p.binary.hash = number(16)?, + "load.base" => p.load.base = Some(Addr(number(16)?)), + // An override names a decoder; anything the native names do + // not cover is a SLEIGH language id. + "load.arch" => { + p.load.arch = Some(value.parse().unwrap_or_else(|_| Arch::Sleigh(value.into()))) + } + "load.reader" => { + p.load.readers.insert(value.to_string()); + } + "log" => p.log = Some(value.to_string()), + "signatures" => p.signatures.push(value.to_string()), + "patches" => p.patches.push(value.to_string()), + other => match other.strip_prefix("analysis.") { + Some(opt) if !opt.is_empty() => { + p.analysis.insert(opt.to_string(), value.to_string()); + } + // A key nobody understands is a typo or a truncation, and + // dropping it would silently change the configuration. + _ => return Err(malformed(n, line, "is not a key this version knows")), + }, + } + seen.insert(key.to_string()); + } + for required in ["binary.path", "binary.size", "binary.hash"] { + if !seen.contains(required) { + return Err(Error::inconsistent(format!( + "project is missing {required}, so it cannot say what binary it is for" + ))); + } + } + Ok(p) + } +} + +fn malformed(n: usize, line: &str, why: &str) -> Error { + Error::inconsistent(format!("project line {} {why}: {line:?}", n + 2)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn project() -> Project { + let mut p = Project::for_binary("/srv/bin/target.elf", b"the binary's bytes"); + p.load.base = Some(Addr(0x40_0000)); + p.load.arch = Some(Arch::X86_64); + p.load.readers.insert("eh_frame".into()); + p.load.readers.insert("dwarf".into()); + p.analysis.insert("sweep".into(), "recursive".into()); + p.analysis.insert("follow-tail-calls".into(), "true".into()); + p.log = Some("target.e5r".into()); + p.signatures = vec!["libc.sig".into(), "openssl.sig".into()]; + p.patches = vec!["nop-check.e5r-patch".into()]; + p + } + + #[test] + fn the_text_form_round_trips() { + let p = project(); + let text = p.to_text(); + let back = Project::from_text(&text).unwrap(); + assert_eq!(back, p); + assert_eq!(back.to_text(), text); + } + + #[test] + fn lists_keep_their_order_and_keys_sort() { + let text = project().to_text(); + let keys: Vec<&str> = text + .lines() + .filter(|l| l.contains(" = ")) + .map(|l| l.split_once(" = ").unwrap().0) + .collect(); + let mut sorted = keys.clone(); + sorted.sort_unstable(); + assert_eq!(keys, sorted); + let sigs: Vec<&str> = text + .lines() + .filter_map(|l| l.strip_prefix("signatures = ")) + .collect(); + assert_eq!(sigs, ["libc.sig", "openssl.sig"]); + } + + #[test] + fn verify_tells_the_three_cases_apart() { + let p = project(); + assert_eq!(p.verify(b"the binary's bytes"), Verdict::Same); + assert!(matches!( + p.verify_at("/tmp/copy.elf", b"the binary's bytes"), + Verdict::Moved { .. } + )); + let rebuilt = p.verify(b"the binary's byteS"); + assert!(matches!(rebuilt, Verdict::Different { .. }), "{rebuilt}"); + assert!(!rebuilt.is_usable()); + assert!( + p.verify_at("/tmp/copy.elf", b"the binary's bytes") + .is_usable() + ); + } + + #[test] + fn a_same_length_edit_is_still_a_different_binary() { + // Size alone is what a cheap check would use, so it must not be all + // that is checked. + let p = Project::for_binary("a", b"aaaabbbb"); + assert!(matches!(p.verify(b"aaaabbbc"), Verdict::Different { .. })); + } + + #[test] + fn a_truncated_or_corrupt_file_is_an_error() { + assert!(Project::from_text("").is_err()); + assert!(Project::from_text("not a project\n").is_err()); + assert!(Project::from_text(&format!("{TAG} 99\n")).is_err()); + // Truncated before the binary is named. + let text = project().to_text(); + let head: Vec<&str> = text.lines().take(4).collect(); + assert!(Project::from_text(&head.join("\n")).is_err()); + // A field that is no longer a number. + let broken = text.replace("binary.size = 18", "binary.size = eighteen"); + let e = Project::from_text(&broken).unwrap_err().to_string(); + assert!(e.contains("number"), "{e}"); + // A key from a future version, which would otherwise be dropped. + assert!(Project::from_text(&format!("{text}load.wombat = 1\n")).is_err()); + } + + #[test] + fn a_sleigh_language_survives_the_round_trip() { + let mut p = Project::for_binary("a", b"a"); + p.load.arch = Some(Arch::Sleigh("MIPS:BE:32:default".into())); + let back = Project::from_text(&p.to_text()).unwrap(); + assert_eq!(back.load.arch, p.load.arch); + } + + #[test] + fn the_digest_is_platform_fixed() { + // A literal, so a change in the fold is a test failure rather than a + // silent invalidation of every project file in existence. + assert_eq!(digest(b"e5r"), 0x9ff5_21d2_3f42_96c2); + } +} diff --git a/crates/e5r-db/src/signature.rs b/crates/e5r-db/src/signature.rs new file mode 100644 index 0000000..7767cae --- /dev/null +++ b/crates/e5r-db/src/signature.rs @@ -0,0 +1,354 @@ +//! Identifying a function by what it is, not by where it is. +//! +//! A stripped binary still contains the same code the library did, so a +//! function's fingerprint identifies it across builds. The fingerprint is the +//! content anchor: a hash of the instruction shape stream with branch targets +//! excluded, which survives relinking, and a hash of the bytes, which does not +//! but is exact when it matches. +//! +//! The format is text and sorted, so a signature file is reviewable in a diff +//! and merges the way the annotation log does. A match reports which kind of +//! evidence it rested on, because an exact byte match and a shape match are +//! not the same claim. + +use std::collections::BTreeMap; +use std::fmt::Write as _; + +use crate::anchor::{Anchor, Resolution}; + +/// One function's fingerprint and what it is called. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Signature { + /// Hash of the instruction shapes, branch targets excluded. + pub shape: u64, + /// Hash of the bytes. + pub bytes: u64, + /// How many instructions it has. + pub insns: u32, + /// What it is called. + pub name: String, + /// Where the signature came from, for the record. + pub source: String, +} + +impl Signature { + /// A signature for an anchored function. + pub fn new(anchor: &Anchor, name: &str, source: &str) -> Signature { + Signature { + shape: anchor.shape, + bytes: anchor.bytes, + insns: anchor.insns, + name: name.to_string(), + source: source.to_string(), + } + } +} + +/// A set of signatures, indexed for lookup. +#[derive(Debug, Clone, Default)] +pub struct Library { + /// Every signature, sorted so the file is stable. + pub signatures: Vec, + /// True when a file was longer than the reader would accept and the rest + /// of it was dropped. The library is then a prefix of what the file said, + /// which a caller has to be told rather than left to assume. + pub truncated: bool, + by_bytes: BTreeMap>, + by_shape: BTreeMap>, +} + +/// What a match rested on. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Match<'a> { + /// The signature that matched. + pub signature: &'a Signature, + /// How it was resolved. + pub resolution: Resolution, +} + +impl Library { + /// Build from signatures, dropping shapes that name more than one thing. + pub fn build(mut signatures: Vec) -> Library { + signatures.sort(); + signatures.dedup(); + let mut by_bytes: BTreeMap> = BTreeMap::new(); + let mut by_shape: BTreeMap> = BTreeMap::new(); + for (n, s) in signatures.iter().enumerate() { + by_bytes.entry(s.bytes).or_default().push(n); + by_shape.entry(s.shape).or_default().push(n); + } + Library { + signatures, + truncated: false, + by_bytes, + by_shape, + } + } + + /// How many signatures there are. + pub fn len(&self) -> usize { + self.signatures.len() + } + + /// True when there are none. + pub fn is_empty(&self) -> bool { + self.signatures.is_empty() + } + + /// What a function is, if the library knows. + /// + /// An exact byte match is a fact. A shape match is a claim about a + /// function that was compiled from the same source and linked elsewhere, + /// and it is only made when exactly one signature has that shape and the + /// function is long enough for the shape to mean something. + pub fn identify(&self, anchor: &Anchor) -> Option> { + if let Some(signature) = self + .by_bytes + .get(&anchor.bytes) + .and_then(|c| self.agreed(c, anchor.insns)) + { + return Some(Match { + signature, + resolution: Resolution::Exact, + }); + } + // A short function's shape is shared by hundreds of others: three + // instructions of the same lengths and flow classes say nothing. + if anchor.insns < MIN_SHAPE_INSNS { + return None; + } + let signature = self + .by_shape + .get(&anchor.shape) + .and_then(|c| self.agreed(c, anchor.insns))?; + Some(Match { + signature, + resolution: Resolution::Shape, + }) + } + + /// The one thing a set of candidates agrees this is. + /// + /// Two library functions with the same bytes under different names is a + /// real thing — every import thunk in a binary differs only in the offset + /// it loads — and picking one of them would be a coin flip reported as a + /// fact. + fn agreed(&self, candidates: &[usize], insns: u32) -> Option<&Signature> { + let mut matching = candidates + .iter() + .filter_map(|n| self.signatures.get(*n)) + .filter(|s| s.insns == insns); + let first = matching.next()?; + matching + .all(|other| other.name == first.name) + .then_some(first) + } + + /// The text form: one signature per line, sorted. + pub fn to_text(&self) -> String { + let mut out = String::from("# e5r signatures v1\n"); + for s in &self.signatures { + let _ = writeln!( + out, + "{:016x} {:016x} {} {} {}", + s.shape, s.bytes, s.insns, s.name, s.source + ); + } + out + } + + /// Read the text form, ignoring lines it does not understand. + /// + /// A signature library is a file somebody else wrote: it travels between + /// machines, it is checked into other people's repositories, and it is the + /// output of a script that ran over packages nobody here chose. So an + /// unreadable line is skipped rather than fatal, and the length of the + /// file is bounded before the result is built. + pub fn from_text(text: &str) -> Library { + Library::from_text_capped(text, MAX_SIGNATURES) + } + + /// The same, with the ceiling given explicitly. + /// + /// The text form carries no count for a hostile file to lie about, so the + /// bound has to be on the result instead: reading stops at `max` and says + /// it stopped, rather than growing until the allocator decides. + pub fn from_text_capped(text: &str, max: usize) -> Library { + let mut signatures = Vec::new(); + let mut truncated = false; + for line in text.lines() { + if signatures.len() >= max { + truncated = true; + break; + } + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let mut parts = line.splitn(5, ' '); + let (Some(shape), Some(bytes), Some(insns), Some(name)) = + (parts.next(), parts.next(), parts.next(), parts.next()) + else { + continue; + }; + let (Ok(shape), Ok(bytes), Ok(insns)) = ( + u64::from_str_radix(shape, 16), + u64::from_str_radix(bytes, 16), + insns.parse::(), + ) else { + continue; + }; + signatures.push(Signature { + shape, + bytes, + insns, + name: name.to_string(), + source: parts.next().unwrap_or("").to_string(), + }); + } + let mut library = Library::build(signatures); + library.truncated = truncated; + library + } + + /// Merge another library in. + pub fn merge(&mut self, other: Library) { + let truncated = self.truncated || other.truncated; + let mut all = std::mem::take(&mut self.signatures); + all.extend(other.signatures); + *self = Library::build(all); + self.truncated = truncated; + } +} + +/// Below this many instructions a shape match means nothing. +const MIN_SHAPE_INSNS: u32 = 24; + +/// The most signatures one file may contribute. +/// +/// Two million is more than every static archive a distribution ships, and it +/// is a number rather than an allocator failure. +pub const MAX_SIGNATURES: usize = 2_000_000; + +#[cfg(test)] +mod tests { + use super::*; + use e5r_core::Addr; + + fn anchor(shape: u64, bytes: u64, insns: u32) -> Anchor { + Anchor { + shape, + bytes, + insns, + abs: Addr(0x1000), + offset: 0, + } + } + + #[test] + fn an_exact_byte_match_is_reported_as_exact() { + let library = Library::build(vec![Signature { + shape: 1, + bytes: 2, + insns: 20, + name: "memcpy".into(), + source: "libc".into(), + }]); + let found = library.identify(&anchor(1, 2, 20)).expect("a match"); + assert_eq!(found.signature.name, "memcpy"); + assert_eq!(found.resolution, Resolution::Exact); + } + + #[test] + fn a_relinked_function_still_matches_by_shape() { + let library = Library::build(vec![Signature { + shape: 1, + bytes: 2, + insns: 40, + name: "memcpy".into(), + source: "libc".into(), + }]); + // Same shape, different bytes: the branch targets moved. + let found = library.identify(&anchor(1, 99, 40)).expect("a match"); + assert_eq!(found.resolution, Resolution::Shape); + } + + #[test] + fn two_names_for_the_same_bytes_name_nothing() { + let library = Library::build(vec![ + Signature { + shape: 1, + bytes: 2, + insns: 20, + name: "one@plt".into(), + source: "a".into(), + }, + Signature { + shape: 1, + bytes: 2, + insns: 20, + name: "another@plt".into(), + source: "b".into(), + }, + ]); + assert!(library.identify(&anchor(1, 2, 20)).is_none()); + } + + #[test] + fn an_ambiguous_shape_names_nothing() { + let library = Library::build(vec![ + Signature { + shape: 1, + bytes: 2, + insns: 40, + name: "memcpy".into(), + source: "libc".into(), + }, + Signature { + shape: 1, + bytes: 3, + insns: 40, + name: "memmove".into(), + source: "libc".into(), + }, + ]); + assert!(library.identify(&anchor(1, 99, 40)).is_none()); + } + + #[test] + fn a_short_function_is_not_identified_by_shape_alone() { + let library = Library::build(vec![Signature { + shape: 1, + bytes: 2, + insns: 4, + name: "stub".into(), + source: "libc".into(), + }]); + assert!(library.identify(&anchor(1, 99, 4)).is_none()); + } + + #[test] + fn the_text_form_round_trips() { + let library = Library::build(vec![ + Signature { + shape: 0xdead, + bytes: 0xbeef, + insns: 20, + name: "memcpy".into(), + source: "libc.so.6".into(), + }, + Signature { + shape: 1, + bytes: 2, + insns: 30, + name: "strlen".into(), + source: "libc.so.6".into(), + }, + ]); + let text = library.to_text(); + let back = Library::from_text(&text); + assert_eq!(back.signatures, library.signatures); + // Sorted, so two runs produce the same file. + assert_eq!(back.to_text(), text); + } +} diff --git a/crates/e5r-db/tests/fuzz.rs b/crates/e5r-db/tests/fuzz.rs new file mode 100644 index 0000000..84a6860 --- /dev/null +++ b/crates/e5r-db/tests/fuzz.rs @@ -0,0 +1,245 @@ +//! Mutation fuzzing of the signature reader, in the ordinary test run. +//! +//! A signature library is the one file in this crate that somebody else wrote. +//! It is produced by a script run over packages nobody here chose, it travels +//! between machines, and it gets checked into other people's repositories, so +//! the reader has to treat it the way the loaders treat a binary: any bytes in, +//! a value or a skipped line out, never a panic, never a hang, and never an +//! allocation the file gets to choose the size of. + +use std::time::{Duration, Instant}; + +use e5r_db::signature::{Library, MAX_SIGNATURES, Signature}; + +/// How long each target gets. Enough to find a systematic fault, short enough +/// that nobody is tempted to skip the suite. +const BUDGET: Duration = Duration::from_millis(1200); + +/// A small deterministic generator, so a failure is reproducible from its seed. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545_f491_4f6c_dd1d) + } + + fn below(&mut self, n: usize) -> usize { + if n == 0 { + 0 + } else { + (self.next() % n as u64) as usize + } + } +} + +/// A real library to mutate, with the shapes a written one actually has. +fn seed() -> String { + let mut v = Vec::new(); + for n in 0..200u64 { + v.push(Signature { + shape: n.wrapping_mul(0x9e37_79b9_7f4a_7c15), + bytes: n.wrapping_mul(0xc2b2_ae3d_27d4_eb4f), + insns: 12 + (n as u32 % 90), + name: format!("_ZN4libc7routine{n}Ev"), + source: format!("libc.a(part{}.o)", n % 17), + }); + } + Library::build(v).to_text() +} + +/// Corrupt a buffer in the ways that actually break a text parser. +fn mutate(seed: &[u8], rng: &mut Rng) -> Vec { + let mut v = seed.to_vec(); + if v.is_empty() { + return v; + } + match rng.below(7) { + // Truncate, including in the middle of a multi-byte character. + 0 => v.truncate(rng.below(v.len())), + // Flip a bit, which is how a digit becomes a letter. + 1 => { + let i = rng.below(v.len()); + v[i] ^= 1 << rng.below(8); + } + // Delete the separators, so every field runs into the next. + 2 => v.retain(|b| *b != b' '), + // Delete the line breaks, so the whole file is one enormous line. + 3 => v.retain(|b| *b != b'\n'), + // A number far past what the field can hold. + 4 => { + let i = rng.below(v.len()); + let end = (i + 24).min(v.len()); + for b in &mut v[i..end] { + *b = b'9'; + } + } + // Arbitrary bytes in the middle, including invalid UTF-8 and NULs. + 5 => { + let i = rng.below(v.len()); + let end = (i + rng.below(32)).min(v.len()); + for b in &mut v[i..end] { + *b = (rng.next() & 0xff) as u8; + } + } + // Repeat a chunk, so the file claims the same name for many shapes. + _ => { + let from = rng.below(v.len()); + let len = rng.below(256).min(v.len() - from); + let chunk: Vec = v[from..from + len].to_vec(); + for _ in 0..rng.below(8) { + v.extend_from_slice(&chunk); + } + } + } + v +} + +#[test] +fn the_signature_reader_survives_mutated_files() { + let base = seed(); + let mut rng = Rng(0x5eed_51c0_0000_0001); + let start = Instant::now(); + let mut n = 0u64; + while start.elapsed() < BUDGET { + let case = mutate(base.as_bytes(), &mut rng); + // Not every mutation leaves valid UTF-8, which is itself a case the + // caller has to handle rather than a reason not to test the reader. + let text = String::from_utf8_lossy(&case); + let library = Library::from_text(&text); + // Whatever came back has to be usable: every index the reader built + // has to still address the list it indexes, which a lookup for each + // signature is what checks. + for s in &library.signatures { + let _ = library.identify(&anchor(s)); + } + n += 1; + } + assert!( + n > 50, + "only {n} cases in {BUDGET:?}; something is very slow" + ); + println!("signature fuzz: {n} mutated cases, no panics"); +} + +#[test] +fn arbitrary_text_is_not_a_signature_library() { + let mut rng = Rng(0x5eed_51c0_0000_0002); + let start = Instant::now(); + let mut n = 0u64; + while start.elapsed() < BUDGET { + let len = rng.below(4096); + let mut s = String::with_capacity(len); + while s.len() < len { + match rng.below(6) { + 0 => s.push('\n'), + 1 => s.push(' '), + 2 => s.push('#'), + 3 => s.push_str(&format!("{:x}", rng.next())), + 4 => s.push_str(&rng.next().to_string()), + _ => s.push(char::from_u32((rng.next() % 0x2000) as u32).unwrap_or('?')), + } + } + let _ = Library::from_text(&s); + n += 1; + } + assert!(n > 50, "only {n} cases in {BUDGET:?}"); + println!("signature fuzz: {n} random cases, no panics"); +} + +#[test] +fn a_field_that_cannot_fit_is_skipped_rather_than_wrapped() { + // Each of these is one plausible-looking line with one impossible field. + // Skipping the line is the answer; wrapping the value would put a + // fabricated fingerprint in the library, which is worse than losing it. + let text = concat!( + "# e5r signatures v1\n", + "ffffffffffffffffff 0000000000000002 20 too_wide_shape libc.a\n", + "0000000000000001 zzzzzzzzzzzzzzzz 20 not_hex libc.a\n", + "0000000000000001 0000000000000002 99999999999 too_many_insns libc.a\n", + "0000000000000001 0000000000000002 -3 negative_insns libc.a\n", + "0000000000000001 0000000000000002 20\n", + "0000000000000003 0000000000000004 20 fine libc.a\n", + ); + let library = Library::from_text(text); + assert_eq!(library.len(), 1); + assert_eq!(library.signatures[0].name, "fine"); + assert!(!library.truncated); +} + +#[test] +fn a_file_longer_than_the_cap_stops_at_a_number() { + // The text form carries no count for a hostile file to lie about, so the + // bound is on the result. It has to be reported, because a silently + // truncated library would answer "not found" for real code. + let mut text = String::from("# e5r signatures v1\n"); + for n in 0..64u64 { + text.push_str(&format!("{n:016x} {n:016x} 30 name{n} src\n")); + } + let full = Library::from_text_capped(&text, 1000); + assert_eq!(full.len(), 64); + assert!(!full.truncated); + + let capped = Library::from_text_capped(&text, 10); + assert_eq!(capped.len(), 10); + assert!(capped.truncated, "a truncated library did not say so"); + // And the ceiling the default reads with is one a real library lives + // under: every static archive a distribution ships, together, is short of + // it, so an ordinary file comes back whole. + let default = Library::from_text_capped(&text, MAX_SIGNATURES); + assert_eq!(default.len(), 64); + assert!(!default.truncated); +} + +#[test] +fn a_file_that_is_one_enormous_line_is_read_in_bounded_time() { + // The reader splits on line breaks; a file with none of them is one field + // a megabyte long, and it must not become quadratic in that length. + let mut text = String::from("0000000000000001 0000000000000002 20 "); + text.push_str(&"a".repeat(4 * 1024 * 1024)); + let started = Instant::now(); + let library = Library::from_text(&text); + assert_eq!(library.len(), 1); + assert!( + started.elapsed() < Duration::from_secs(2), + "one long line took {:?}", + started.elapsed() + ); +} + +#[test] +fn a_library_full_of_one_name_still_refuses_the_coin_flip() { + // The shape a repeated chunk produces: many signatures, disagreeing. The + // reader must not turn that into a confident answer downstream. + let mut text = String::from("# e5r signatures v1\n"); + for n in 0..500u64 { + text.push_str(&format!( + "0000000000000001 0000000000000002 40 name{n} src\n" + )); + } + let library = Library::from_text(&text); + assert_eq!(library.len(), 500); + let a = e5r_db::Anchor { + shape: 1, + bytes: 2, + insns: 40, + abs: e5r_core::Addr(0x1000), + offset: 0, + }; + assert!(library.identify(&a).is_none(), "500 names named one thing"); +} + +/// The anchor a signature describes, for feeding the matcher its own output. +fn anchor(s: &Signature) -> e5r_db::Anchor { + e5r_db::Anchor { + shape: s.shape, + bytes: s.bytes, + insns: s.insns, + abs: e5r_core::Addr(0x1000), + offset: 0, + } +} diff --git a/crates/e5r-db/tests/git_merge.rs b/crates/e5r-db/tests/git_merge.rs new file mode 100644 index 0000000..945f03c --- /dev/null +++ b/crates/e5r-db/tests/git_merge.rs @@ -0,0 +1,212 @@ +//! G9: two analysts on separate branches merge with `git merge` alone. +//! +//! This is the claim the whole storage design exists to make, so it is tested +//! against real git rather than against a model of it. Skipped when git is not +//! installed. + +use std::path::Path; +use std::process::Command; + +use e5r_core::Addr; +use e5r_db::{Anchor, Field, Log}; + +fn git(dir: &Path, args: &[&str]) -> Option { + let out = Command::new("git") + .args(args) + .current_dir(dir) + .env("GIT_AUTHOR_NAME", "test") + .env("GIT_AUTHOR_EMAIL", "test@example.invalid") + .env("GIT_COMMITTER_NAME", "test") + .env("GIT_COMMITTER_EMAIL", "test@example.invalid") + .output() + .ok()?; + out.status + .success() + .then(|| String::from_utf8_lossy(&out.stdout).into_owned()) +} + +fn anchor(n: u64) -> Anchor { + Anchor { + shape: n.wrapping_mul(0x9e37_79b9_7f4a_7c15), + bytes: n.wrapping_mul(0xc2b2_ae3d_27d4_eb4f), + insns: 8, + abs: Addr(0x400000 + n * 0x40), + offset: 0, + } +} + +/// A repo whose annotation files use git's union merge driver, which keeps +/// both sides of a conflicting hunk. For a log whose fold ignores order, the +/// union of two branches is the correct merge. +fn setup(dir: &Path) -> Option<()> { + git(dir, &["init", "-q", "-b", "main"])?; + std::fs::write(dir.join(".gitattributes"), "*.e5r merge=union\n").ok()?; + Some(()) +} + +#[test] +fn two_branches_merge_without_conflict_markers() { + let tmp = std::env::temp_dir().join(format!("e5r-merge-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&tmp); + std::fs::create_dir_all(&tmp).unwrap(); + if setup(&tmp).is_none() { + return; // no git here + } + let file = tmp.join("bin.e5r"); + + // A shared starting point: both analysts pull the same file. + let mut base = Log::new(); + base.binary = Some("sha256:shared".into()); + for i in 0..20 { + base.assert(anchor(i), Field::Name, Some(format!("known_{i}")), "base"); + } + std::fs::write(&file, base.to_text()).unwrap(); + git(&tmp, &["add", "."]).unwrap(); + git(&tmp, &["commit", "-qm", "base"]).unwrap(); + + // Alice names twenty more functions on her branch. + git(&tmp, &["checkout", "-qb", "alice"]).unwrap(); + let mut alice = base.clone(); + for i in 100..120 { + alice.assert(anchor(i), Field::Name, Some(format!("alice_{i}")), "alice"); + alice.assert( + anchor(i), + Field::Comment, + Some(format!("note {i}")), + "alice", + ); + } + std::fs::write(&file, alice.to_text()).unwrap(); + git(&tmp, &["commit", "-qam", "alice"]).unwrap(); + + // Bob does the same on his, from the same base. + git(&tmp, &["checkout", "-q", "main"]).unwrap(); + git(&tmp, &["checkout", "-qb", "bob"]).unwrap(); + let mut bob = base.clone(); + for i in 200..220 { + bob.assert(anchor(i), Field::Name, Some(format!("bob_{i}")), "bob"); + bob.assert( + anchor(i), + Field::Type, + Some(format!("int f{i}(void)")), + "bob", + ); + } + std::fs::write(&file, bob.to_text()).unwrap(); + git(&tmp, &["commit", "-qam", "bob"]).unwrap(); + + // git merges them with no help from us. + git(&tmp, &["checkout", "-q", "alice"]).unwrap(); + let merged = git(&tmp, &["merge", "--no-edit", "bob"]); + let text = std::fs::read_to_string(&file).unwrap(); + assert!( + merged.is_some(), + "git could not merge the two branches:\n{text}" + ); + assert!( + !text.contains("<<<<<<<"), + "the merge left conflict markers:\n{text}" + ); + + // And the merged file says what both analysts wrote. + let log = Log::from_text(&text).unwrap(); + let folded = log.fold(); + let values: Vec<&str> = folded.values().filter_map(|a| a.value.as_deref()).collect(); + assert_eq!(folded.len(), 20 + 40 + 40, "lost assertions in the merge"); + // Union merge can repeat a line; reading must not double-count it. + assert_eq!( + log.records().len(), + log.fold().len(), + "duplicates survived the read" + ); + assert!(values.contains(&"alice_100")); + assert!(values.contains(&"bob_200")); + assert!(values.contains(&"known_0")); + + let _ = std::fs::remove_dir_all(&tmp); +} + +#[test] +fn editing_the_same_function_resolves_rather_than_conflicting() { + let tmp = std::env::temp_dir().join(format!("e5r-same-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&tmp); + std::fs::create_dir_all(&tmp).unwrap(); + if setup(&tmp).is_none() { + return; + } + let file = tmp.join("bin.e5r"); + + let mut base = Log::new(); + for i in 0..10 { + base.assert(anchor(i), Field::Name, Some(format!("f{i}")), "base"); + } + std::fs::write(&file, base.to_text()).unwrap(); + git(&tmp, &["add", "."]).unwrap(); + git(&tmp, &["commit", "-qm", "base"]).unwrap(); + + // Both analysts rename the same function, differently. + git(&tmp, &["checkout", "-qb", "alice"]).unwrap(); + let mut alice = base.clone(); + alice.assert(anchor(3), Field::Name, Some("parse_header".into()), "alice"); + std::fs::write(&file, alice.to_text()).unwrap(); + git(&tmp, &["commit", "-qam", "alice"]).unwrap(); + + git(&tmp, &["checkout", "-q", "main"]).unwrap(); + git(&tmp, &["checkout", "-qb", "bob"]).unwrap(); + let mut bob = base.clone(); + bob.assert(anchor(3), Field::Name, Some("read_header".into()), "bob"); + std::fs::write(&file, bob.to_text()).unwrap(); + git(&tmp, &["commit", "-qam", "bob"]).unwrap(); + + git(&tmp, &["checkout", "-q", "alice"]).unwrap(); + let merged = git(&tmp, &["merge", "--no-edit", "bob"]); + let text = std::fs::read_to_string(&file).unwrap(); + + // Two edits to the same field are distinct lines, so git still merges. + // The fold picks one deterministically and keeps the other as history. + if merged.is_some() { + assert!(!text.contains("<<<<<<<"), "{text}"); + let log = Log::from_text(&text).unwrap(); + let winner = log + .fold() + .values() + .find(|a| a.target.shape == anchor(3).shape && a.field == Field::Name) + .and_then(|a| a.value.clone()) + .unwrap(); + assert!( + winner == "parse_header" || winner == "read_header", + "unexpected winner {winner:?}" + ); + // Both are still recorded, so the losing name is recoverable. + assert!(text.contains("parse_header")); + assert!(text.contains("read_header")); + } + + let _ = std::fs::remove_dir_all(&tmp); +} + +#[test] +fn the_file_survives_a_round_trip_through_git() { + let tmp = std::env::temp_dir().join(format!("e5r-rt-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&tmp); + std::fs::create_dir_all(&tmp).unwrap(); + if setup(&tmp).is_none() { + return; + } + let file = tmp.join("bin.e5r"); + let mut log = Log::new(); + log.assert( + anchor(1), + Field::Comment, + Some("a comment with \"quotes\", a\ttab and a\nnewline".into()), + "alice", + ); + std::fs::write(&file, log.to_text()).unwrap(); + git(&tmp, &["add", "."]).unwrap(); + git(&tmp, &["commit", "-qm", "one"]).unwrap(); + git(&tmp, &["checkout", "-q", "--", "bin.e5r"]).unwrap(); + + let back = Log::from_text(&std::fs::read_to_string(&file).unwrap()).unwrap(); + assert_eq!(back.records(), log.records()); + let _ = std::fs::remove_dir_all(&tmp); +} diff --git a/crates/e5r-db/tests/patch.rs b/crates/e5r-db/tests/patch.rs new file mode 100644 index 0000000..3686b9b --- /dev/null +++ b/crates/e5r-db/tests/patch.rs @@ -0,0 +1,281 @@ +//! Patch sets against random images and against real binaries. +//! +//! The claim a patch set makes is that it either lands whole or does nothing, +//! and that what it did can be undone byte for byte. Neither is provable from +//! a handful of examples, so the round trip is checked over a few hundred +//! generated patch sets, with a deterministic generator so a failure is +//! reproducible from its seed. + +use std::path::{Path, PathBuf}; + +use e5r_core::Addr; +use e5r_db::anchor::{Anchor, AnchorIndex, Resolution}; +use e5r_db::patch::{Conflict, Edit, Patch}; + +/// A small deterministic generator, so a failure is reproducible from its seed. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + // xorshift64*, chosen for being four lines and reproducible. + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545_f491_4f6c_dd1d) + } + + fn below(&mut self, n: usize) -> usize { + if n == 0 { + 0 + } else { + (self.next() % n as u64) as usize + } + } + + fn bytes(&mut self, n: usize) -> Vec { + (0..n).map(|_| self.next() as u8).collect() + } +} + +/// The address the generated images are loaded at. +const BASE: Addr = Addr(0x40_0000); + +/// A patch set of non-overlapping edits captured from `image`. +fn generate(rng: &mut Rng, image: &[u8]) -> Patch { + let mut patch = Patch::new("generated"); + let mut at = 0usize; + loop { + at += 1 + rng.below(16); + let len = 1 + rng.below(8); + if at + len > image.len() { + return patch; + } + let addr = BASE.wrapping_offset(at as i64); + // Half anchored by content, half by address alone: both have to work + // when no index is supplied. + let target = if rng.next() & 1 == 0 { + Anchor { + shape: rng.next(), + bytes: rng.next(), + insns: 4, + abs: addr, + offset: 0, + } + } else { + Anchor { + shape: 0, + bytes: 0, + insns: 0, + abs: addr, + offset: 0, + } + }; + patch + .record(target, image, BASE, rng.bytes(len)) + .expect("the edit is inside the image"); + at += len; + } +} + +#[test] +fn applying_then_reverting_restores_every_byte() { + let mut rng = Rng(0x1234_5678_9abc_def0); + for _ in 0..300 { + let len = 64 + rng.below(960); + let original = rng.bytes(len); + let patch = generate(&mut rng, &original); + let mut image = original.clone(); + let applied = patch.apply(&mut image, BASE, None).expect("it applies"); + assert_eq!(applied.changes.len(), patch.len()); + assert_eq!( + applied.bytes, + patch.edits().iter().map(|e| e.expect.len()).sum::() + ); + // Every edit is on the image, and the preview said so in advance. + for c in &applied.changes { + let off = (c.addr.get() - BASE.get()) as usize; + assert_eq!(&image[off..off + c.after.len()], &c.after[..]); + assert_eq!(&original[off..off + c.before.len()], &c.before[..]); + } + patch + .revert() + .apply(&mut image, BASE, None) + .expect("the inverse applies"); + assert_eq!(image, original); + } +} + +#[test] +fn a_patch_made_against_other_bytes_writes_nothing() { + let mut rng = Rng(0xfeed_face_dead_beef); + let mut checked = 0; + for _ in 0..300 { + let len = 64 + rng.below(960); + let original = rng.bytes(len); + let patch = generate(&mut rng, &original); + if patch.is_empty() { + continue; + } + // Corrupt one byte an edit covers, the way a rebuilt binary would. + let victim = &patch.edits()[rng.below(patch.len())]; + let off = (victim.addr().get() - BASE.get()) as usize + rng.below(victim.expect.len()); + let mut image = original.clone(); + image[off] ^= 0xff; + let corrupt = image.clone(); + + let e = patch + .apply(&mut image, BASE, None) + .expect_err("the expected bytes are gone"); + assert!(matches!(e, Conflict::Mismatch { .. }), "{e}"); + assert_eq!(image, corrupt, "a failed apply wrote something"); + checked += 1; + } + assert!(checked > 200, "only {checked} sets had an edit to corrupt"); +} + +#[test] +fn the_text_form_round_trips_over_generated_sets() { + let mut rng = Rng(0x0bad_c0de_0bad_c0de); + for _ in 0..300 { + let len = 64 + rng.below(256); + let image = rng.bytes(len); + let mut patch = generate(&mut rng, &image); + patch.binary = Some(format!("fnv:{:016x}", rng.next())); + let text = patch.to_text(); + let back = Patch::from_text(&text).expect("it parses"); + assert_eq!(back.edits(), patch.edits()); + assert_eq!(back.binary, patch.binary); + assert_eq!(back.to_text(), text); + } +} + +/// A four-instruction function whose third word is a branch, so a relink +/// changes the bytes without changing the shape. +fn function(base: u64, displacement: u32) -> (Anchor, Vec) { + let words = [ + 0xd280_0020u32, // mov x0, #1 + 0xf100_041f, // cmp x0, #1 + 0x9400_0000 | displacement, // bl + 0xd65f_03c0, // ret + ]; + let body: Vec = words.iter().flat_map(|w| w.to_le_bytes()).collect(); + let insns: Vec = words + .iter() + .enumerate() + .filter_map(|(n, w)| e5r_arch::aarch64::decode_word(*w, Addr(base + n as u64 * 4))) + .collect(); + assert_eq!(insns.len(), 4, "the fixture words all decode"); + (Anchor::function(Addr(base), &insns, &body), body) +} + +#[test] +fn an_anchored_edit_survives_a_relink() { + // The patch was made against the function at 0x1000; the rebuilt binary + // has it at 0x9000 with a different call displacement, so its bytes + // fingerprint differs and only the shape can find it. + let (before, _) = function(0x1000, 4); + let (after, mut image) = function(0x9000, 9); + assert_eq!(before.shape, after.shape); + assert_ne!(before.bytes, after.bytes); + + let mut patch = Patch::new("nop the compare"); + patch.push( + Edit::new( + before.at_offset(4), + 0xf100_041fu32.to_le_bytes().to_vec(), + 0xd503_201fu32.to_le_bytes().to_vec(), + ) + .unwrap() + .by("alice") + .noted("the length check"), + ); + + // Without an index the edit falls back to the address it was recorded at, + // which is not in this image at all. + let e = patch + .apply(&mut image.clone(), Addr(0x9000), None) + .expect_err("0x1004 is not in the relinked image"); + assert!(matches!(e, Conflict::OutOfRange { .. }), "{e}"); + + let index = AnchorIndex::build([after]); + let applied = patch + .apply(&mut image, Addr(0x9000), Some(&index)) + .expect("the shape resolves"); + assert_eq!(applied.changes[0].addr, Addr(0x9004)); + assert_eq!(applied.changes[0].resolution, Resolution::Shape); + assert!(applied.is_confident()); + assert_eq!(&image[4..8], &0xd503_201fu32.to_le_bytes()); +} + +#[test] +fn the_resolution_says_when_only_the_address_matched() { + let (before, image) = function(0x1000, 4); + // An index that knows nothing about this function. + let (other, _) = function(0x5000, 0x20); + let mut index_anchor = other; + index_anchor.shape ^= 0xdead; + let index = AnchorIndex::build([index_anchor]); + + let mut patch = Patch::new("p"); + patch.push( + Edit::new( + before.at_offset(12), + 0xd65f_03c0u32.to_le_bytes().to_vec(), + 0xd503_201fu32.to_le_bytes().to_vec(), + ) + .unwrap(), + ); + let changes = patch + .preview(&image, Addr(0x1000), Some(&index)) + .expect("it still lands, on the address alone"); + assert_eq!(changes[0].resolution, Resolution::Address); + assert!(!changes[0].resolution.is_confident()); +} + +/// A real binary, or `None` when the fixtures have not been built. +fn fixture() -> Option<(PathBuf, Vec)> { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + let mut files: Vec = std::fs::read_dir(&dir) + .ok()? + .flatten() + .map(|e| e.path()) + .filter(|p| p.is_file()) + .collect(); + files.sort(); + let path = files + .into_iter() + .find(|p| std::fs::read(p).is_ok_and(|d| d.starts_with(b"\x7fELF") && d.len() > 0x400))?; + let data = std::fs::read(&path).ok()?; + Some((path, data)) +} + +#[test] +fn a_real_binary_is_patched_and_restored() { + let Some((path, original)) = fixture() else { + // Fixtures are built on demand; their absence is not a failure. + return; + }; + let mut rng = Rng(0x5eed_0000_0000_0001); + let mut image = original.clone(); + let patch = generate(&mut rng, &original); + assert!(!patch.is_empty(), "{} produced no edits", path.display()); + + let applied = patch.apply(&mut image, BASE, None).expect("it applies"); + assert!(applied.bytes > 0); + assert_ne!(image, original); + + // The same patch against the file it has already changed is a conflict, + // which is what stops a patch being applied twice. + let e = patch + .apply(&mut image.clone(), BASE, None) + .expect_err("the expected bytes are gone"); + assert!(matches!(e, Conflict::Mismatch { .. }), "{e}"); + + patch + .revert() + .apply(&mut image, BASE, None) + .expect("the inverse applies"); + assert_eq!(image, original); +} diff --git a/crates/e5r-db/tests/project.rs b/crates/e5r-db/tests/project.rs new file mode 100644 index 0000000..1d6b7d7 --- /dev/null +++ b/crates/e5r-db/tests/project.rs @@ -0,0 +1,223 @@ +//! Project files: the verdict on a binary, and a parser that never panics. +//! +//! A project file is the one file a session starts from, so it is also the one +//! most likely to be hand-edited, truncated by a full disk, or merged badly. A +//! reader that panics on any of that takes the session with it, so the parser +//! is checked against mutated and truncated copies of a good file rather than +//! against a good file alone. + +use std::path::{Path, PathBuf}; + +use e5r_core::{Addr, Arch}; +use e5r_db::patch::Patch; +use e5r_db::project::{Project, Verdict, digest}; + +/// A small deterministic generator, so a failure is reproducible from its seed. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + // xorshift64*, chosen for being four lines and reproducible. + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545_f491_4f6c_dd1d) + } + + fn below(&mut self, n: usize) -> usize { + if n == 0 { + 0 + } else { + (self.next() % n as u64) as usize + } + } +} + +fn session(path: &str, data: &[u8]) -> Project { + let mut p = Project::for_binary(path, data); + p.load.base = Some(Addr(0x40_0000)); + p.load.arch = Some(Arch::AArch64); + p.load.readers.insert("eh_frame".into()); + p.load.readers.insert("dwarf".into()); + p.load.readers.insert("go".into()); + p.analysis.insert("sweep".into(), "recursive".into()); + p.analysis.insert("jump-tables".into(), "on".into()); + p.log = Some("notes.e5r".into()); + p.signatures = vec!["libc.sig".into(), "openssl.sig".into()]; + p.patches = vec!["nop-check.e5r-patch".into()]; + p +} + +#[test] +fn a_session_round_trips_through_the_file() { + let p = session("/srv/bin/target", b"bytes of a binary"); + let text = p.to_text(); + let back = Project::from_text(&text).expect("it parses"); + assert_eq!(back, p); + assert_eq!(back.to_text(), text); + // Every reference survives, because losing one silently drops an + // analyst's work out of the session. + assert_eq!(back.log.as_deref(), Some("notes.e5r")); + assert_eq!(back.signatures.len(), 2); + assert_eq!(back.patches, ["nop-check.e5r-patch"]); + assert_eq!(back.load.readers.len(), 3); +} + +#[test] +fn the_three_verdicts_are_distinguishable() { + let data = b"bytes of a binary".to_vec(); + let p = session("/srv/bin/target", &data); + assert_eq!(p.verify(&data), Verdict::Same); + assert_eq!(p.verify_at("/srv/bin/target", &data), Verdict::Same); + + let moved = p.verify_at("/home/analyst/target", &data); + assert!(matches!(moved, Verdict::Moved { .. }), "{moved}"); + assert!(moved.is_usable()); + if let Verdict::Moved { recorded, found } = moved { + assert_eq!(recorded, "/srv/bin/target"); + assert_eq!(found, "/home/analyst/target"); + } + + let mut rebuilt = data.clone(); + rebuilt[3] ^= 1; + let verdict = p.verify(&rebuilt); + assert!(matches!(verdict, Verdict::Different { .. }), "{verdict}"); + assert!(!verdict.is_usable()); + // A rebuild that happens to keep the size is still a different binary. + assert_eq!(rebuilt.len(), data.len()); + // And so is a truncation. + assert!(!p.verify(&data[..data.len() - 1]).is_usable()); +} + +#[test] +fn a_mutated_file_errors_rather_than_panicking() { + let good = session("/srv/bin/target", b"bytes of a binary").to_text(); + let mut rng = Rng(0xc0ff_ee00_0000_0001); + let mut parsed = 0; + for _ in 0..600 { + let mut bytes = good.clone().into_bytes(); + match rng.below(3) { + // Truncate anywhere, including mid-line and mid-header. + 0 => bytes.truncate(rng.below(good.len())), + // Flip a byte, which breaks keys, numbers and the header alike. + 1 => { + let at = rng.below(bytes.len()); + bytes[at] ^= 1 << rng.below(7); + } + // Drop a line, the way a bad merge resolution does. + _ => { + let keep: Vec<&str> = good + .lines() + .enumerate() + .filter(|(n, _)| *n != rng.below(good.lines().count())) + .map(|(_, l)| l) + .collect(); + bytes = keep.join("\n").into_bytes(); + } + } + let text = String::from_utf8_lossy(&bytes).into_owned(); + if let Ok(p) = Project::from_text(&text) { + // Whatever survived must still be a project that writes and reads + // back as itself. + assert_eq!(Project::from_text(&p.to_text()).unwrap(), p); + parsed += 1; + } + } + assert!( + parsed > 0, + "every mutation was rejected, so nothing was read" + ); +} + +#[test] +fn a_project_without_a_binary_is_refused() { + // The one thing it cannot do without: a project that cannot name its + // binary cannot check it either. + let text = "e5r-project 1\nlog = notes.e5r\n"; + let e = Project::from_text(text).unwrap_err().to_string(); + assert!(e.contains("binary"), "{e}"); +} + +#[test] +fn the_digest_separates_files_of_the_same_length() { + let mut rng = Rng(0xabcd_ef01_2345_6789); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..2000 { + let data: Vec = (0..32).map(|_| rng.next() as u8).collect(); + seen.insert(digest(&data)); + } + assert_eq!( + seen.len(), + 2000, + "the digest collided on random 32-byte files" + ); +} + +/// A real binary, or `None` when the fixtures have not been built. +fn fixture() -> Option<(PathBuf, Vec)> { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + let mut files: Vec = std::fs::read_dir(&dir) + .ok()? + .flatten() + .map(|e| e.path()) + .filter(|p| p.is_file()) + .collect(); + files.sort(); + let path = files + .into_iter() + .find(|p| std::fs::read(p).is_ok_and(|d| d.starts_with(b"\x7fELF")))?; + let data = std::fs::read(&path).ok()?; + Some((path, data)) +} + +#[test] +fn a_project_for_a_real_binary_recognizes_it() { + let Some((path, data)) = fixture() else { + // Fixtures are built on demand; their absence is not a failure. + return; + }; + let name = path.to_string_lossy().into_owned(); + let mut p = Project::for_binary(&name, &data); + assert_eq!(p.binary.size, data.len() as u64); + assert_eq!(p.verify(&data), Verdict::Same); + assert!( + p.verify_at("/elsewhere/copy", &data).is_usable(), + "a copy of the same file is still the same binary" + ); + + // A patch set the project names, applied to the file the project names: + // the pair has to agree about the same bytes. + let mut patch = Patch::new("first byte of the ident"); + patch.binary = Some(format!("{:016x}", p.binary.hash)); + patch + .record( + e5r_db::anchor::Anchor { + shape: 0, + bytes: 0, + insns: 0, + abs: Addr(0x10), + offset: 0, + }, + &data, + Addr(0), + vec![0x00, 0x00], + ) + .expect("the edit is inside the file"); + p.patches.push("ident.e5r-patch".into()); + + let mut image = data.clone(); + patch.apply(&mut image, Addr(0), None).expect("it applies"); + // The patched file is no longer the binary the project was made for, which + // is the whole point of recording the digest. + assert!(matches!(p.verify(&image), Verdict::Different { .. })); + patch + .revert() + .apply(&mut image, Addr(0), None) + .expect("the inverse applies"); + assert_eq!(p.verify(&image), Verdict::Same); + + let back = Project::from_text(&p.to_text()).expect("it parses"); + assert_eq!(back, p); +} diff --git a/crates/e5r-db/tests/static_signatures.rs b/crates/e5r-db/tests/static_signatures.rs new file mode 100644 index 0000000..d392807 --- /dev/null +++ b/crates/e5r-db/tests/static_signatures.rs @@ -0,0 +1,264 @@ +//! Signature matching against a real distribution's static libraries. +//! +//! This is the case the feature exists for. A statically linked binary has the +//! library code inside the image and, once stripped, nothing at all to say +//! about what any of it is: several thousand functions, no names. The names +//! are not lost though, they are in the distribution's own development +//! packages, in the `.a` archives the linker copied that code out of. +//! +//! So the library here is built the way `scripts/build-siglib.sh` builds one +//! for a user: every object in every archive is analyzed, and every function a +//! symbol names becomes one signature. It is then applied to the stripped +//! binary and checked against the unstripped copy, which is the oracle. +//! +//! A wrong name is worse than no name, because the reader will believe it, so +//! the wrong count is asserted at zero and recall is a floor that only moves +//! one way. The oracle is a set of names per address rather than one name: a +//! linker puts `strlen` and `__strlen` at the same byte, and calling that +//! address either of them is right. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_db::anchor::Anchor; +use e5r_db::signature::{Library, Signature}; +use e5r_format::LoadOptions; + +/// Floor on the share of the binary's named functions that get a name back. +/// +/// Below the measured number with room for another distribution's glibc to be +/// built differently, because the floor is here to catch a regression and the +/// number the test prints is the measure. What keeps it off 100% is that most +/// of what a static link pulls in is short: under the shape floor a function is +/// identified by its exact bytes alone, and one relocation applied anywhere in +/// the body changes those. +const MIN_RECALL: f64 = 0.40; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// Analysis with the parts a signature does not use switched off. +fn options() -> Options { + Options { + strings: false, + xrefs: false, + ..Options::default() + } +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &options())) +} + +/// Where the distribution keeps the static libraries this binary was linked +/// from. The compiler knows, so it is asked rather than guessed at. +fn archive(name: &str) -> Option { + // The compiler that built the fixture, not the one that matches the host: + // the binary these signatures are matched against is AArch64 wherever it + // was built, so an x86-64 `libc.a` yields signatures that cannot match a + // single function in it -- which reads as a recovery rate of zero rather + // than as a library about the wrong machine. + let cc = ["aarch64-linux-gnu-gcc", "gcc"] + .into_iter() + .find(|c| Command::new(c).arg("--version").output().is_ok())?; + let out = Command::new(cc) + .arg(format!("-print-file-name={name}")) + .output() + .ok()?; + let path = PathBuf::from(String::from_utf8(out.stdout).ok()?.trim()); + // The compiler echoes the name back unchanged when it has no such file. + (path.is_absolute() && path.is_file()).then_some(path) +} + +/// One function's fingerprint, exactly as a signature records it. +fn anchor_of(p: &Program, f: &e5r_analysis::Function) -> Anchor { + let insns = p.instructions(f); + let mut body = Vec::with_capacity(f.cfg.covered_bytes() as usize); + for b in f.cfg.blocks.values() { + if let Some(bytes) = p.object.memory.slice(b.range.start(), b.range.len()) { + body.extend_from_slice(bytes); + } + } + Anchor::function(f.entry, &insns, &body) +} + +/// Signatures for every named function in every object an archive holds. +/// +/// This is what the script does through the command line, done in process so +/// the gate does not need a built binary to run. +fn signatures_of_archive(path: &Path, into: &mut Vec) -> usize { + let Ok(data) = std::fs::read(path) else { + return 0; + }; + let Ok(ar) = e5r_format::archive::open(&data) else { + return 0; + }; + let label = path.file_name().unwrap_or_default().to_string_lossy(); + let mut objects = 0; + for (n, member) in ar.objects() { + let Ok(obj) = ar.load_member(n, &LoadOptions::default()) else { + continue; + }; + objects += 1; + let p = analyze(obj, &options()); + let source = format!("{label}({})", member.name); + for f in p.functions_by_address() { + // A name is the whole point: a signature called `sub_401000` + // identifies nothing and a library of them matches everything. + let Some(name) = f.raw_name() else { + continue; + }; + if name.is_empty() || name.ends_with("@plt") { + continue; + } + into.push(Signature::new(&anchor_of(&p, f), name, &source)); + } + } + objects +} + +/// Every name the file gives each address, so an alias is not a wrong answer. +fn names_by_address(p: &Program) -> BTreeMap> { + let mut out: BTreeMap> = BTreeMap::new(); + for s in &p.object.symbols { + if s.is_defined_function() && !s.name.is_empty() { + out.entry(s.addr.get()).or_default().insert(s.name.clone()); + } + } + for f in p.functions_by_address() { + if let Some(n) = f.raw_name() { + if !n.is_empty() { + out.entry(f.entry.get()).or_default().insert(n.to_string()); + } + } + } + out +} + +#[test] +fn a_stripped_static_binary_gets_its_library_names_back() { + let (Some(named), Some(stripped)) = + (open("hello.static.a64"), open("hello.static.a64.stripped")) + else { + return; + }; + // libm is in the link line even when nothing calls it, and libc_nonshared + // carries the handful of stubs libc.a leaves out. + let archives: Vec = ["libc.a", "libm.a", "libc_nonshared.a"] + .iter() + .filter_map(|n| archive(n)) + .collect(); + if archives.is_empty() { + return; + } + + let mut collected = Vec::new(); + let mut objects = 0; + for a in &archives { + objects += signatures_of_archive(a, &mut collected); + } + let raw = collected.len(); + let library = Library::build(collected); + assert!( + !library.is_empty(), + "no signatures came out of {archives:?}" + ); + + let truth = names_by_address(&named); + let mut recovered = 0usize; + let mut wrong: Vec = Vec::new(); + for f in stripped.functions_by_address() { + let Some(found) = library.identify(&anchor_of(&stripped, f)) else { + continue; + }; + // A function the unstripped copy does not name cannot be checked, and + // a name claimed for it is not evidence either way. + let Some(real) = truth.get(&f.entry.get()) else { + continue; + }; + if real.contains(&found.signature.name) { + recovered += 1; + } else { + wrong.push(format!( + "{}: called it {:?}, it is {:?} ({}, from {})", + f.entry, + found.signature.name, + real, + found.resolution.as_str(), + found.signature.source + )); + } + } + + let claimed = stripped + .functions_by_address() + .filter(|f| library.identify(&anchor_of(&stripped, f)).is_some()) + .count(); + let checkable = stripped + .functions_by_address() + .filter(|f| truth.contains_key(&f.entry.get())) + .count(); + let recall = recovered as f64 / checkable.max(1) as f64; + println!( + "{raw} signatures ({} after merging) from {objects} objects in {} archives", + library.len(), + archives.len() + ); + println!( + "{claimed} functions identified, {recovered} of {checkable} names \ + recovered ({:.1}%), {} wrong", + recall * 100.0, + wrong.len() + ); + + assert!( + wrong.is_empty(), + "{} names are wrong, which is worse than having none:\n {}", + wrong.len(), + wrong.join("\n ") + ); + assert!( + recall >= MIN_RECALL, + "recovered {recovered} of {checkable} ({recall:.3}), floor is {MIN_RECALL}" + ); +} + +#[test] +fn a_library_from_one_distribution_does_not_name_another_program() { + // The other direction, and the one that matters more: applying a library + // to a binary none of its code is in must produce no name at all, not a + // plausible-looking wrong one. + let Some(p) = open("insndiff.a64") else { + return; + }; + let Some(a) = archive("libc.a") else { + return; + }; + let mut collected = Vec::new(); + signatures_of_archive(&a, &mut collected); + let library = Library::build(collected); + if library.is_empty() { + return; + } + let truth = names_by_address(&p); + for f in p.functions_by_address() { + let Some(found) = library.identify(&anchor_of(&p, f)) else { + continue; + }; + let Some(real) = truth.get(&f.entry.get()) else { + continue; + }; + assert!( + real.contains(&found.signature.name), + "{} is {real:?} and a libc library called it {:?}", + f.entry, + found.signature.name + ); + } +} diff --git a/crates/e5r-db/tests/types.rs b/crates/e5r-db/tests/types.rs new file mode 100644 index 0000000..e5c1df4 --- /dev/null +++ b/crates/e5r-db/tests/types.rs @@ -0,0 +1,179 @@ +//! Applied types in the annotation log. +//! +//! The log stores a type as the text a person typed, and parses it on read. +//! What makes that a decision rather than a shrug is the check on the way in: +//! a typo is refused at the moment it is typed, when the person who made it is +//! still there, instead of sitting in the file forever being ignored. + +use e5r_core::Addr; +use e5r_db::anchor::Anchor; +use e5r_db::log::{Declared, Field, Log, declared}; +use e5r_types::ctype::{Type, Types}; + +fn anchor(n: u64) -> Anchor { + Anchor { + shape: 0x1000 + n, + bytes: 0x2000 + n, + insns: 12, + abs: Addr(0x40_0000 + n * 0x10), + offset: 0, + } +} + +#[test] +fn a_valid_declaration_is_accepted_and_read_back_as_a_prototype() { + let mut log = Log::new(); + log.assert_checked( + anchor(1), + Field::Type, + Some("int arith8(unsigned char a, unsigned char b)".into()), + "ht", + ) + .expect("a declaration an analyst would type"); + + let stored = log.records()[0].value.clone().expect("the source text"); + let mut types = Types::new(); + let Declared::Function { + name, signature, .. + } = declared(&mut types, &stored).expect("it parses on read") + else { + panic!("a function declaration") + }; + assert_eq!(name.as_deref(), Some("arith8")); + assert_eq!(signature.parameters.len(), 2); + assert_eq!( + types.get(signature.parameters[0].1), + Some(&Type::Int { + size: 1, + signed: false + }) + ); +} + +#[test] +fn a_type_on_a_data_object_is_a_bare_type_name() { + let mut log = Log::new(); + for text in [ + "struct header", + "uint32_t [16]", + "void (*)(int)", + "const char *", + "struct header { uint32_t magic; uint16_t len; };", + ] { + log.assert_checked(anchor(2), Field::Type, Some(text.into()), "ht") + .unwrap_or_else(|e| panic!("{text:?} should be accepted: {e}")); + } + let mut types = Types::new(); + let d = declared(&mut types, "uint32_t [16]").expect("an array of sixteen words"); + assert_eq!(types.size_of(d.ty()), Some(64)); + assert!(matches!(d, Declared::Object { .. })); +} + +#[test] +fn a_typo_is_refused_at_the_moment_it_is_typed() { + let mut log = Log::new(); + let e = log + .assert_checked( + anchor(3), + Field::Type, + Some("int arith8(unsinged char a)".into()), + "ht", + ) + .expect_err("a misspelled type is not a type"); + let message = format!("{e}"); + assert!(message.contains("unsinged"), "{message}"); + assert!(log.is_empty(), "nothing was written"); + + for bad in ["int f(", "struct { ", "int 3x", "??"] { + assert!( + log.assert_checked(anchor(3), Field::Type, Some(bad.into()), "ht") + .is_err(), + "{bad:?} should have been refused" + ); + } + assert!(log.is_empty()); +} + +#[test] +fn a_name_is_checked_too_and_a_comment_is_not() { + let mut log = Log::new(); + assert!( + log.assert_checked(anchor(4), Field::Name, Some(" ".into()), "ht") + .is_err() + ); + assert!( + log.assert_checked(anchor(4), Field::Name, Some("a\nb".into()), "ht") + .is_err() + ); + // A demangled C++ name is full of punctuation and is a perfectly good name. + log.assert_checked( + anchor(4), + Field::Name, + Some("std::vector::push_back(int const&)".into()), + "ht", + ) + .expect("a demangled name"); + // A comment is prose and is nobody's business to validate. + log.assert_checked( + anchor(4), + Field::Comment, + Some("looks like a ??? state machine".into()), + "ht", + ) + .expect("a comment"); +} + +#[test] +fn clearing_a_field_needs_no_value_to_check() { + let mut log = Log::new(); + log.assert_checked(anchor(5), Field::Type, Some("int f(void)".into()), "ht") + .unwrap(); + log.assert_checked(anchor(5), Field::Type, None, "ht") + .expect("a tombstone clears the field"); + assert!(log.fold().is_empty()); +} + +#[test] +fn reading_a_log_never_rejects_what_writing_would_have() { + // A log written by a newer build, or hand-edited, must still fold. Refusing + // the whole file because one type no longer parses would throw away the + // names and comments beside it, which is the opposite of what the store is + // for. + let mut log = Log::new(); + log.assert( + anchor(6), + Field::Type, + Some("_Atomic(int) f(void) [[weird]]".into()), + "someone", + ); + log.assert( + anchor(6), + Field::Name, + Some("parse_header".into()), + "someone", + ); + let text = log.to_text(); + let back = Log::from_text(&text).expect("it reads"); + assert_eq!(back.records().len(), 2); + assert_eq!(back.to_text(), text); + + // And the unreadable type is reported when something asks for it, rather + // than crashing whatever asked. + let mut types = Types::new(); + assert!(declared(&mut types, "_Atomic(int) f(void) [[weird]]").is_err()); +} + +#[test] +fn the_source_text_is_what_survives_into_the_file() { + // Keeping the text rather than a serialized type is what makes the log + // reviewable in a diff, and what lets the parser improve without anybody + // rewriting history. + let mut log = Log::new(); + let source = "typedef struct node { struct node *next; int value; } node_t;"; + log.assert_checked(anchor(7), Field::Type, Some(source.into()), "ht") + .expect("a typedef"); + let text = log.to_text(); + assert!(text.contains("struct node *next"), "{text}"); + let back = Log::from_text(&text).unwrap(); + assert_eq!(back.records()[0].value.as_deref(), Some(source)); +} diff --git a/crates/e5r-decomp/Cargo.toml b/crates/e5r-decomp/Cargo.toml new file mode 100644 index 0000000..0a697af --- /dev/null +++ b/crates/e5r-decomp/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "e5r-decomp" +description = "e5r — expression rebuilding, structuring and C emission." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +e5r-core = { workspace = true } +e5r-arch = { workspace = true } +e5r-ir = { workspace = true } +serde = { workspace = true } + +[dev-dependencies] +e5r-analysis = { workspace = true } +e5r-format = { workspace = true } +# Path only, with no version. A dev-dependency that carries a version is +# resolved against the registry when the crate is packaged, and these two +# point back at this crate and at one that depends on it, which is a cycle +# nothing can publish. Stripped from the published manifest without one. +e5r-decomp = { path = "." } +e5r-api = { path = "../e5r-api" } diff --git a/crates/e5r-decomp/src/emit.rs b/crates/e5r-decomp/src/emit.rs new file mode 100644 index 0000000..fab18cc --- /dev/null +++ b/crates/e5r-decomp/src/emit.rs @@ -0,0 +1,1223 @@ +//! Rendering a structured function as pseudo-C. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write as _; + +use e5r_core::Addr; +use e5r_ir::op::{Op, Space}; +use e5r_ir::ssa::{Operand, SsaFunction, SsaKind, SsaOp, Value}; + +use crate::expr::{Expr, Rebuilder, c_type, input_name, negate}; +use crate::structure::{Graph, Region, Switches, Taken, structure_with}; + +/// A decompiled function. +#[derive(Debug, Clone)] +pub struct Output { + /// The C text. + pub text: String, + /// Declarations the text needs to compile: the functions it calls and the + /// helpers for operations C has no operator for. + pub declarations: Vec, + /// Gotos the structuring needed; lower is better. + pub gotos: usize, + /// Reachable blocks the structuring never placed, whose code is therefore + /// missing from the text. Zero in a correct decompilation. + pub lost: usize, + /// Named locals declared. + pub locals: usize, + /// Operations no expression covered. + pub unmodelled: usize, + /// The declaration this definition would need, without the body, so a + /// unit can be assembled with every function declared before it is called. + pub signature: String, + /// How many parameters it declares, so a call to it passes that many. + pub arity: usize, + /// Which of them are pointers. + pub pointer_parameters: Vec, + /// How wide each one is declared, in bytes, so a caller passing a constant + /// can write it at the width the callee reads. + pub parameter_widths: Vec, +} + +/// What the debug information said about a function, when it said anything. +/// +/// Names and types the compiler recorded beat anything inferred, so when this +/// is present the parameters are declared as written rather than as the +/// convention's registers. +#[derive(Debug, Clone, Default)] +pub struct Prototype { + /// The parameters, in order. + pub parameters: Vec, + /// The return type as C spells it. + pub returns: Option, + /// Local variables by their offset from the frame base. + pub locals: BTreeMap, + /// Definitions the declarations need, in an order C accepts. + pub definitions: Vec, +} + +/// A function this unit also defines. +#[derive(Debug, Clone)] +pub struct Callee { + /// What it is called, as C spells it. + pub name: String, + /// How many parameters it declares. + pub arity: usize, + /// Which of them are pointers, so a call passes something C will take. + pub pointer_parameters: Vec, + /// How wide each one is declared, in bytes. + pub parameter_widths: Vec, + /// False when it was declared to return nothing, so its result is not + /// assigned to anything. + pub returns_value: bool, + /// Its declaration, without a body. Every name the emitter can write has + /// to be declared somewhere, including a callee nobody asked to see: a + /// call to an undeclared function is not C a compiler will accept. + pub signature: String, +} + +/// One declared parameter. +#[derive(Debug, Clone)] +pub struct Param { + /// The name the source gave it. + pub name: String, + /// The whole declaration, as C spells it: the type wraps around the name, + /// so it cannot be rebuilt from the two halves. + pub decl: String, + /// True when the convention passes it in a floating point register. + pub floating: bool, + /// True when its declared type is a pointer. + pub pointer: bool, + /// Its declared width in bytes, zero when unknown. + pub size: u8, + /// Fields seen through it, when it is a pointer whose shape was + /// recovered rather than declared. + pub fields: Vec<(i64, u8)>, + /// The element size, when it walks an array of them. + pub stride: Option, + /// Its offset above the entry stack pointer, when the convention ran out + /// of registers and the caller left it on the stack. `None` for the ones + /// that arrive in registers. + pub stack: Option, +} + +/// Decompile an SSA function to pseudo-C. +pub fn decompile(name: &str, f: &SsaFunction) -> Output { + decompile_with(name, f, None) +} + +/// Decompile, using what the debug information said where it said anything. +pub fn decompile_with(name: &str, f: &SsaFunction, prototype: Option<&Prototype>) -> Output { + decompile_in(name, f, prototype, &BTreeMap::new()) +} + +/// Decompile as part of a unit, knowing how many arguments its callees take. +/// +/// A call whose callee is defined alongside it has to pass the number of +/// arguments that definition declares, or the two do not agree. Where the +/// callee is not in the unit, the declaration leaves the parameters +/// unspecified and the call passes what it can find. +pub fn decompile_in( + name: &str, + f: &SsaFunction, + prototype: Option<&Prototype>, + callees: &BTreeMap, +) -> Output { + decompile_full(name, f, prototype, callees, &Switches::new()) +} + +/// Decompile knowing what the program's jump tables mean, so a multi-way +/// branch comes out as a switch rather than as a page of labels. +pub fn decompile_full( + name: &str, + f: &SsaFunction, + prototype: Option<&Prototype>, + callees: &BTreeMap, + switches: &Switches, +) -> Output { + let graph: Graph = f + .blocks + .iter() + .map(|(a, b)| (*a, b.successors.clone())) + .collect(); + // Where each conditional branch goes when it is taken, read off the + // branch rather than guessed from the order of the successors. + let taken: Taken = f + .blocks + .iter() + .filter_map(|(a, b)| { + let op = b + .ops + .iter() + .rev() + .find(|op| op.kind == SsaKind::Op(Op::CBranch))?; + let target = op.inputs.first()?.as_const()?; + Some((*a, Addr(target))) + }) + .collect(); + let s = structure_with(f.entry, &graph, &taken, switches); + let mut rebuilder = Rebuilder::new(f); + // A declared parameter arrives in a register the convention chooses, so + // the body can use the name the source gave it rather than `arg0`. + if let Some(p) = prototype { + let (mut ints, mut floats) = (0usize, 0usize); + for param in &p.parameters { + let offset = if param.floating { + let o = rebuilder.abi.float_arguments.get(floats).copied(); + floats += 1; + o + } else { + let o = rebuilder.abi.integer_arguments.get(ints).copied(); + ints += 1; + o + }; + if let Some(o) = offset { + rebuilder.names.insert(o, param.name.clone()); + if param.pointer { + rebuilder.pointers.insert(o); + } + if param.size > 0 { + rebuilder.sizes.insert(o, param.size); + } + if !param.fields.is_empty() { + rebuilder.fields.insert(o, param.fields.clone()); + if let Some(stride) = param.stride { + rebuilder.strides.insert(o, stride); + } + } + // A parameter declared floating is floating even when nothing + // in the body does arithmetic on it: at O0 it is stored to the + // stack before anything touches it. + if param.floating { + rebuilder.floats.insert(e5r_ir::ssa::Location { + space: Space::Register, + offset: o, + size: 8, + }); + } + } + } + } + let rebuilder = rebuilder; + + let result = result_register(f, &rebuilder); + let returns = prototype.and_then(|p| p.returns.clone()); + let structured_switches = switch_heads(&s.root); + let mut e = Emitter { + f, + r: &rebuilder, + labels: &s.labels, + switches: &structured_switches, + result, + callees, + returns, + unmodelled: 0, + }; + + let body = { + let mut out = String::new(); + e.region(&mut out, &s.root, 1); + out + }; + + // Values that arrive from outside and are not arguments: registers the + // function inherited. Declaring them says where they came from without + // pretending they are parameters. + // Keyed by name: the same location read at two widths must be declared + // once, not twice with different types. + let mut inherited: BTreeMap = BTreeMap::new(); + // Which names are parameters: the declared ones when there is a prototype, + // and otherwise the convention's argument registers. + let declared: BTreeSet = match prototype { + Some(p) => p + .parameters + .iter() + .map(|param| param.name.clone()) + .collect(), + None => BTreeSet::new(), + }; + let mut called: BTreeSet = BTreeSet::new(); + let mut helpers: BTreeSet<&'static str> = BTreeSet::new(); + for b in f.blocks.values() { + for op in &b.ops { + for i in &op.inputs { + if let e5r_ir::ssa::Operand::Undefined(l) = i { + let name = rebuilder.name_of(*l); + let is_parameter = if prototype.is_some() { + declared.contains(&name) + } else { + name.starts_with("arg") || name.starts_with("farg") + }; + if !is_parameter { + let ty = if rebuilder.floats.contains(l) { + crate::expr::float_type(l.size) + } else { + c_type(l.size) + }; + inherited.entry(name).or_insert_with(|| ty.to_string()); + } + } + } + if op.kind == SsaKind::Op(Op::Call) { + if let Some(target) = op.inputs.first().and_then(|i| i.as_const()) { + called.insert(target); + } + } + if let Some(target) = tail_call(f, op) { + called.insert(target); + } + if let SsaKind::Op(o) = op.kind { + if let Some(h) = helper_for(o) { + helpers.insert(h); + } + } + } + } + + let mut declarations: Vec = + prototype.map(|p| p.definitions.clone()).unwrap_or_default(); + declarations.extend(helpers.iter().map(|h| h.to_string())); + declarations.extend( + called + .iter() + .filter(|a| !callees.contains_key(a)) + // No parameter list: what a called function takes is what + // prototype recovery is for, and guessing four would be a claim. + .map(|a| format!("uint64_t sub_{a:x}();")), + ); + + // The unknown-result return is written during emission, after the helpers + // the operations need have been collected, so it declares its own. + if body.contains("__clobbered(") && !declarations.iter().any(|d| d.contains("__clobbered")) { + declarations.push("uint64_t __clobbered(void);".to_string()); + } + + let mut text = String::new(); + let declared_return = prototype + .and_then(|p| p.returns.clone()) + .unwrap_or_else(|| return_type(result, &rebuilder).to_string()); + let declared: Vec = match prototype { + Some(p) if !p.parameters.is_empty() => p + .parameters + .iter() + // A parameter whose type did not resolve is still a parameter; C + // has no `void` argument, so it is declared as what it occupies. + .map(|param| { + if param.decl.starts_with("void ") { + format!("uint64_t {}", param.name) + } else { + param.decl.clone() + } + }) + .collect(), + Some(p) if p.returns.is_some() => Vec::new(), + _ => parameter_list(f, &rebuilder), + }; + let arity = declared.len(); + let pointer_parameters: Vec = match prototype { + Some(p) => p.parameters.iter().map(|param| param.pointer).collect(), + None => vec![false; arity], + }; + let parameter_widths: Vec = match prototype { + Some(p) => p.parameters.iter().map(|param| param.size).collect(), + None => vec![8; arity], + }; + let declared_parameters = if declared.is_empty() { + "void".to_string() + } else { + declared.join(", ") + }; + let signature = format!( + "{declared_return} {}({declared_parameters})", + identifier(name) + ); + let _ = writeln!(text, "{signature}"); + text.push_str("{\n"); + // Only the ones the body actually mentions: an inherited flag that every + // pass removed should not be declared. + let inherited: Vec<(&String, &String)> = inherited + .iter() + .filter(|(name, _)| mentions(&body, name)) + .collect(); + for (name, ty) in &inherited { + let _ = writeln!(text, " {ty} {name}; // inherited"); + } + for (value, local) in &rebuilder.locals { + let ty = if rebuilder.floats.contains(&value.location) { + crate::expr::float_type(value.location.size) + } else { + c_type(value.location.size) + }; + let _ = writeln!(text, " {ty} {local};"); + } + if !rebuilder.locals.is_empty() || !inherited.is_empty() { + text.push('\n'); + } + text.push_str(&body); + text.push_str("}\n"); + + Output { + text, + signature, + arity, + pointer_parameters, + parameter_widths, + declarations, + gotos: s.gotos, + lost: s.lost.len(), + locals: rebuilder.locals.len(), + unmodelled: e.unmodelled, + } +} + +/// Declarations every unit needs: the reinterpretations between a value's bits +/// and the number they stand for, which the machine does for free and C does +/// not. +const REINTERPRET: &str = "\ +static inline uint64_t __bits(double v){union{double d;uint64_t u;}x;x.d=v;return x.u;} +static inline double __dbl(uint64_t v){union{double d;uint64_t u;}x;x.u=v;return x.d;} +static inline uint32_t __bits32(float v){union{float f;uint32_t u;}x;x.f=v;return x.u;} +static inline float __flt(uint64_t v){union{float f;uint32_t u;}x;x.u=(uint32_t)v;return x.f;}"; + +/// The function a branch tails into, when it leaves this function entirely. +/// +/// A tail call is a plain branch to something that is no block of this +/// function: the callee returns on the caller's behalf. Nothing else in the IR +/// marks it, and a branch is not otherwise a statement, so without this both +/// the call and the return it stands for vanish and the body comes out empty. +fn tail_call(f: &SsaFunction, op: &SsaOp) -> Option { + if op.kind != SsaKind::Op(Op::Branch) { + return None; + } + let target = op.inputs.first()?.as_const()?; + (!f.blocks.contains_key(&Addr(target))).then_some(target) +} + +/// True when a region always leaves by itself, so no `break` is needed after. +fn ends_control(r: &Region) -> bool { + match r { + Region::Break | Region::Continue | Region::Goto(_) => true, + Region::Seq(parts) => parts.last().is_some_and(ends_control), + _ => false, + } +} + +/// A name C will accept: anything else becomes an underscore. +pub fn identifier(name: &str) -> String { + let mut out: String = name + .chars() + .map(|c| { + if c.is_alphanumeric() || c == '_' { + c + } else { + '_' + } + }) + .collect(); + if out.chars().next().is_some_and(|c| c.is_ascii_digit()) { + out.insert(0, '_'); + } + if out.is_empty() { + out.push('_'); + } + out +} + +/// True when a name appears in the text as a whole word. +fn mentions(text: &str, name: &str) -> bool { + let mut at = 0; + while let Some(found) = text[at..].find(name) { + let start = at + found; + let end = start + name.len(); + let before = text[..start].chars().next_back(); + let after = text[end..].chars().next(); + let word = |c: Option| c.is_some_and(|c| c.is_alphanumeric() || c == '_'); + if !word(before) && !word(after) { + return true; + } + at = end; + } + false +} + +/// The declaration an operation's helper needs, when it has one. +fn helper_for(o: Op) -> Option<&'static str> { + Some(match o { + Op::FloatAdd + | Op::FloatSub + | Op::FloatMul + | Op::FloatDiv + | Op::FloatNeg + | Op::FloatEqual + | Op::FloatNotEqual + | Op::FloatLess + | Op::FloatLessEqual => REINTERPRET, + Op::FloatAbs => "double __fabs(double);", + Op::FloatSqrt => "double __sqrt(double);", + Op::FloatMax => "double __fmax(double, double);", + Op::FloatMin => "double __fmin(double, double);", + Op::FloatNan => "int __isnan(double);", + Op::FloatTrunc => "double __trunc(double);", + Op::FloatRound => "double __rint(double);", + Op::FloatCeil => "double __ceil(double);", + Op::FloatFloor => "double __floor(double);", + Op::FloatMulAdd => "double __fma(double, double, double);", + Op::FloatConvert | Op::IntToFloat | Op::UIntToFloat | Op::FloatToInt | Op::FloatToUInt => { + REINTERPRET + } + Op::CallInd => "uint64_t __callind(uint64_t);", + Op::BranchInd => "void __indirect_branch(uint64_t);", + Op::IntDiv128 => "uint64_t __udiv128(uint64_t, uint64_t, uint64_t);", + Op::IntSDiv128 => "uint64_t __sdiv128(uint64_t, uint64_t, uint64_t);", + Op::IntRem128 => "uint64_t __urem128(uint64_t, uint64_t, uint64_t);", + Op::IntSRem128 => "uint64_t __srem128(uint64_t, uint64_t, uint64_t);", + Op::IntMulHigh => "uint64_t __mulhi(uint64_t, uint64_t);", + Op::IntSMulHigh => "uint64_t __smulhi(uint64_t, uint64_t);", + Op::PopCount => "uint64_t __popcount(uint64_t);", + Op::LzCount => "uint64_t __clz(uint64_t);", + Op::IntCarry => "uint64_t __carry(uint64_t, uint64_t);", + Op::IntSCarry => "uint64_t __overflow(uint64_t, uint64_t);", + Op::IntSBorrow => "uint64_t __borrow(uint64_t, uint64_t);", + Op::Unimplemented => "void __unmodelled(uint64_t);", + Op::Undefine => "uint64_t __clobbered(void);", + Op::CBranch => "int __condition(void);", + _ => return None, + }) +} + +/// Which register the function leaves its result in, if any. +/// +/// The convention lists the candidates; which one this function writes says +/// whether it returns an integer, a floating point value, or nothing. +fn result_register(f: &SsaFunction, r: &Rebuilder) -> Option { + // The one written latest before the return. A function that computes into + // a general register and then converts into a vector one writes both, and + // only the order says which the caller reads. + let mut best: Option<(usize, u64)> = None; + for b in f.blocks.values() { + let returns = b.ops.iter().any(|op| op.kind == SsaKind::Op(Op::Return)); + if !returns { + continue; + } + for (n, op) in b.ops.iter().enumerate() { + let Some(v) = op.out else { continue }; + if v.location.space != Space::Register || !r.abi.results.contains(&v.location.offset) { + continue; + } + if best.map(|(at, _)| n > at).unwrap_or(true) { + best = Some((n, v.location.offset)); + } + } + } + if let Some((_, offset)) = best { + return Some(offset); + } + // Nothing was written in the returning block, so whichever the function + // writes at all is the answer. + r.abi.results.iter().copied().find(|offset| { + f.blocks.values().any(|b| { + b.ops + .iter() + .any(|op| op.out.is_some_and(|v| v.location.offset == *offset)) + }) + }) +} + +/// The declared return type, from where the result was left. +fn return_type(result: Option, r: &Rebuilder) -> &'static str { + match result { + None => "void", + Some(offset) if offset >= r.abi.vector_base => "double", + Some(_) => "uint64_t", + } +} + +/// The parameters, taken from the argument registers read before being written. +fn parameter_list(f: &SsaFunction, r: &Rebuilder) -> Vec { + // Ordered by where the convention puts them, not by name, so `arg10` does + // not sort before `arg2`. + let mut seen: BTreeMap = BTreeMap::new(); + let order: Vec = r + .abi + .integer_arguments + .iter() + .chain(r.abi.float_arguments.iter()) + .copied() + .collect(); + for b in f.blocks.values() { + for op in &b.ops { + for i in &op.inputs { + let Operand::Undefined(l) = i else { continue }; + let name = input_name(*l, &r.abi); + let ty = if r.floats.contains(l) { + crate::expr::float_type(l.size) + } else { + c_type(l.size) + }; + // Register arguments come first, in the order the convention + // uses them; then the ones the caller left on the stack, in + // address order. + if l.space == Space::Register { + if let Some(n) = order.iter().position(|o| *o == l.offset) { + seen.insert(n, format!("{ty} {name}")); + } + } else if crate::expr::is_stack_argument(*l) { + seen.insert(order.len() + l.offset as usize, format!("{ty} {name}")); + } + } + } + } + seen.into_values().collect() +} + +/// The blocks a region tree turned into a `switch`. +/// +/// Their indirect branch is the switch itself, so writing it out as well would +/// say the control transfer twice. +fn switch_heads(r: &Region) -> BTreeSet { + let mut out = BTreeSet::new(); + collect_switch_heads(r, &mut out); + out +} + +fn collect_switch_heads(r: &Region, out: &mut BTreeSet) { + match r { + Region::Seq(parts) => { + for p in parts { + collect_switch_heads(p, out); + } + } + Region::If { + then, otherwise, .. + } => { + collect_switch_heads(then, out); + if let Some(o) = otherwise { + collect_switch_heads(o, out); + } + } + Region::While { body, .. } | Region::Infinite { body, .. } => { + collect_switch_heads(body, out); + } + Region::Switch { + head, + cases, + default, + } => { + out.insert(*head); + for c in cases { + collect_switch_heads(&c.body, out); + } + if let Some(d) = default { + collect_switch_heads(d, out); + } + } + _ => {} + } +} + +struct Emitter<'a> { + f: &'a SsaFunction, + r: &'a Rebuilder<'a>, + labels: &'a BTreeSet, + /// Blocks whose indirect branch the region tree already says as a `switch`. + switches: &'a BTreeSet, + /// Where the function leaves its result. + result: Option, + /// How many arguments each callee defined in this unit takes. + callees: &'a BTreeMap, + /// The declared return type, which the returned expression is converted + /// to: the machine leaves bits in a register and the declaration says what + /// they mean. + returns: Option, + unmodelled: usize, +} + +impl Emitter<'_> { + fn region(&mut self, out: &mut String, region: &Region, depth: usize) { + let pad = " ".repeat(depth); + match region { + Region::Empty => {} + Region::Seq(parts) => { + for p in parts { + self.region(out, p, depth); + } + } + Region::Block(at) => { + if self.labels.contains(at) { + let _ = writeln!( + out, + "{}L{:x}:", + " ".repeat(depth.saturating_sub(1)), + at.0 + ); + } + self.statements(out, *at, depth); + } + Region::Goto(at) => { + let _ = writeln!(out, "{pad}goto L{:x};", at.0); + } + Region::Break => { + let _ = writeln!(out, "{pad}break;"); + } + Region::Continue => { + let _ = writeln!(out, "{pad}continue;"); + } + Region::If { + head, + invert, + then, + otherwise, + } => { + let cond = self.condition(*head, *invert); + let _ = writeln!(out, "{pad}if ({cond}) {{"); + self.region(out, then, depth + 1); + match otherwise { + Some(o) if **o != Region::Empty => { + let _ = writeln!(out, "{pad}}} else {{"); + self.region(out, o, depth + 1); + let _ = writeln!(out, "{pad}}}"); + } + _ => { + let _ = writeln!(out, "{pad}}}"); + } + } + } + Region::While { head, invert, body } => { + if self.labels.contains(head) { + let _ = writeln!( + out, + "{}L{:x}:", + " ".repeat(depth.saturating_sub(1)), + head.0 + ); + } + // The head's own statements compute the condition, so they run + // on every iteration: a `for (; cond; )` with them hoisted + // would be wrong. Write the loop as `while (1)` with the test + // at the top when the head does more than test. + let cond = self.condition(*head, *invert); + if self.head_is_only_a_test(*head) { + let _ = writeln!(out, "{pad}while ({cond}) {{"); + self.region(out, body, depth + 1); + let _ = writeln!(out, "{pad}}}"); + } else { + let _ = writeln!(out, "{pad}while (1) {{"); + self.statements(out, *head, depth + 1); + let _ = writeln!(out, "{pad} if ({}) break;", negate(cond.clone())); + self.region(out, body, depth + 1); + let _ = writeln!(out, "{pad}}}"); + } + } + Region::Switch { + head, + cases, + default, + } => { + let index = self.switch_index(*head); + let _ = writeln!(out, "{pad}switch ({index}) {{"); + for case in cases { + for value in &case.values { + let _ = writeln!(out, "{pad}case {value}:"); + } + self.region(out, &case.body, depth + 1); + if !ends_control(&case.body) { + let _ = writeln!(out, "{pad} break;"); + } + } + if let Some(d) = default { + let _ = writeln!(out, "{pad}default:"); + self.region(out, d, depth + 1); + if !ends_control(d) { + let _ = writeln!(out, "{pad} break;"); + } + } + let _ = writeln!(out, "{pad}}}"); + } + Region::Infinite { head, body } => { + if self.labels.contains(head) { + let _ = writeln!( + out, + "{}L{:x}:", + " ".repeat(depth.saturating_sub(1)), + head.0 + ); + } + let _ = writeln!(out, "{pad}while (1) {{"); + self.statements(out, *head, depth + 1); + self.region(out, body, depth + 1); + let _ = writeln!(out, "{pad}}}"); + } + } + } + + /// What a switch switches on. + /// + /// The branch computes an address; the index is what was used to look it + /// up. Recovering it means finding the table read in the address's own + /// expression, which is the shape the jump table analysis already proved + /// is there. Where that fails the address itself is switched on, which is + /// correct and reads worse. + fn switch_index(&self, at: Addr) -> Expr { + let branch = self + .f + .blocks + .get(&at) + .and_then(|b| { + b.ops + .iter() + .rev() + .find(|op| op.kind == SsaKind::Op(Op::BranchInd)) + }) + .and_then(|op| op.inputs.first().copied()); + let Some(operand) = branch else { + return Expr::Unknown("switch"); + }; + if let Some(index) = self.table_index(&operand, 0) { + return index; + } + self.r.operand(&operand) + } + + /// The index an expression looks up, found by walking to the table read. + fn table_index(&self, operand: &Operand, depth: u32) -> Option { + if depth > 8 { + return None; + } + let Operand::Value(v) = operand else { + return None; + }; + let (block, index) = self.r.definition_site(*v)?; + let op = self.f.blocks.get(&block)?.ops.get(index)?; + let SsaKind::Op(o) = op.kind else { return None }; + match o { + // The read itself: the address is a constant table plus the index + // scaled by the entry size. + Op::Load => { + let address = op.inputs.first()?; + self.scaled_index(address, 0) + } + // Anything on the way there: the sign extension, the addition of + // the base, the shift. + _ => op + .inputs + .iter() + .find_map(|i| self.table_index(i, depth + 1)), + } + } + + /// The index inside an address of the form `table + index * size`. + fn scaled_index(&self, operand: &Operand, depth: u32) -> Option { + if depth > 8 { + return None; + } + let Operand::Value(v) = operand else { + return None; + }; + let (block, index) = self.r.definition_site(*v)?; + let op = self.f.blocks.get(&block)?.ops.get(index)?; + let SsaKind::Op(o) = op.kind else { return None }; + match o { + Op::IntAdd => { + // One side is the table's address, the other the scaled index. + let (a, b) = (op.inputs.first()?, op.inputs.get(1)?); + if a.as_const().is_some() { + self.scaled_index(b, depth + 1) + .or_else(|| Some(self.r.operand(b))) + } else if b.as_const().is_some() { + self.scaled_index(a, depth + 1) + .or_else(|| Some(self.r.operand(a))) + } else { + self.scaled_index(a, depth + 1) + .or_else(|| self.scaled_index(b, depth + 1)) + } + } + Op::IntLeft | Op::IntMul => { + let a = op.inputs.first()?; + self.scaled_index(a, depth + 1) + .or_else(|| Some(self.r.operand(a))) + } + Op::IntZExt | Op::IntSExt | Op::Copy => { + let a = op.inputs.first()?; + self.scaled_index(a, depth + 1) + .or_else(|| Some(self.r.operand(a))) + } + // A truncation is where the index starts: the table was indexed by + // the narrow value, so walking past it switches on the wide one + // and every argument whose low half is in range falls off the end + // of the switch instead of into its arm. + Op::SubPiece => Some(self.r.operand(operand)), + _ => None, + } + } + + /// True when a block's only job is to compute the branch condition, so the + /// condition can move into a `while` header. + fn head_is_only_a_test(&self, at: Addr) -> bool { + let Some(b) = self.f.blocks.get(&at) else { + return true; + }; + !b.ops.iter().any(|op| self.is_statement(op)) + } + + /// The branch condition of a block, inverted if asked. + fn condition(&mut self, at: Addr, invert: bool) -> Expr { + let cond = self + .f + .blocks + .get(&at) + .and_then(|b| { + b.ops + .iter() + .rev() + .find(|op| op.kind == SsaKind::Op(Op::CBranch)) + }) + .and_then(|op| op.inputs.get(1)) + .map(|i| self.r.operand(i)); + match cond { + Some(e) if invert => negate(e), + Some(e) => e, + None => { + // A branch whose condition the lifter did not model. Saying so + // is the point; the helper exists so the output still + // compiles. + self.unmodelled += 1; + Expr::Named("__condition", Vec::new()) + } + } + } + + /// The statements of one block, branches excluded. + fn statements(&mut self, out: &mut String, at: Addr, depth: usize) { + self.block_ops(out, at, depth); + self.phi_copies(out, at, depth); + } + + /// The assignments a block's successors' phis stand for. + /// + /// A phi is not an instruction; it says that a value came from one path or + /// another. Leaving SSA means writing that down as an assignment at the end + /// of each path, which is the only place it can be said in C. + fn phi_copies(&mut self, out: &mut String, at: Addr, depth: usize) { + let pad = " ".repeat(depth); + let Some(b) = self.f.blocks.get(&at) else { + return; + }; + for successor in &b.successors { + let Some(s) = self.f.blocks.get(successor) else { + continue; + }; + let Some(slot) = s.predecessors.iter().position(|p| *p == at) else { + continue; + }; + for op in &s.ops { + if op.kind != SsaKind::Phi { + continue; + } + let (Some(v), Some(input)) = (op.out, op.inputs.get(slot)) else { + continue; + }; + let Some(name) = self.r.locals.get(&v) else { + continue; + }; + // An assignment from itself says nothing. + let value = self.r.integer(self.r.operand(input), input); + let text = format!("{value}"); + if text == *name { + continue; + } + let _ = writeln!(out, "{pad}{name} = {text};"); + } + } + } + + fn block_ops(&mut self, out: &mut String, at: Addr, depth: usize) { + let pad = " ".repeat(depth); + let Some(b) = self.f.blocks.get(&at) else { + return; + }; + for (index, op) in b.ops.iter().enumerate() { + if !self.is_statement(op) { + continue; + } + let SsaKind::Op(o) = op.kind else { continue }; + match o { + Op::Store => { + // A store to a known field says so, the same as a load. + if let Some(field) = self + .r + .field_access(op.inputs.first(), op.size) + .filter(|_| op.size > 0) + { + if let Some(value) = op + .inputs + .get(1) + .map(|i| self.r.integer(self.r.operand(i), i)) + { + let _ = writeln!(out, "{pad}{field} = {value};"); + continue; + } + } + let addr = op + .inputs + .first() + .map(|i| self.r.integer(self.r.operand(i), i)); + let val = op + .inputs + .get(1) + .map(|i| self.r.integer(self.r.operand(i), i)); + if let (Some(a), Some(v)) = (addr, val) { + let _ = writeln!( + out, + "{pad}*({}*){} = {};", + c_type(op.size), + parenthesize(&a), + v + ); + } + } + Op::Return => { + let _ = writeln!(out, "{pad}{}", self.return_statement(at)); + } + Op::Call | Op::CallInd => { + let void = match op.inputs.first().and_then(|i| i.as_const()) { + Some(target) => self.callees.get(&target).is_some_and(|c| !c.returns_value), + None => false, + }; + let e = match (o, op.inputs.first().and_then(|i| i.as_const())) { + (Op::Call, Some(target)) => { + let callee = self.callees.get(&target); + let name = callee + .map(|c| c.name.clone()) + .unwrap_or_else(|| crate::expr::default_call_name(target)); + Expr::Call(name, self.arguments(at, index, callee)) + } + _ => self.r.expr(op), + }; + match op.out.and_then(|v| self.r.locals.get(&v)).filter(|_| !void) { + // The local is an integer and the callee may be + // declared to return a pointer, which C will not + // assign without being told. + Some(name) => { + let _ = writeln!(out, "{pad}{name} = (uint64_t)({e});"); + } + None => { + let _ = writeln!(out, "{pad}{e};"); + } + } + } + Op::Unimplemented => { + self.unmodelled += 1; + let _ = writeln!(out, "{pad}__unmodelled(0x{:x});", op.addr.0); + } + Op::Branch => { + let Some(target) = tail_call(self.f, op) else { + continue; + }; + let callee = self.callees.get(&target); + let name = callee + .map(|c| c.name.clone()) + .unwrap_or_else(|| crate::expr::default_call_name(target)); + let e = Expr::Call(name, self.arguments(at, index, callee)); + let _ = writeln!(out, "{pad}{e};"); + let _ = writeln!(out, "{pad}{}", self.return_statement(at)); + } + Op::BranchInd => { + // A computed branch that structuring did not turn into a + // switch. C's own computed goto needs a label table this + // does not have, so the target is named and the control + // transfer is said rather than spelled. + if self.switches.contains(&at) { + continue; + } + if let Some(t) = op.inputs.first().map(|i| self.r.operand(i)) { + let target = self.r.integer(t, &op.inputs[0]); + let _ = writeln!(out, "{pad}__indirect_branch({target});"); + let _ = writeln!(out, "{pad}{}", self.return_statement(at)); + } + } + _ => { + // Anything else that reaches here has a named output. + if let Some(name) = op.out.and_then(|v| self.r.locals.get(&v)) { + let _ = writeln!(out, "{pad}{name} = {};", self.r.expr(op)); + } + } + } + } + } + + /// True when an operation has to appear as a statement rather than being + /// inlined into whatever reads it. + fn is_statement(&self, op: &SsaOp) -> bool { + let SsaKind::Op(o) = op.kind else { + // A phi is a merge, written where the paths join, not here. + return false; + }; + match o { + Op::Store | Op::Return | Op::Call | Op::CallInd | Op::Unimplemented | Op::BranchInd => { + true + } + // A branch out of the function is a tail call and has to be said. + // Any other branch is control flow the region tree already carries. + Op::Branch => tail_call(self.f, op).is_some(), + Op::CBranch => false, + // Everything else is an expression, needed as a statement only when + // its result got a name. + _ => op.out.is_some_and(|v| self.r.locals.contains_key(&v)), + } + } + + /// The `return` a block ends with, converted to the declared type. + fn return_statement(&self, at: Addr) -> String { + let value = self.result(at); + match (&self.returns, value.is_empty()) { + (Some(ty), _) if ty == "void" => "return;".to_string(), + // The value is already rendered, so the cast needs its own + // parentheses: a cast binds tighter than anything in an + // expression and would otherwise apply to the first term. + (Some(ty), false) => format!("return ({ty})({value});"), + // Declared to return something, with nothing found that it + // returns: an import thunk, or a result the rebuilder could not + // name. Zero would be a claim about the value; the helper says + // only that it is not known, which is what was established. + (Some(ty), true) => format!("return ({ty})__clobbered();"), + (None, false) => format!("return {value};"), + (None, true) => "return;".to_string(), + } + } + + /// The arguments a call passes, read out of the convention's registers. + /// + /// The value each one holds is whatever last wrote it before the call. A + /// register nothing wrote is passed as zero rather than left out, because + /// the count has to match what the callee declares. + fn arguments(&self, at: Addr, index: usize, callee: Option<&Callee>) -> Vec { + let Some(callee) = callee else { + return Vec::new(); + }; + let n = callee.arity; + let pointers = &callee.pointer_parameters; + let mut out = Vec::new(); + for (slot, offset) in self.r.abi.integer_arguments.iter().enumerate().take(n) { + let value = match self.value_before(at, index, *offset) { + Some(v) => self.r.operand(&Operand::Value(v)), + None => Expr::Const(0, 8), + }; + // A constant written into a 64-bit register by a 32-bit move is + // held as the whole register, so `-512` arrives as `0xfffffe00`. + // The callee reads four bytes of it, so four bytes is what the + // call passes, and the reader gets the number back. + let value = match (value, callee.parameter_widths.get(slot)) { + (Expr::Const(v, size), Some(&w)) if w > 0 && w < size => { + Expr::Const(v & (u64::MAX >> (64 - w as u32 * 8)), w) + } + (v, _) => v, + }; + // A parameter declared as a pointer needs the argument cast to it: + // the machine passes bits and C wants to be told what they are. + match pointers.get(slot) { + Some(true) => out.push(Expr::Cast("void *", Box::new(value))), + _ => out.push(value), + } + } + // A callee with more parameters than there are argument registers + // takes the rest on the stack, which this does not recover; the count + // still has to match what it declares. + while out.len() < n { + out.push(Expr::Const(0, 8)); + } + out + } + + /// The value a register held just before an operation. + fn value_before(&self, at: Addr, index: usize, offset: u64) -> Option { + let b = self.f.blocks.get(&at)?; + if let Some(v) = b.ops[..index] + .iter() + .rev() + .filter_map(|op| op.out) + .find(|v| v.location.space == Space::Register && v.location.offset == offset) + { + return Some(v); + } + // Nothing in this block wrote it, so whatever reached the block did. + // Without liveness this takes the newest version, which is right + // whenever the register is written once. + self.f + .blocks + .values() + .flat_map(|other| other.ops.iter()) + .filter_map(|op| op.out) + .filter(|v| v.location.space == Space::Register && v.location.offset == offset) + .max_by_key(|v| v.version) + } + + /// What a function returns: whatever last reached the result register. + fn result(&self, at: Addr) -> String { + let Some(offset) = self.result else { + return String::new(); + }; + let in_block = self.f.blocks.get(&at).and_then(|b| { + b.ops + .iter() + .rev() + .filter_map(|op| op.out) + .find(|v| v.location.space == Space::Register && v.location.offset == offset) + }); + // Nothing in this block wrote it, so the value came from wherever it + // was last written: the newest version is the one that reaches here. + let value = in_block.or_else(|| { + self.f + .blocks + .values() + .flat_map(|b| b.ops.iter()) + .filter_map(|op| op.out) + .filter(|v| v.location.space == Space::Register && v.location.offset == offset) + .max_by_key(|v| v.version) + }); + match value { + Some(v) => match self.r.locals.get(&v) { + Some(name) => name.clone(), + None => format!("{}", self.r.operand(&Operand::Value(v))), + }, + None => "0".to_string(), + } + } +} + +/// Wrap in parentheses unless it is already atomic. +fn parenthesize(e: &Expr) -> String { + match e { + Expr::Const(..) | Expr::Local(_) | Expr::Input(..) => format!("{e}"), + _ => format!("({e})"), + } +} + +/// Values each block defines, for callers that want the mapping. +pub fn block_definitions(f: &SsaFunction) -> BTreeMap> { + f.blocks + .iter() + .map(|(a, b)| (*a, b.ops.iter().filter_map(|op| op.out).collect())) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn negation_flips_comparisons_rather_than_wrapping_them() { + let a = Box::new(Expr::Local("a".into())); + let b = Box::new(Expr::Local("b".into())); + assert_eq!( + format!("{}", negate(Expr::Binary("<", a.clone(), b.clone()))), + "a >= b" + ); + assert_eq!( + format!( + "{}", + negate(Expr::Unary("!", Box::new(Expr::Local("c".into())))) + ), + "c" + ); + } +} diff --git a/crates/e5r-decomp/src/expr.rs b/crates/e5r-decomp/src/expr.rs new file mode 100644 index 0000000..59a9c97 --- /dev/null +++ b/crates/e5r-decomp/src/expr.rs @@ -0,0 +1,1318 @@ +//! Expressions rebuilt from SSA. +//! +//! An SSA operation is a machine-shaped thing: one operator, operands that are +//! versioned locations. C is tree-shaped. Rebuilding means inlining a value's +//! definition into its use, which is only safe when the value is used once and +//! nothing between the two changes what it depends on. +//! +//! The rule here is deliberately conservative. A value used more than once +//! becomes a named local rather than being duplicated, because duplicating a +//! load or a call would change what the code does, and duplicating arithmetic +//! makes the output longer rather than clearer. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +use e5r_ir::abi::Abi; +use e5r_ir::op::Op; +use e5r_ir::ssa::{Location, Operand, SsaFunction, SsaKind, SsaOp, Value}; + +use crate::emit::Prototype; + +/// A rebuilt expression. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Expr { + /// A literal. + Const(u64, u8), + /// A literal read as a floating point number, which is how the bit pattern + /// was meant when a floating point operation consumes it. + FConst(u64, u8), + /// A named local. + Local(String), + /// A value that arrived from outside: an argument, or a register the + /// function inherited. The name is resolved once, where the convention is + /// known. + Input(Location, String), + /// A unary operator applied to an expression. + Unary(&'static str, Box), + /// A binary operator. + Binary(&'static str, Box, Box), + /// A memory read. + Deref(Box, u8), + /// A field of a structure reached through a pointer. + Field(Box, String), + /// A field of one element of an array of structures. + Element(Box, Box, String), + /// A named operation C has no operator for. + Named(&'static str, Vec), + /// A call to a function, named. + Call(String, Vec), + /// A call through an expression. + CallInd(Box), + /// A cast, which the machine performs and C has to be told about. + Cast(&'static str, Box), + /// Something the rebuilder does not model, named by its operator. + Unknown(&'static str), +} + +impl Expr { + /// True when the expression is a literal. + pub fn is_const(&self) -> bool { + matches!(self, Expr::Const(..)) + } + + /// How deep the tree goes, used to keep one statement from growing past + /// what a person can read. + pub fn depth(&self) -> usize { + match self { + Expr::Unary(_, a) | Expr::Cast(_, a) | Expr::Deref(a, _) => 1 + a.depth(), + Expr::Binary(_, a, b) => 1 + a.depth().max(b.depth()), + Expr::Call(_, args) => 1 + args.iter().map(Expr::depth).max().unwrap_or(0), + Expr::CallInd(a) => 1 + a.depth(), + _ => 1, + } + } +} + +/// The precedence of an operator, so parentheses appear only where they are +/// needed. C's table, with the operators this emits. +fn precedence(op: &str) -> u8 { + match op { + "*" | "/" | "%" => 10, + "+" | "-" => 9, + "<<" | ">>" => 8, + "<" | "<=" | ">" | ">=" => 7, + "==" | "!=" => 6, + "&" => 5, + "^" => 4, + "|" => 3, + "&&" => 2, + "||" => 1, + _ => 11, + } +} + +/// Read a constant as signed at its own width. +fn sign_extend(v: u64, size: u8) -> i64 { + if size == 0 || size >= 8 { + return v as i64; + } + let bits = size as u32 * 8; + ((v << (64 - bits)) as i64) >> (64 - bits) +} + +impl fmt::Display for Expr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.render(f, 0) + } +} + +impl Expr { + fn render(&self, f: &mut fmt::Formatter<'_>, outer: u8) -> fmt::Result { + match self { + Expr::Const(v, size) => { + // A small value reads better in decimal; a large one is almost + // always a mask or an address and reads better in hex. + let signed = sign_extend(*v, *size); + if *size <= 8 && signed < 0 && signed > -0x10000 { + write!(f, "{signed}") + } else if *v < 0x10000 { + write!(f, "{v}") + } else { + write!(f, "{v:#x}") + } + } + Expr::FConst(bits, size) => { + let v = if *size == 4 { + f32::from_bits(*bits as u32) as f64 + } else { + f64::from_bits(*bits) + }; + // A round number still reads as one, and anything else keeps + // enough digits to name the same value again. + if v == v.trunc() && v.abs() < 1e15 { + write!(f, "{v:.1}") + } else { + write!(f, "{v}") + } + } + Expr::Local(n) => f.write_str(n), + Expr::Input(_, name) => f.write_str(name), + Expr::Unary(op, a) => { + f.write_str(op)?; + a.render(f, 11) + } + Expr::Binary(op, a, b) => { + let p = precedence(op); + let parens = p < outer; + if parens { + f.write_str("(")?; + } + a.render(f, p)?; + write!(f, " {op} ")?; + // The right operand of a left-associative operator needs + // parentheses at equal precedence. + b.render(f, p + 1)?; + if parens { + f.write_str(")")?; + } + Ok(()) + } + Expr::Field(base, name) => { + base.render(f, 12)?; + write!(f, "->{name}") + } + Expr::Element(base, index, name) => { + base.render(f, 12)?; + f.write_str("[")?; + index.render(f, 0)?; + write!(f, "].{name}") + } + Expr::Deref(a, size) => { + write!(f, "*({} *)", c_type(*size))?; + a.render(f, 11) + } + Expr::Named(name, args) => { + f.write_str(name)?; + f.write_str("(")?; + for (n, a) in args.iter().enumerate() { + if n > 0 { + f.write_str(", ")?; + } + a.render(f, 0)?; + } + f.write_str(")") + } + Expr::Call(name, args) => { + write!(f, "{name}(")?; + for (n, a) in args.iter().enumerate() { + if n > 0 { + f.write_str(", ")?; + } + a.render(f, 0)?; + } + f.write_str(")") + } + Expr::CallInd(a) => { + f.write_str("(*")?; + a.render(f, 11)?; + f.write_str(")()") + } + Expr::Cast(t, a) => { + // A signed cast of a constant C already gives that type is + // noise. `(int32_t)81` and `81` convert identically in every + // context, because a decimal literal is already `int` and the + // usual arithmetic conversions then treat the two the same. + // Only for values `int` can hold, and only for signed targets: + // `(uint32_t)81` beside a signed operand chooses unsigned + // division, which dropping it would silently change. + if let Expr::Const(v, size) = a.as_ref() + && t.starts_with("int") + && (0..=0x7fff_ffff).contains(&sign_extend(*v, *size)) + { + return a.render(f, outer); + } + write!(f, "({t})")?; + a.render(f, 11) + } + Expr::Unknown(what) => write!(f, "__{what}()"), + } + } +} + +/// The C type for a size in bytes. +/// What to call a function whose name is not known. +pub fn default_call_name(target: u64) -> String { + format!("sub_{target:x}") +} + +/// The helper that reads a bit pattern as a number of this width. +pub fn reinterpret_to_float(size: u8) -> &'static str { + if size == 4 { "__flt" } else { "__dbl" } +} + +/// The helper that reads a number of this width as its bits. +pub fn reinterpret_to_bits(size: u8) -> &'static str { + if size == 4 { "__bits32" } else { "__bits" } +} + +/// The floating point type of a width. +pub fn float_type(size: u8) -> &'static str { + if size == 4 { "float" } else { "double" } +} + +/// The unsigned C type of a width. +pub fn c_type(size: u8) -> &'static str { + match size { + 1 => "uint8_t", + 2 => "uint16_t", + 4 => "uint32_t", + 16 => "__int128", + _ => "uint64_t", + } +} + +/// The signed C type for a size in bytes. +fn signed_type(size: u8) -> &'static str { + match size { + 1 => "int8_t", + 2 => "int16_t", + 4 => "int32_t", + _ => "int64_t", + } +} + +/// A readable name for a location nothing defines. +/// +/// On AArch64 the first eight registers carry arguments, so they are named as +/// such; anything else keeps its machine name, which is honest about not +/// knowing where the value came from. +pub fn input_name(l: Location, abi: &Abi) -> String { + if l.space == e5r_ir::op::Space::Stack { + return slot_name(l); + } + if l.space != e5r_ir::op::Space::Register { + return format!("mem{:x}", l.offset); + } + if l.offset == abi.stack_pointer { + return "sp".to_string(); + } + // A register the convention passes arguments in, read before anything + // wrote it, is an argument. Anything else read that way is a register the + // function inherited, which is named rather than invented. + if let Some(name) = abi.argument_name(l.offset) { + return name; + } + if l.size == 1 { + return format!("flag{}", l.offset % 8); + } + format!("reg{:x}", l.offset) +} + +/// How many times each value is read. +pub fn use_counts(f: &SsaFunction) -> BTreeMap { + let mut out: BTreeMap = BTreeMap::new(); + for b in f.blocks.values() { + for op in &b.ops { + for i in &op.inputs { + if let Operand::Value(v) = i { + *out.entry(*v).or_default() += 1; + } + } + } + } + out +} + +/// How an address was built from an incoming pointer. +struct Walk { + register: u64, + offset: i64, + index: Option<(Expr, u64)>, +} + +/// What to call a field at an offset, since nothing named it. +pub fn field_name(offset: i64) -> String { + if offset < 0 { + format!("field_m{:x}", -offset) + } else { + format!("field_{offset:x}") + } +} + +/// The name a promoted stack slot gets, from its offset. +pub fn slot_name(l: Location) -> String { + let offset = l.offset as i64; + if offset < 0 { + format!("local_{:x}", -offset) + } else { + // Above the stack pointer on entry is the caller's frame, which is + // where arguments past the registers arrive. + format!("arg_s{offset:x}") + } +} + +/// True when a location is an argument the caller passed on the stack. +pub fn is_stack_argument(l: Location) -> bool { + l.space == e5r_ir::op::Space::Stack && (l.offset as i64) > 0 +} + +/// Negate a condition without stacking `!` on something already negated. +pub fn negate(e: Expr) -> Expr { + match e { + Expr::Unary("!", inner) => *inner, + Expr::Binary("==", a, b) => Expr::Binary("!=", a, b), + Expr::Binary("!=", a, b) => Expr::Binary("==", a, b), + Expr::Binary("<", a, b) => Expr::Binary(">=", a, b), + Expr::Binary("<=", a, b) => Expr::Binary(">", a, b), + Expr::Binary(">", a, b) => Expr::Binary("<=", a, b), + Expr::Binary(">=", a, b) => Expr::Binary("<", a, b), + other => Expr::Unary("!", Box::new(other)), + } +} + +/// Which locations hold floating point values. +/// +/// Anything a floating point operation reads or writes, which is as much type +/// recovery as this needs: the operation says what the bits mean. +pub fn float_locations(f: &SsaFunction) -> BTreeSet { + let mut out = BTreeSet::new(); + for b in f.blocks.values() { + for op in &b.ops { + let SsaKind::Op(o) = op.kind else { continue }; + if !is_float_op(o) { + continue; + } + if let Some(v) = op.out { + if !produces_bool(o) { + out.insert(v.location); + } + } + // The integer side of a conversion is not floating. + if matches!(o, Op::IntToFloat | Op::UIntToFloat) { + continue; + } + for i in &op.inputs { + match i { + Operand::Value(v) => { + out.insert(v.location); + } + Operand::Undefined(l) => { + out.insert(*l); + } + Operand::Const(..) => {} + } + } + } + } + out +} + +fn is_float_op(o: Op) -> bool { + matches!( + o, + Op::FloatAdd + | Op::FloatSub + | Op::FloatMul + | Op::FloatDiv + | Op::FloatMulAdd + | Op::FloatNeg + | Op::FloatAbs + | Op::FloatSqrt + | Op::FloatMax + | Op::FloatMin + | Op::FloatEqual + | Op::FloatNotEqual + | Op::FloatLess + | Op::FloatLessEqual + | Op::FloatNan + | Op::FloatTrunc + | Op::FloatRound + | Op::FloatCeil + | Op::FloatFloor + | Op::FloatConvert + | Op::FloatToInt + | Op::FloatToUInt + | Op::IntToFloat + | Op::UIntToFloat + ) +} + +fn produces_bool(o: Op) -> bool { + matches!( + o, + Op::FloatEqual + | Op::FloatNotEqual + | Op::FloatLess + | Op::FloatLessEqual + | Op::FloatNan + | Op::FloatToInt + | Op::FloatToUInt + ) +} + +/// Rebuilds expressions from an SSA function. +pub struct Rebuilder<'a> { + f: &'a SsaFunction, + /// The calling convention, which says what an incoming register means. + pub abi: Abi, + defs: BTreeMap, + uses: BTreeMap, + /// Values that became named locals because they are read more than once. + pub locals: BTreeMap, + /// Locations that hold floating point values, which is what decides how a + /// literal is printed and how a parameter is declared. + pub floats: BTreeSet, + /// Names for incoming registers, from whatever knew better than the + /// convention did. + pub names: BTreeMap, + /// Incoming registers whose declared type is a pointer. C scales + /// arithmetic on those by the size of what they point at, and the machine + /// has already done the scaling, so they are cast back to integers + /// wherever they are used as addresses. + pub pointers: BTreeSet, + /// The declared width of an incoming register's value, which is not the + /// width of the register: a `float` arrives in a sixteen-byte one. + pub sizes: BTreeMap, + /// Fields seen through each incoming pointer, so an access at a known + /// offset reads as the field it is rather than as arithmetic. + pub fields: BTreeMap>, + /// The element size of each of those, when the pointer walks an array. + pub strides: BTreeMap, + /// What a declaration called the field at an offset, by pointer register. + /// + /// An offset missing from here is named after itself, which is how the + /// output tells the two apart: `p->tag` is a name somebody wrote down and + /// `p->field_8` is one this crate made up from the offset the code used. + /// Nothing is lost by the fallback except the name, so a shape nobody + /// declared still reads as a structure. + pub field_names: BTreeMap>, +} + +impl<'a> Rebuilder<'a> { + /// Prepare to rebuild, naming the values that need names. + pub fn new(f: &'a SsaFunction) -> Rebuilder<'a> { + let defs = f.definitions(); + let uses = use_counts(f); + let mut locals = BTreeMap::new(); + let mut n = 0; + // Values read more than once, and every phi, get a name. A phi is a + // merge of paths and inlining it would mean writing the merge out at + // each use. + for (value, (block, index)) in &defs { + let Some(op) = f.blocks.get(block).and_then(|b| b.ops.get(*index)) else { + continue; + }; + let multiple = uses.get(value).copied().unwrap_or(0) > 1; + // A call's result gets a name whether or not it is read twice: the + // call is a statement, and inlining its result would write the + // call out again and make it happen twice. + let call = matches!(op.kind, SsaKind::Op(Op::Call) | SsaKind::Op(Op::CallInd)); + if multiple || call || op.kind == SsaKind::Phi { + locals.insert(*value, format!("v{n}")); + n += 1; + } + } + Rebuilder { + f, + names: BTreeMap::new(), + pointers: BTreeSet::new(), + sizes: BTreeMap::new(), + fields: BTreeMap::new(), + strides: BTreeMap::new(), + field_names: BTreeMap::new(), + floats: float_locations(f), + abi: e5r_ir::abi::of(&f.arch), + defs, + uses, + locals, + } + } + + /// An expression read as an integer, whatever it was declared as. + /// + /// A floating value has to be reinterpreted, and a pointer has to be cast, + /// because C scales arithmetic on a pointer and the machine already did. + pub fn integer(&self, e: Expr, o: &Operand) -> Expr { + let location = match o { + Operand::Value(v) => Some(v.location), + Operand::Undefined(l) => Some(*l), + Operand::Const(..) => None, + }; + let Some(l) = location else { return e }; + // A value a floating point operation produced is a number whatever + // location it landed in: the conversion ops write one into a register + // this pass has no other reason to call floating. + if !self.floats.contains(&l) && self.produces_float(o) { + let size = match o { + Operand::Value(v) => v.location.size, + _ => l.size, + }; + return Expr::Named(reinterpret_to_bits(size.min(8)), vec![e]); + } + if self.floats.contains(&l) { + let size = if l.space == e5r_ir::op::Space::Register { + self.sizes.get(&l.offset).copied().unwrap_or(l.size) + } else { + l.size + }; + return Expr::Named(reinterpret_to_bits(size), vec![e]); + } + if l.space == e5r_ir::op::Space::Register && self.pointers.contains(&l.offset) { + return Expr::Cast("uint64_t", Box::new(e)); + } + e + } + + /// The field an address names, when it is a known offset from a pointer + /// whose shape was recovered. + /// + /// Only a constant offset from the pointer itself: anything with an index + /// in it is walking an array, and calling that a field would be wrong. + pub fn field_access(&self, address: Option<&Operand>, size: u8) -> Option { + let walk = self.pointer_offset(address?, 0)?; + let shape = self.fields.get(&walk.register)?; + let (at, width) = shape.iter().find(|(at, _)| *at == walk.offset)?; + if *width != size { + return None; + } + let name = self.names.get(&walk.register).cloned()?; + let base = Expr::Input( + Location { + space: e5r_ir::op::Space::Register, + offset: walk.register, + size: 8, + }, + name, + ); + let field = self.field_label(walk.register, *at); + match walk.index { + // An index scaled by the element size is an array subscript; one + // scaled by anything else is arithmetic this does not understand. + Some((index, scale)) if self.strides.get(&walk.register) == Some(&scale) => { + Some(Expr::Element(Box::new(base), Box::new(index), field)) + } + Some(_) => None, + None => Some(Expr::Field(Box::new(base), field)), + } + } + + /// What to call the field at an offset through one incoming pointer. + /// + /// The declared name when a declaration gave one, and otherwise a name + /// built from the offset, which says on its face that nothing declared it. + fn field_label(&self, register: u64, offset: i64) -> String { + self.field_names + .get(®ister) + .and_then(|m| m.get(&offset)) + .cloned() + .unwrap_or_else(|| field_name(offset)) + } + + /// An address written as an incoming register, a constant, and at most one + /// scaled index. + fn pointer_offset(&self, o: &Operand, depth: u32) -> Option { + if depth > 8 { + return None; + } + match o { + Operand::Undefined(l) if l.space == e5r_ir::op::Space::Register => Some(Walk { + register: l.offset, + offset: 0, + index: None, + }), + Operand::Value(v) => { + let op = self.definition(*v)?; + let SsaKind::Op(kind) = op.kind else { + return None; + }; + match kind { + Op::Copy => self.pointer_offset(op.inputs.first()?, depth + 1), + Op::IntAdd => { + let (a, b) = (op.inputs.first()?, op.inputs.get(1)?); + if let Some(k) = b.as_const() { + let mut walk = self.pointer_offset(a, depth + 1)?; + walk.offset += k as i64; + return Some(walk); + } + // One side is the pointer and the other a scaled index. + let (base, other) = match self.pointer_offset(a, depth + 1) { + Some(w) => (w, b), + None => (self.pointer_offset(b, depth + 1)?, a), + }; + if base.index.is_some() { + return None; + } + let (index, scale) = self.scaled(other)?; + Some(Walk { + index: Some((index, scale)), + ..base + }) + } + _ => None, + } + } + _ => None, + } + } + + /// An index multiplied by a constant, however it was written. + fn scaled(&self, o: &Operand) -> Option<(Expr, u64)> { + let Operand::Value(v) = o else { return None }; + let op = self.definition(*v)?; + let SsaKind::Op(kind) = op.kind else { + return None; + }; + match kind { + Op::IntLeft => { + let n = op.inputs.get(1)?.as_const()?; + if n >= 32 { + return None; + } + Some((self.operand(op.inputs.first()?), 1u64 << n)) + } + Op::IntMul => { + let n = op.inputs.get(1)?.as_const()?; + Some((self.operand(op.inputs.first()?), n)) + } + Op::Copy | Op::IntSExt | Op::IntZExt => self.scaled(op.inputs.first()?), + _ => None, + } + } + + /// True when an operand holds a value a floating point operation made. + fn produces_float(&self, o: &Operand) -> bool { + let Operand::Value(v) = o else { return false }; + let Some(op) = self.definition(*v) else { + return false; + }; + let SsaKind::Op(kind) = op.kind else { + return false; + }; + is_float_op(kind) && !produces_bool(kind) + } + + /// What to call a value that arrived from outside. + pub fn name_of(&self, l: Location) -> String { + if l.space == e5r_ir::op::Space::Register { + if let Some(name) = self.names.get(&l.offset) { + return name.clone(); + } + } + input_name(l, &self.abi) + } + + /// The expression for one operand. + pub fn operand(&self, o: &Operand) -> Expr { + match o { + Operand::Const(v, s) => Expr::Const(*v, *s), + Operand::Undefined(l) => Expr::Input(*l, self.name_of(*l)), + Operand::Value(v) => { + if let Some(name) = self.locals.get(v) { + return Expr::Local(name.clone()); + } + match self.definition(*v) { + Some(op) => self.expr(op), + None => Expr::Input(v.location, self.name_of(v.location)), + } + } + } + } + + /// Where a value is defined: which block, and which operation in it. + pub fn definition_site(&self, v: Value) -> Option<(e5r_core::Addr, usize)> { + self.defs.get(&v).copied() + } + + fn definition(&self, v: Value) -> Option<&'a SsaOp> { + let (block, index) = self.defs.get(&v)?; + self.f.blocks.get(block)?.ops.get(*index) + } + + /// The expression one operation computes. + pub fn expr(&self, op: &SsaOp) -> Expr { + let SsaKind::Op(o) = op.kind else { + // A phi always has a name, so reaching one here means the naming + // missed it; say so rather than inventing a value. + return Expr::Unknown("phi"); + }; + let a = || { + op.inputs + .first() + .map(|i| self.operand(i)) + .unwrap_or(Expr::Unknown("missing")) + }; + let b = || { + op.inputs + .get(1) + .map(|i| self.operand(i)) + .unwrap_or(Expr::Unknown("missing")) + }; + let bin = |sym: &'static str| Expr::Binary(sym, Box::new(a()), Box::new(b())); + let signed_at = |width: u8, sym: &'static str| { + let t = signed_type(width); + Expr::Binary( + sym, + Box::new(Expr::Cast(t, Box::new(a()))), + Box::new(Expr::Cast(t, Box::new(b()))), + ) + }; + // A comparison's answer is one byte whatever it compared, so its width + // comes from the operands. Signed arithmetic's answer is as wide as + // the operation, and its operands are read at that width however wide + // the location holding them is. + let compared = |sym: &'static str| { + let width = op + .inputs + .first() + .map(|i| i.size()) + .unwrap_or(op.size) + .max(op.inputs.get(1).map(|i| i.size()).unwrap_or(0)); + signed_at(width, sym) + }; + let signed_bin = |sym: &'static str| signed_at(op.size, sym); + + // An operand is read as whatever the operation consuming it expects. + // A value the machine holds in a register has no type of its own, so + // where the two disagree the reinterpretation is written down rather + // than left for the C compiler to do something else with. + // The width the operation works at, which is what a literal operand + // means whatever width it was stored as. + let width = op + .inputs + .first() + .map(|i| i.size()) + .filter(|s| *s == 4 || *s == 8) + .unwrap_or(op.size); + let float_in = |e: Expr, o: &Operand| -> Expr { + match o { + Operand::Const(v, _) => Expr::FConst(*v, width), + Operand::Value(v) if !self.floats.contains(&v.location) => { + Expr::Named(reinterpret_to_float(width), vec![e]) + } + Operand::Undefined(l) if !self.floats.contains(l) => { + Expr::Named(reinterpret_to_float(width), vec![e]) + } + _ => e, + } + }; + let int_in = |e: Expr, o: &Operand| -> Expr { self.integer(e, o) }; + + // A literal consumed by a floating point operation is a number, not a + // bit pattern, and printing it as one is the difference between `1.0` + // and `0x3ff0000000000000`. + let fa = || match op.inputs.first() { + Some(o) => float_in(a(), o), + None => a(), + }; + let fb = || match op.inputs.get(1) { + Some(o) => float_in(b(), o), + None => b(), + }; + let ia = || match op.inputs.first() { + Some(o) => int_in(a(), o), + None => a(), + }; + let ib = || match op.inputs.get(1) { + Some(o) => int_in(b(), o), + None => b(), + }; + // The width of the operand an extension reads, which is the width it + // extends from. + let input_width = op.inputs.first().map(|i| i.size()).unwrap_or(op.size); + let ibin = |sym: &'static str| Expr::Binary(sym, Box::new(ia()), Box::new(ib())); + let fbin = |sym: &'static str| Expr::Binary(sym, Box::new(fa()), Box::new(fb())); + + match o { + // A copy of a value whose declared type is not an integer still + // lands in an integer local, so the conversion is written down. + Op::Copy => ia(), + Op::Load => match self.field_access(op.inputs.first(), op.size) { + Some(field) => field, + None => Expr::Deref(Box::new(ia()), op.size), + }, + Op::IntAdd => ibin("+"), + Op::IntSub => ibin("-"), + Op::IntMul => ibin("*"), + Op::IntDiv => ibin("/"), + Op::IntRem => ibin("%"), + Op::IntSDiv => signed_bin("/"), + Op::IntSRem => signed_bin("%"), + Op::IntAnd => ibin("&"), + Op::IntOr => ibin("|"), + Op::IntXor => ibin("^"), + Op::IntNot => Expr::Unary("~", Box::new(ia())), + Op::IntNegate => Expr::Unary("-", Box::new(ia())), + Op::IntLeft => ibin("<<"), + Op::IntRight => ibin(">>"), + Op::IntSRight => signed_bin(">>"), + Op::IntEqual => ibin("=="), + Op::IntNotEqual => ibin("!="), + Op::IntLess => ibin("<"), + Op::IntLessEqual => ibin("<="), + Op::IntSLess => compared("<"), + Op::IntSLessEqual => compared("<="), + Op::BoolAnd => bin("&&"), + Op::BoolOr => bin("||"), + Op::BoolXor => bin("^"), + // A negated comparison is the opposite comparison, which is what + // the source said before the machine turned it into flags. + Op::BoolNot => negate(a()), + // Both extensions name the width they extend *from* as well as + // the one they extend to. Every local is declared at its whole + // register's width, so without the inner cast the bits above the + // source's width come along and the extension does nothing. + Op::IntZExt => Expr::Cast( + c_type(op.size), + Box::new(Expr::Cast(c_type(input_width), Box::new(ia()))), + ), + Op::IntSExt => Expr::Cast( + signed_type(op.size), + Box::new(Expr::Cast(signed_type(input_width), Box::new(ia()))), + ), + // A shift by a byte count, which is how a narrow read is expressed. + Op::SubPiece => match op.inputs.get(1).and_then(|i| i.as_const()) { + // Taking the low bytes of a register that holds a narrower + // float is that float, not its bit pattern: a `float` + // argument arrives in the low half of a vector register, and + // `__bits32` around it would say the caller passed bits. + Some(0) if self.narrow_float(op) => a(), + Some(0) => Expr::Cast(c_type(op.size), Box::new(ia())), + Some(n) => Expr::Cast( + c_type(op.size), + Box::new(Expr::Binary( + ">>", + Box::new(ia()), + Box::new(Expr::Const(n * 8, 1)), + )), + ), + None => Expr::Unknown("subpiece"), + }, + Op::CallInd => Expr::Named("__callind", vec![ia()]), + Op::Call => match op.inputs.first().and_then(|i| i.as_const()) { + Some(target) => Expr::Call(default_call_name(target), Vec::new()), + None => Expr::Unknown("call"), + }, + Op::PopCount => Expr::Named("__popcount", vec![a()]), + Op::IntMulHigh => Expr::Binary("__mulhi", Box::new(a()), Box::new(b())), + Op::IntSMulHigh => Expr::Binary("__smulhi", Box::new(a()), Box::new(b())), + // The double-width divides take their dividend in two halves, + // which C has no notation for; the call form says so plainly. + Op::IntDiv128 | Op::IntSDiv128 | Op::IntRem128 | Op::IntSRem128 => { + let name = match o { + Op::IntDiv128 => "__udiv128", + Op::IntSDiv128 => "__sdiv128", + Op::IntRem128 => "__urem128", + _ => "__srem128", + }; + Expr::Named(name, op.inputs.iter().map(|i| self.operand(i)).collect()) + } + Op::FloatAdd => fbin("+"), + Op::FloatSub => fbin("-"), + Op::FloatMul => fbin("*"), + Op::FloatDiv => fbin("/"), + Op::FloatEqual => fbin("=="), + Op::FloatNotEqual => fbin("!="), + Op::FloatLess => fbin("<"), + Op::FloatLessEqual => fbin("<="), + Op::FloatNeg => Expr::Unary("-", Box::new(fa())), + Op::FloatAbs => Expr::Named("__fabs", vec![a()]), + Op::FloatSqrt => Expr::Named("__sqrt", vec![a()]), + Op::FloatMax => Expr::Named("__fmax", vec![a(), b()]), + Op::FloatMin => Expr::Named("__fmin", vec![a(), b()]), + Op::FloatNan => Expr::Named("__isnan", vec![a()]), + Op::FloatTrunc => Expr::Named("__trunc", vec![a()]), + Op::FloatRound => Expr::Named("__rint", vec![a()]), + Op::FloatCeil => Expr::Named("__ceil", vec![a()]), + Op::FloatFloor => Expr::Named("__floor", vec![a()]), + Op::FloatMulAdd => Expr::Named( + "__fma", + op.inputs + .iter() + .map(|i| match i { + Operand::Const(v, size) => Expr::FConst(*v, *size), + other => self.operand(other), + }) + .collect(), + ), + Op::IntToFloat | Op::UIntToFloat => Expr::Cast(float_type(op.size), Box::new(ia())), + Op::FloatToInt => Expr::Cast(signed_type(op.size), Box::new(fa())), + Op::FloatToUInt => Expr::Cast(c_type(op.size), Box::new(fa())), + Op::FloatConvert => Expr::Cast(float_type(op.size), Box::new(fa())), + Op::LzCount => Expr::Named("__clz", vec![a()]), + Op::IntCarry => Expr::Named("__carry", vec![a(), b()]), + Op::IntSCarry => Expr::Named("__overflow", vec![a(), b()]), + Op::IntSBorrow => Expr::Named("__borrow", vec![a(), b()]), + Op::Piece => Expr::Unknown("piece"), + Op::Undefine => Expr::Unknown("clobbered"), + Op::Unimplemented => Expr::Unknown("unmodelled"), + _ => Expr::Unknown("op"), + } + } + + /// How many times a value is read. + pub fn uses(&self, v: Value) -> usize { + self.uses.get(&v).copied().unwrap_or(0) + } + + /// Wire in what a prototype says about the values arriving from outside. + /// + /// A declared parameter arrives in a register the convention chooses, so + /// once the declaration is laid over the convention the body can use the + /// name, the width and the fields the declaration gave rather than the + /// register it happened to come in. + pub fn declare(&mut self, p: &Prototype) { + let (mut ints, mut floats) = (0usize, 0usize); + for param in &p.parameters { + // A stack argument already carries the name the body gives it, and + // it occupies no register, so laying the declaration over the + // convention has nothing to do for it. + if param.stack.is_some() { + continue; + } + let offset = if param.floating { + let o = self.abi.float_arguments.get(floats).copied(); + floats += 1; + o + } else { + let o = self.abi.integer_arguments.get(ints).copied(); + ints += 1; + o + }; + let Some(o) = offset else { continue }; + self.names.insert(o, param.name.clone()); + if param.pointer { + self.pointers.insert(o); + } + if param.size > 0 { + self.sizes.insert(o, param.size); + } + if !param.fields.is_empty() { + self.fields.insert(o, param.fields.clone()); + if let Some(stride) = param.stride { + self.strides.insert(o, stride); + } + } + // A parameter declared floating is floating even when nothing in + // the body does arithmetic on it: at O0 it is stored to the stack + // before anything touches it. + if param.floating { + // At the whole register, and at the width the parameter is + // declared with: a `float` argument is read as the low four + // bytes of the register it arrives in, and that piece is + // floating too. + for size in [8, param.size] { + self.floats.insert(Location { + space: e5r_ir::op::Space::Register, + offset: o, + size, + }); + } + } + } + } + + /// Whether a `SubPiece` is taking a float out of the register it came in. + /// + /// Only when the location is floating and the piece is exactly as wide as + /// the width that location was declared at, so the piece is the whole + /// value and not part of it. + fn narrow_float(&self, op: &SsaOp) -> bool { + let Some(l) = op.inputs.first().and_then(|i| match i { + Operand::Value(v) => Some(v.location), + Operand::Undefined(l) => Some(*l), + Operand::Const(..) => None, + }) else { + return false; + }; + self.floats.contains(&l) + && l.space == e5r_ir::op::Space::Register + && self.sizes.get(&l.offset).copied() == Some(op.size) + } + + /// The type the body declares a value of this location with. + fn type_of(&self, l: Location) -> &'static str { + if self.floats.contains(&l) { + float_type(l.size) + } else { + c_type(l.size) + } + } +} + +/// Where the machine kept a variable. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Home { + /// A register, by its offset in the register file. + Register(u64), + /// A slot at this offset from the stack pointer on entry, which is what a + /// frame offset means once the stack has been promoted. + Stack(i64), + /// Somewhere the rebuilder did not have to name: a value that exists only + /// between the operation that made it and the one that reads it. + Anywhere, +} + +/// What a variable is to the function that declares it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Role { + /// Something the caller passed. + Parameter, + /// Something the body computes and names. + Local, + /// A register or slot read before anything in this function wrote it, + /// which the convention does not pass arguments in either. Neither a + /// parameter nor anything this function made. + Inherited, +} + +/// One variable a decompiled function declares. +/// +/// The count of these is not a fact anything can be checked against: matching +/// what was recovered to what the source declared needs the name, the type and +/// where the value lived. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Variable { + /// What the body calls it. + pub name: String, + /// Its type, as the declaration spells it. + pub ty: String, + /// Its width in bytes, zero when nothing said. + pub size: u8, + /// Parameter, local, or inherited. + pub role: Role, + /// Where the machine kept it. + pub home: Home, +} + +/// Every variable one decompiled function declares. +/// +/// Rebuilt from the same SSA and the same prototype the emitter used, so these +/// are the names that appear in its text rather than a second opinion. `body` +/// is that text: an inherited register every optimization removed is declared +/// by neither, and whether the text mentions it is how the emitter decides. +pub fn variables(f: &SsaFunction, prototype: Option<&Prototype>, body: &str) -> Vec { + let mut r = Rebuilder::new(f); + if let Some(p) = prototype { + r.declare(p); + } + let r = r; + + let mut out: Vec = Vec::new(); + let mut seen: BTreeSet = BTreeSet::new(); + let mut push = |out: &mut Vec, v: Variable| { + if seen.insert(v.name.clone()) { + out.push(v); + } + }; + + // Parameters in the order the declaration gives them, which is the order + // they appear in the signature. + match prototype.filter(|p| !p.parameters.is_empty()) { + Some(p) => { + let (mut ints, mut floats) = (0usize, 0usize); + for param in &p.parameters { + // One the convention ran out of registers for is on the stack + // and takes no register slot. + let home = match param.stack { + Some(offset) => Home::Stack(offset), + None => { + let offset = if param.floating { + let o = r.abi.float_arguments.get(floats).copied(); + floats += 1; + o + } else { + let o = r.abi.integer_arguments.get(ints).copied(); + ints += 1; + o + }; + offset.map_or(Home::Anywhere, Home::Register) + } + }; + push( + &mut out, + Variable { + ty: declared_type(¶m.decl, ¶m.name), + name: param.name.clone(), + size: param.size, + role: Role::Parameter, + home, + }, + ); + } + } + // Nothing declared anything: the convention's argument registers that + // arrive with values are the parameters, in the convention's order. + None => { + let order: Vec = r + .abi + .integer_arguments + .iter() + .chain(r.abi.float_arguments.iter()) + .copied() + .collect(); + let mut by_slot: BTreeMap = BTreeMap::new(); + for l in undefined(f) { + let name = r.name_of(l); + let slot = if l.space == e5r_ir::op::Space::Register { + order.iter().position(|o| *o == l.offset) + } else if is_stack_argument(l) { + Some(order.len() + l.offset as usize) + } else { + None + }; + let Some(slot) = slot else { continue }; + by_slot.entry(slot).or_insert(Variable { + name, + ty: r.type_of(l).to_string(), + size: l.size, + role: Role::Parameter, + home: home_of(l), + }); + } + for v in by_slot.into_values() { + push(&mut out, v); + } + } + } + + // Registers and slots the function inherited: declared by the emitter only + // where the body still mentions them. + let parameters: BTreeSet = out.iter().map(|v| v.name.clone()).collect(); + for l in undefined(f) { + let name = r.name_of(l); + if parameters.contains(&name) || !mentions(body, &name) { + continue; + } + push( + &mut out, + Variable { + name, + ty: r.type_of(l).to_string(), + size: l.size, + role: Role::Inherited, + home: home_of(l), + }, + ); + } + + for (value, name) in &r.locals { + push( + &mut out, + Variable { + name: name.clone(), + ty: r.type_of(value.location).to_string(), + size: value.location.size, + role: Role::Local, + home: home_of(value.location), + }, + ); + } + out +} + +/// Every location read before anything in the function wrote it. +fn undefined(f: &SsaFunction) -> Vec { + let mut out = Vec::new(); + for b in f.blocks.values() { + for op in &b.ops { + for i in &op.inputs { + if let Operand::Undefined(l) = i { + out.push(*l); + } + } + } + } + out +} + +/// Where a location lives, in the terms a reader of the output cares about. +fn home_of(l: Location) -> Home { + match l.space { + e5r_ir::op::Space::Register => Home::Register(l.offset), + e5r_ir::op::Space::Stack => Home::Stack(l.offset as i64), + _ => Home::Anywhere, + } +} + +/// The type half of a declaration. +/// +/// C wraps the type around the name rather than putting the two side by side, +/// so this is a removal and not a split: `struct Point *p` is a `struct Point +/// *` and `int (*f)(int)` is an `int (*)(int)`. +pub fn declared_type(decl: &str, name: &str) -> String { + let Some(at) = word(decl, name) else { + return decl.trim().to_string(); + }; + let (head, tail) = (&decl[..at], &decl[at + name.len()..]); + let joined = format!("{}{tail}", head.trim_end()); + joined.trim().to_string() +} + +/// Where a name appears in a text as a whole word, last occurrence first. +fn word(text: &str, name: &str) -> Option { + if name.is_empty() { + return None; + } + let mut found = None; + let mut at = 0; + while let Some(next) = text[at..].find(name) { + let start = at + next; + let end = start + name.len(); + let part = |c: Option| c.is_some_and(|c| c.is_alphanumeric() || c == '_'); + if !part(text[..start].chars().next_back()) && !part(text[end..].chars().next()) { + found = Some(start); + } + at = end; + } + found +} + +/// True when a name appears in a text as a whole word. +pub fn mentions(text: &str, name: &str) -> bool { + word(text, name).is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn precedence_puts_parentheses_only_where_needed() { + let a = Expr::Local("a".into()); + let b = Expr::Local("b".into()); + let c = Expr::Local("c".into()); + // Multiplication binds tighter, so no parentheses. + let e = Expr::Binary( + "+", + Box::new(a.clone()), + Box::new(Expr::Binary("*", Box::new(b.clone()), Box::new(c.clone()))), + ); + assert_eq!(e.to_string(), "a + b * c"); + // Addition inside multiplication needs them. + let e = Expr::Binary( + "*", + Box::new(Expr::Binary("+", Box::new(a.clone()), Box::new(b.clone()))), + Box::new(c.clone()), + ); + assert_eq!(e.to_string(), "(a + b) * c"); + } + + #[test] + fn subtraction_is_not_reassociated_by_the_printer() { + // `a - (b - c)` must keep its parentheses, or the output means + // something else. + let e = Expr::Binary( + "-", + Box::new(Expr::Local("a".into())), + Box::new(Expr::Binary( + "-", + Box::new(Expr::Local("b".into())), + Box::new(Expr::Local("c".into())), + )), + ); + assert_eq!(e.to_string(), "a - (b - c)"); + } + + #[test] + fn constants_print_in_the_base_that_reads_better() { + assert_eq!(Expr::Const(10, 8).to_string(), "10"); + assert_eq!(Expr::Const(0x4006e8, 8).to_string(), "0x4006e8"); + // A small negative at a narrow width is a number, not a huge mask. + assert_eq!(Expr::Const(0xffff_ffff, 4).to_string(), "-1"); + } + + #[test] + fn depth_counts_the_tree() { + let e = Expr::Binary( + "+", + Box::new(Expr::Local("a".into())), + Box::new(Expr::Binary( + "*", + Box::new(Expr::Local("b".into())), + Box::new(Expr::Local("c".into())), + )), + ); + assert_eq!(e.depth(), 3); + } +} diff --git a/crates/e5r-decomp/src/lib.rs b/crates/e5r-decomp/src/lib.rs new file mode 100644 index 0000000..70daecb --- /dev/null +++ b/crates/e5r-decomp/src/lib.rs @@ -0,0 +1,21 @@ +//! Expression rebuilding, structuring and C emission. +//! +//! The output is pseudo-C: it says what the machine does, in C's notation, +//! without claiming to be the source. Where the structure does not fit an `if` +//! or a `while`, a labelled `goto` appears rather than a shape that is not +//! there, and the number of them is reported, because it is the honest measure +//! of how well the structuring worked. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod emit; +pub mod expr; +pub mod structure; + +pub use emit::{ + Callee, Output, Param, Prototype, decompile, decompile_full, decompile_in, decompile_with, + identifier, +}; +pub use expr::{Expr, c_type, field_name, slot_name}; +pub use structure::{Case, Region, Structured, Switches}; diff --git a/crates/e5r-decomp/src/structure.rs b/crates/e5r-decomp/src/structure.rs new file mode 100644 index 0000000..bf813b5 --- /dev/null +++ b/crates/e5r-decomp/src/structure.rs @@ -0,0 +1,2464 @@ +//! Recovering structure from a control flow graph. +//! +//! A graph becomes an `if`, a `while` and a sequence when it has that shape and +//! a labelled `goto` when it does not. Inventing a shape that is not there is +//! the one thing this must not do: output that looks like a loop but is not is +//! worse than output with a `goto` in it, because the reader cannot tell. +//! +//! The number of gotos is the quality signal, reported rather than hidden. It +//! is what the roadmap's G7 gate measures and what a later structuring pass +//! would be judged against. +//! +//! Two environment variables exist for measuring that, and only for that. +//! `E5R_GOTO_STATS` prints the histogram of why each goto was needed and why +//! each sink was refused, on stderr, per function. Three switches turn one +//! mechanism each off, so a before and an after can come from one build over +//! one corpus, which is the only way two structuring numbers are comparable: +//! `E5R_NO_SINK` for writing a shared tail after the branch, `E5R_NO_BREAK_SINK` +//! for doing that when the jumps to it come out of a loop or a switch arm, and +//! `E5R_NO_TIGHT_TAIL` for measuring a copied tail as what the walk would +//! write rather than as everything reachable past it. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_core::Addr; + +/// The most blocks one copied tail may span. +/// +/// Past this a copy stops reading as the same few statements written twice and +/// starts reading as a second piece of code, which is worse than the goto it +/// replaces. Measured over the corpus: four is where the curve flattens, and +/// every block past it costs more output than it saves gotos. +const MAX_COPIED_TAIL: usize = 4; + +/// How many blocks in total a function may write out a second time. +/// +/// Proportional to the function, because a shared tail is a fact about how many +/// arms a function has, plus a floor for the small ones where one copy is the +/// whole difference. Bounded so that duplication cannot turn a chain of +/// comparisons into an exponential one. +fn duplication_budget(blocks: usize) -> u32 { + 8 + blocks as u32 +} + +/// A recovered region. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Region { + /// One basic block's statements, then whatever follows. + Block(Addr), + /// Regions one after another. + Seq(Vec), + /// A two-way branch that reconverges. + If { + /// The block holding the condition. + head: Addr, + /// True when the condition has to be inverted to read naturally, which + /// happens because a machine branch jumps on the condition being true + /// and an `if` falls into its body. + invert: bool, + /// The body taken when the condition holds. + then: Box, + /// The other body, when there is one. + otherwise: Option>, + }, + /// A loop whose condition is tested at the top. + While { + /// The block holding the condition. + head: Addr, + /// True when the condition has to be inverted. + invert: bool, + /// The body. + body: Box, + }, + /// A loop with no recognized condition: `for (;;)` with a break inside. + Infinite { + /// Where it starts. + head: Addr, + /// The body. + body: Box, + }, + /// A jump to a label, for an edge no shape covered. + Goto(Addr), + /// Leaving the loop this sits inside. + Break, + /// Going back to the top of the loop this sits inside. + Continue, + /// A multi-way branch through a jump table. + Switch { + /// The block holding the branch. + head: Addr, + /// The arms, in case-value order. + cases: Vec, + /// Where control goes for an index the table does not cover. + default: Option>, + }, + /// Nothing. + Empty, +} + +/// One arm of a switch. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Case { + /// The values that reach this arm; several indices can share a body. + pub values: Vec, + /// What it does. + pub body: Region, +} + +/// A structured function, with the measure of how well it worked. +#[derive(Debug, Clone)] +pub struct Structured { + /// The region tree. + pub root: Region, + /// Blocks that need a label because something jumps to them. + pub labels: BTreeSet, + /// How many gotos the structuring had to emit. The quality signal. + pub gotos: usize, + /// Reachable blocks the region tree never mentions. + /// + /// Their code would be missing from the output with nothing downstream able + /// to tell, which is worse than any number of gotos. Correct structuring + /// leaves this empty; it is reported rather than asserted so a caller can + /// decide what to do about it. + pub lost: BTreeSet, +} + +/// The graph structuring works over: successors per block. +pub type Graph = BTreeMap>; + +/// The cases of each multi-way branch: the value the index takes and where it +/// goes. Without this a jump table is a block with many successors and no way +/// to say which is which, and every arm but one becomes a goto. +pub type Switches = BTreeMap>; + +/// Which successor a conditional branch jumps to when its condition holds. +/// +/// Guessing this from the order of the successors is how an `if` comes out +/// inverted, so it is taken from the branch itself. +pub type Taken = BTreeMap; + +/// Structure a control flow graph. +pub fn structure(entry: Addr, graph: &Graph, taken: &Taken) -> Structured { + structure_with(entry, graph, taken, &Switches::new()) +} + +/// Structure a control flow graph, knowing what its jump tables mean. +pub fn structure_with( + entry: Addr, + graph: &Graph, + taken: &Taken, + switches: &Switches, +) -> Structured { + let order = reverse_postorder(entry, graph); + let index: BTreeMap = order.iter().enumerate().map(|(i, a)| (*a, i)).collect(); + let preds = predecessors(graph); + let idom = dominators(entry, graph, &preds, &order, &index); + + // A back edge goes to a block that dominates its source; its target is a + // loop header. This is the definition of a natural loop and the only one + // that cannot be fooled by a graph that merely looks like one. + let mut loop_headers: BTreeSet = BTreeSet::new(); + let mut latches: BTreeMap> = BTreeMap::new(); + for (from, succs) in graph { + for to in succs { + if dominates(&idom, *to, *from) { + loop_headers.insert(*to); + latches.entry(*to).or_default().insert(*from); + } + } + } + // The blocks each loop contains. + let bodies: BTreeMap> = loop_headers + .iter() + .map(|h| (*h, loop_body(*h, &latches, &preds))) + .collect(); + + let postdom = postdominators(graph, &order, &index); + let tight = std::env::var_os("E5R_NO_TIGHT_TAIL").is_none(); + + let mut ctx = Ctx { + graph, + taken, + switches, + postdom: &postdom, + loop_headers: &loop_headers, + bodies: &bodies, + emitted: BTreeSet::new(), + labels: BTreeSet::new(), + gotos: 0, + depth: 0, + duplication: duplication_budget(graph.len()), + loops: Vec::new(), + keep_single: BTreeSet::new(), + switch_depth: 0, + tight, + reasons: BTreeMap::new(), + }; + + // Writing a tail out twice and then jumping to it are each fine and + // together are not: two copies of a labelled block are two definitions of + // one C label. Which copy a later goto would want is not knowable while + // the copy is being made, so the block is barred from duplication and the + // function structured again. The bar only grows, so this settles. + let mut root = ctx.region(entry, None, None); + for _ in 0..4 { + let clashing = labelled_twice(&root, &ctx.labels); + if clashing.is_empty() { + break; + } + let mut keep_single = std::mem::take(&mut ctx.keep_single); + keep_single.extend(clashing); + ctx = Ctx { + graph, + taken, + switches, + postdom: &postdom, + loop_headers: &loop_headers, + bodies: &bodies, + emitted: BTreeSet::new(), + labels: BTreeSet::new(), + gotos: 0, + depth: 0, + duplication: duplication_budget(graph.len()), + loops: Vec::new(), + keep_single, + switch_depth: 0, + tight, + reasons: BTreeMap::new(), + }; + root = ctx.region(entry, None, None); + } + + // A tail several arms reach can often be written after them all rather + // than inside one of them, which is a goto fewer and no copy. Done here, + // over the finished tree, because whether an arm falls into the tail or + // has to skip it is a question about the tree and not about the walk. + let mut refused: BTreeMap<&'static str, usize> = BTreeMap::new(); + // Off switch for the second mechanism only, so a before and an after come + // from one build over one corpus. + let breaks = std::env::var_os("E5R_NO_BREAK_SINK").is_none(); + if std::env::var_os("E5R_NO_SINK").is_none() { + for at in sink_shared_tails(&mut root, graph, &mut ctx.labels, &mut refused, breaks) { + ctx.gotos = ctx.gotos.saturating_sub(1); + let reason = ctx.goto_reason(at); + if let Some(n) = ctx.reasons.get_mut(reason) { + *n = n.saturating_sub(1); + } + *ctx.reasons.entry("sunk instead of jumped to").or_default() += 1; + } + } + + // Why each goto was needed, which is what says where to spend the next + // piece of work. Off by default and per function, so aggregating it is the + // caller's job: a whole-corpus run is the only useful unit. Note that + // `decompile_program` structures every function twice, once to learn what + // the callees take and once to use it, so raw totals are double. + if std::env::var_os("E5R_GOTO_STATS").is_some() { + for (reason, n) in &ctx.reasons { + eprintln!("GOTOREASON\t{reason}\t{n}"); + } + for (reason, n) in &refused { + eprintln!("SINKREFUSED\t{reason}\t{n}"); + } + } + let lost = lost_blocks(entry, graph, &root); + Structured { + root, + labels: ctx.labels, + gotos: ctx.gotos, + lost, + } +} + +/// Blocks a region tree both writes out more than once and labels. +fn labelled_twice(root: &Region, labels: &BTreeSet) -> BTreeSet { + let mut counts: BTreeMap = BTreeMap::new(); + occurrences(root, &mut counts); + counts + .into_iter() + .filter(|(at, n)| *n > 1 && labels.contains(at)) + .map(|(at, _)| at) + .collect() +} + +/// How many times each block's statements appear in a region tree. +fn occurrences(r: &Region, out: &mut BTreeMap) { + match r { + Region::Seq(parts) => { + for p in parts { + occurrences(p, out); + } + } + Region::If { + head, + then, + otherwise, + .. + } => { + *out.entry(*head).or_default() += 1; + occurrences(then, out); + if let Some(o) = otherwise { + occurrences(o, out); + } + } + Region::While { head, body, .. } | Region::Infinite { head, body } => { + *out.entry(*head).or_default() += 1; + occurrences(body, out); + } + Region::Switch { + head, + cases, + default, + } => { + *out.entry(*head).or_default() += 1; + for c in cases { + occurrences(&c.body, out); + } + if let Some(d) = default { + occurrences(d, out); + } + } + Region::Block(at) => { + *out.entry(*at).or_default() += 1; + } + Region::Goto(_) | Region::Break | Region::Continue | Region::Empty => {} + } +} + +/// Reachable blocks the region tree does not mention. +fn lost_blocks(entry: Addr, graph: &Graph, root: &Region) -> BTreeSet { + let mut seen = BTreeSet::new(); + mentioned(root, &mut seen); + reverse_postorder(entry, graph) + .into_iter() + .filter(|a| !seen.contains(a)) + .collect() +} + +/// Every block a region tree emits the statements of. +/// +/// A `goto` target does not count: it is a reference to a block, not a copy of +/// it, and a label with no block under it is itself a defect. +fn mentioned(r: &Region, out: &mut BTreeSet) { + match r { + Region::Block(at) => { + out.insert(*at); + } + Region::Seq(parts) => { + for p in parts { + mentioned(p, out); + } + } + Region::If { + head, + then, + otherwise, + .. + } => { + out.insert(*head); + mentioned(then, out); + if let Some(o) = otherwise { + mentioned(o, out); + } + } + Region::While { head, body, .. } | Region::Infinite { head, body } => { + out.insert(*head); + mentioned(body, out); + } + Region::Switch { + head, + cases, + default, + } => { + out.insert(*head); + for c in cases { + mentioned(&c.body, out); + } + if let Some(d) = default { + mentioned(d, out); + } + } + Region::Goto(_) | Region::Break | Region::Continue | Region::Empty => {} + } +} + +struct Ctx<'a> { + graph: &'a Graph, + taken: &'a Taken, + switches: &'a Switches, + postdom: &'a BTreeMap, + loop_headers: &'a BTreeSet, + bodies: &'a BTreeMap>, + emitted: BTreeSet, + labels: BTreeSet, + gotos: usize, + depth: u32, + /// How many more blocks may be written out a second time. Counted in + /// blocks rather than in copies, because what has to stay bounded is how + /// much text the duplication adds, not how often it happens. + duplication: u32, + /// The loops currently being structured, innermost last. An edge to the + /// innermost loop's exit is a `break` and an edge to its header is a + /// `continue`; without this both come out as gotos. + loops: Vec, + /// Blocks that must appear once, because a goto names them. + keep_single: BTreeSet, + /// Switch arms entered since the innermost loop was pushed. + /// + /// `break` leaves the innermost loop *or* switch, so a loop exit reached + /// from inside an arm cannot be written as one. `continue` is unaffected: + /// it skips switches and binds to the loop. + switch_depth: u32, + /// Whether a copied tail is measured as what the walk would write rather + /// than as everything reachable past it. A measurement switch. + tight: bool, + reasons: BTreeMap<&'static str, usize>, +} + +#[derive(Debug, Clone, Copy)] +struct Nesting { + head: Addr, + exit: Option, +} + +impl Ctx<'_> { + /// Structure from `at` until `stop`, inside `enclosing` loop if any. + fn region(&mut self, at: Addr, stop: Option, enclosing: Option) -> Region { + self.depth += 1; + let out = self.region_inner(at, stop, enclosing); + self.depth -= 1; + out + } + + fn region_inner(&mut self, at: Addr, stop: Option, enclosing: Option) -> Region { + // A guard, because a graph from hostile input need not be reducible. + if self.depth > 200 { + *self.reasons.entry("depth limit").or_default() += 1; + self.gotos += 1; + self.labels.insert(at); + return Region::Goto(at); + } + let mut parts: Vec = Vec::new(); + let mut cursor = at; + + loop { + if Some(cursor) == stop { + break; + } + if !self.graph.contains_key(&cursor) { + break; + } + // Reaching the innermost loop's header again is a continue, and + // reaching where it leaves to is a break. Checked before the stop + // address, because an arm of an `if` that ends by going back to the + // header has to say so: ending the arm silently would fall past the + // `if` instead of repeating. + if let Some(inner) = self.loops.last().copied() { + if cursor == inner.head { + parts.push(Region::Continue); + break; + } + if Some(cursor) == inner.exit { + if self.switch_depth == 0 { + parts.push(Region::Break); + } else { + // Inside a switch arm a `break` would leave the switch + // and run what follows it, not leave the loop, so the + // exit has to be named. + *self + .reasons + .entry("a loop exit reached from inside a switch arm") + .or_default() += 1; + self.gotos += 1; + self.labels.insert(cursor); + parts.push(Region::Goto(cursor)); + } + break; + } + } + if self.emitted.contains(&cursor) { + // A small tail is written out again rather than jumped to. + if let Some(blocks) = self.copyable_tail(cursor, stop) { + self.duplication -= blocks.len() as u32; + parts.push(self.duplicate(cursor, stop, enclosing, &blocks)); + break; + } + *self.reasons.entry(self.goto_reason(cursor)).or_default() += 1; + self.gotos += 1; + self.labels.insert(cursor); + parts.push(Region::Goto(cursor)); + break; + } + + // A loop header starts a loop, unless we are already in it. + if self.loop_headers.contains(&cursor) && Some(cursor) != enclosing { + parts.push(self.build_loop(cursor)); + // Control continues at whatever the loop exits to. + match self.loop_exit(cursor) { + Some(next) if Some(next) != stop => { + cursor = next; + continue; + } + _ => break, + } + } + + self.emitted.insert(cursor); + let succs = self.graph.get(&cursor).cloned().unwrap_or_default(); + match succs.len() { + 0 => { + parts.push(Region::Block(cursor)); + break; + } + 1 => { + // Where the successor goes is the top of this loop's + // decision, not this arm's: the top is the one place that + // knows a `continue` from a `break` from a tail worth + // copying. Settling it here instead is what turned every + // fall into an already written block into a goto with no + // copy considered, which was 42% of them. + parts.push(Region::Block(cursor)); + cursor = succs[0]; + } + _ if self.switches.contains_key(&cursor) => { + let (region, after) = self.build_switch(cursor, stop, enclosing); + parts.push(Region::Block(cursor)); + parts.push(region); + match after { + Some(next) if Some(next) != stop => cursor = next, + _ => break, + } + } + _ => { + let (region, after) = self.build_if(cursor, &succs, stop, enclosing); + parts.push(Region::Block(cursor)); + parts.push(region); + // Likewise: an `if` whose arms rejoin at a block already + // written is the same decision one level up, and the top + // of this loop is where it is made. + match after { + Some(next) if Some(next) != stop => cursor = next, + _ => break, + } + } + } + } + + match parts.len() { + 0 => Region::Empty, + 1 => parts.pop().unwrap(), + _ => Region::Seq(parts), + } + } + + fn preds_of(&self, at: Addr) -> usize { + self.graph.values().filter(|s| s.contains(&at)).count() + } + + /// Which bucket a goto to this block belongs in. + /// + /// Shared between emitting one and taking one back off when sinking + /// removes it, so the histogram stays a partition of the gotos that are + /// actually left. + fn goto_reason(&self, at: Addr) -> &'static str { + if self.loop_headers.contains(&at) { + "already written: loop entered a second way" + } else if self.preds_of(at) > 1 { + "already written: shared tail" + } else { + "already written: other" + } + } + + /// How many blocks a tail would copy, when copying it is allowed. + /// + /// A compiler merges identical tails: every path that ends in the same + /// `return`, and cross-jumping for the rest. That merge is what leaves one + /// block with several predecessors, and a single walk can place it under + /// only one of them, so every other predecessor becomes a goto. Writing the + /// tail out again in each arm undoes the merge rather than inventing + /// structure, and over this corpus it is where most of the removable gotos + /// are. + fn copyable_tail(&self, at: Addr, stop: Option) -> Option> { + let mut seen: BTreeSet = BTreeSet::new(); + let mut work = vec![at]; + while let Some(block) = work.pop() { + if Some(block) == stop || !self.graph.contains_key(&block) { + continue; + } + // The walk writes `continue` where the tail reaches the top of the + // loop it is in and `break` where it reaches what the loop leaves + // to, one line each and no blocks. Counting everything past those + // two, which is most of the function, is what refused tails the + // walk would have written as two statements. + if self.tight { + if let Some(inner) = self.loops.last() { + if block == inner.head || Some(block) == inner.exit { + continue; + } + } + } + // An outer loop is written out again only when it lies wholly + // inside the tail, which the size bound below decides: every block + // of a natural loop is reachable from its header, so a loop that + // does not fit makes the walk exceed the bound. A loop currently + // being structured is never a tail, because writing it out again + // inside itself would not terminate. + if self.loops.iter().any(|l| l.head == block) || self.keep_single.contains(&block) { + return None; + } + if !seen.insert(block) { + continue; + } + if seen.len() > MAX_COPIED_TAIL || seen.len() as u32 > self.duplication { + return None; + } + for s in self.graph.get(&block).into_iter().flatten() { + if !seen.contains(s) { + work.push(*s); + } + } + } + (!seen.is_empty()).then_some(seen) + } + + /// Structure a tail again, as a copy. + /// + /// Exactly the blocks `copyable_tail` counted are unmarked, so what is + /// written out again is what the budget was charged for. Anything the copy + /// reaches beyond them is still marked and comes out as the jump it was. + fn duplicate( + &mut self, + at: Addr, + stop: Option, + enclosing: Option, + blocks: &BTreeSet, + ) -> Region { + let mut restored: Vec = Vec::new(); + for block in blocks { + if self.emitted.remove(block) { + restored.push(*block); + } + } + let region = self.region(at, stop, enclosing); + for block in restored { + self.emitted.insert(block); + } + region + } + + /// Build a loop from its header. + fn build_loop(&mut self, head: Addr) -> Region { + self.emitted.insert(head); + let exit = self.loop_exit(head); + self.loops.push(Nesting { head, exit }); + // A loop entered inside a switch arm takes `break` back, so the count + // is of arms entered since *this* loop, not since the function. + let outer = std::mem::take(&mut self.switch_depth); + let region = self.build_loop_body(head); + self.switch_depth = outer; + self.loops.pop(); + trim_trailing_continue(region) + } + + fn build_loop_body(&mut self, head: Addr) -> Region { + let body_blocks = self.bodies.get(&head).cloned().unwrap_or_default(); + let succs = self.graph.get(&head).cloned().unwrap_or_default(); + + // A header with two successors, one inside the loop and one outside, is + // a `while`: the condition is tested at the top. + if succs.len() == 2 { + let (inside, outside) = if body_blocks.contains(&succs[0]) { + (succs[0], succs[1]) + } else { + (succs[1], succs[0]) + }; + if body_blocks.contains(&inside) && !body_blocks.contains(&outside) { + // A `while` enters its body when the condition holds, so the + // test needs inverting when the branch jumps out instead. + let taken = self.taken.get(&head).copied().unwrap_or(succs[0]); + let invert = inside != taken; + let body = self.region(inside, Some(head), Some(head)); + return Region::While { + head, + invert, + body: Box::new(body), + }; + } + } + + // Otherwise the loop has no condition at the top; the exits inside + // become breaks. + let body = self.after_head(head, &succs); + Region::Infinite { + head, + body: Box::new(body), + } + } + + /// What runs after a loop header whose test is not at the top. + /// + /// The header still ends in whatever branch it ends in. Following one + /// successor and forgetting the other drops every block only the other arm + /// reaches, which is how an inner loop vanishes out of an outer one, so the + /// branch is structured here the same way it would be anywhere else and the + /// walk carries on where its arms rejoin. + fn after_head(&mut self, head: Addr, succs: &[Addr]) -> Region { + let stop = Some(head); + let (branch, after) = match succs.len() { + 0 => return Region::Empty, + 1 => return self.region(succs[0], stop, stop), + _ if self.switches.contains_key(&head) => self.build_switch(head, stop, stop), + _ => self.build_if(head, succs, stop, stop), + }; + let mut parts = vec![branch]; + match after { + Some(next) if Some(next) != stop => parts.push(self.region(next, stop, stop)), + _ => {} + } + match parts.len() { + 1 => parts.pop().unwrap(), + _ => Region::Seq(parts), + } + } + + /// Where a loop leaves to. + /// + /// A loop with several exits still has one the code continues at; the rest + /// become gotos. Choosing it as the one the header itself branches to, and + /// otherwise the one most of the body branches to, is what turns an + /// optimized loop's exits into `break` rather than a page of labels. + fn loop_exit(&self, head: Addr) -> Option { + let body = self.bodies.get(&head)?; + let mut counts: BTreeMap = BTreeMap::new(); + for b in body { + for s in self.graph.get(b).into_iter().flatten() { + if !body.contains(s) { + *counts.entry(*s).or_default() += 1; + } + } + } + if counts.is_empty() { + return None; + } + // The header's own way out, when it has one. + if let Some(succs) = self.graph.get(&head) { + if succs.len() == 2 { + if let Some(outside) = succs.iter().find(|s| !body.contains(*s)) { + return Some(*outside); + } + } + } + counts + .into_iter() + .max_by_key(|(addr, n)| (*n, std::cmp::Reverse(*addr))) + .map(|(addr, _)| addr) + } + + /// Build an `if` from a two-way branch, returning the region and where + /// control continues. + fn build_if( + &mut self, + head: Addr, + succs: &[Addr], + stop: Option, + enclosing: Option, + ) -> (Region, Option) { + // The branch says which way it goes when the condition holds; the + // other successor is what falls through. + let taken = self.taken.get(&head).copied().unwrap_or(succs[1]); + let fallthrough = succs + .iter() + .copied() + .find(|s| *s != taken) + .unwrap_or(succs[0]); + let join = self.join_of(head); + + // One arm empty: a plain `if` with no else. + let then_start = if Some(fallthrough) == join { + taken + } else { + fallthrough + }; + let other_start = if Some(fallthrough) == join || Some(taken) == join { + None + } else { + Some(taken) + }; + // Inverted when the body is the branch's fall-through, because the + // machine jumped past it. + let invert = then_start == fallthrough; + + // An arm that starts at the join does nothing. An arm that starts at + // the enclosing region's stop address is not empty: it may be a jump + // back to a loop header, which `region` turns into a `continue`. + let then = if Some(then_start) == join { + Region::Empty + } else { + self.region(then_start, join.or(stop), enclosing) + }; + let otherwise = other_start.and_then(|s| { + if Some(s) == join { + None + } else { + Some(Box::new(self.region(s, join.or(stop), enclosing))) + } + }); + + ( + Region::If { + head, + invert, + then: Box::new(then), + otherwise, + }, + join, + ) + } + + /// Build a switch from a block whose jump table is known. + fn build_switch( + &mut self, + head: Addr, + stop: Option, + enclosing: Option, + ) -> (Region, Option) { + let entries = self.switches.get(&head).cloned().unwrap_or_default(); + let join = self.postdom.get(&head).copied().filter(|j| *j != head); + + // Several indices often share a body, which C says with several + // labels on one arm rather than with a copy of the body each. + let mut grouped: BTreeMap> = BTreeMap::new(); + for (value, target) in &entries { + grouped.entry(*target).or_default().push(*value); + } + // In the order the values first appear, so the output reads in the + // order the source was written. + let mut order: Vec = Vec::new(); + for (_, target) in &entries { + if !order.contains(target) { + order.push(*target); + } + } + + let mut cases = Vec::new(); + for target in order { + let values = grouped.remove(&target).unwrap_or_default(); + if Some(target) == join { + // An arm that goes straight to the join does nothing but + // leave; it still needs its labels. + cases.push(Case { + values, + body: Region::Break, + }); + continue; + } + self.switch_depth += 1; + let body = self.region(target, join.or(stop), enclosing); + self.switch_depth -= 1; + cases.push(Case { values, body }); + } + + // A successor the table does not name is where an out-of-range index + // goes, which C calls the default. + let named: BTreeSet = entries.iter().map(|(_, t)| *t).collect(); + let default = self + .graph + .get(&head) + .into_iter() + .flatten() + .find(|s| !named.contains(s) && Some(**s) != join) + .copied(); + let default = default.map(|s| { + self.switch_depth += 1; + let d = Box::new(self.region(s, join.or(stop), enclosing)); + self.switch_depth -= 1; + d + }); + + ( + Region::Switch { + head, + cases, + default, + }, + join, + ) + } + + /// Where the two arms of a branch come back together. + /// + /// The immediate post-dominator: the first block every path from the + /// branch has to reach. Anything weaker, such as the earliest block both + /// arms can reach, picks a join too far away and turns each arm's tail + /// into a goto. + fn join_of(&self, head: Addr) -> Option { + let succs = self.graph.get(&head)?; + if succs.len() != 2 { + return None; + } + let join = self.postdom.get(&head).copied()?; + (join != head && self.graph.contains_key(&join)).then_some(join) + } +} + +/// Drop a `continue` in tail position, where repeating is what happens anyway. +fn trim_trailing_continue(r: Region) -> Region { + match r { + Region::While { head, invert, body } => Region::While { + head, + invert, + body: Box::new(trim_tail(*body)), + }, + Region::Infinite { head, body } => Region::Infinite { + head, + body: Box::new(trim_tail(*body)), + }, + other => other, + } +} + +fn trim_tail(r: Region) -> Region { + match r { + Region::Continue => Region::Empty, + Region::Seq(mut parts) => { + if matches!(parts.last(), Some(Region::Continue)) { + parts.pop(); + } + match parts.len() { + 0 => Region::Empty, + 1 => parts.pop().unwrap(), + _ => Region::Seq(parts), + } + } + other => other, + } +} + +/// The blocks a natural loop contains: everything that reaches a latch without +/// leaving through the header. +fn loop_body( + head: Addr, + latches: &BTreeMap>, + preds: &BTreeMap>, +) -> BTreeSet { + let mut body = BTreeSet::new(); + body.insert(head); + let mut work: Vec = latches.get(&head).into_iter().flatten().copied().collect(); + while let Some(at) = work.pop() { + if !body.insert(at) { + continue; + } + for p in preds.get(&at).into_iter().flatten() { + if !body.contains(p) { + work.push(*p); + } + } + } + body +} + +/// Reverse post-order from the entry. +pub fn reverse_postorder(entry: Addr, graph: &Graph) -> Vec { + let mut seen = BTreeSet::new(); + let mut post = Vec::new(); + let mut stack = vec![(entry, 0usize)]; + seen.insert(entry); + while let Some((at, i)) = stack.pop() { + let succs = graph.get(&at).cloned().unwrap_or_default(); + if i < succs.len() { + stack.push((at, i + 1)); + let next = succs[i]; + if graph.contains_key(&next) && seen.insert(next) { + stack.push((next, 0)); + } + } else { + post.push(at); + } + } + post.reverse(); + post +} + +/// Immediate post-dominators: for each block, the first block every path from +/// it must pass through on the way out. +/// +/// Computed as dominators on the reversed graph, with every block that leaves +/// the function treated as a predecessor of one virtual exit. +fn postdominators( + graph: &Graph, + order: &[Addr], + index: &BTreeMap, +) -> BTreeMap { + // The reversed graph, and the blocks that end the function. + let mut reverse: BTreeMap> = BTreeMap::new(); + let mut exits: Vec = Vec::new(); + for (from, succs) in graph { + reverse.entry(*from).or_default(); + let live: Vec = succs + .iter() + .copied() + .filter(|s| graph.contains_key(s)) + .collect(); + if live.is_empty() { + exits.push(*from); + } + for to in live { + reverse.entry(to).or_default().push(*from); + } + } + if exits.is_empty() { + // Every block continues somewhere, which happens when the function is + // one endless loop. The last block in order stands in for the exit. + if let Some(last) = order.last() { + exits.push(*last); + } + } + + // Post-order from the exits over the reversed graph, which is the order + // the fixed point wants. + let mut seen: BTreeSet = BTreeSet::new(); + let mut post: Vec = Vec::new(); + for start in &exits { + let mut stack = vec![(*start, 0usize)]; + if !seen.insert(*start) { + continue; + } + while let Some((at, i)) = stack.pop() { + let preds = reverse.get(&at).cloned().unwrap_or_default(); + if i < preds.len() { + stack.push((at, i + 1)); + let next = preds[i]; + if seen.insert(next) { + stack.push((next, 0)); + } + } else { + post.push(at); + } + } + } + post.reverse(); + let rank: BTreeMap = post.iter().enumerate().map(|(i, a)| (*a, i)).collect(); + + let mut ipdom: BTreeMap = BTreeMap::new(); + for e in &exits { + ipdom.insert(*e, *e); + } + let mut changed = true; + let mut rounds = 0; + while changed && rounds < 1000 { + changed = false; + rounds += 1; + for at in &post { + if exits.contains(at) { + continue; + } + let mut new: Option = None; + for s in graph.get(at).into_iter().flatten() { + if !ipdom.contains_key(s) { + continue; + } + new = Some(match new { + None => *s, + Some(cur) => intersect(&ipdom, &rank, cur, *s), + }); + } + if let Some(n) = new { + if ipdom.get(at) != Some(&n) { + ipdom.insert(*at, n); + changed = true; + } + } + } + } + let _ = index; + ipdom +} + +/// Walk two chains up until they meet, which is their nearest common ancestor. +fn intersect( + tree: &BTreeMap, + rank: &BTreeMap, + mut a: Addr, + mut b: Addr, +) -> Addr { + let mut guard = 0; + while a != b && guard < 10_000 { + guard += 1; + let (ra, rb) = ( + rank.get(&a).copied().unwrap_or(0), + rank.get(&b).copied().unwrap_or(0), + ); + if ra > rb { + let next = tree.get(&a).copied().unwrap_or(a); + if next == a { + break; + } + a = next; + } else { + let next = tree.get(&b).copied().unwrap_or(b); + if next == b { + break; + } + b = next; + } + } + a +} + +fn predecessors(graph: &Graph) -> BTreeMap> { + let mut out: BTreeMap> = BTreeMap::new(); + for (from, succs) in graph { + out.entry(*from).or_default(); + for to in succs { + out.entry(*to).or_default().insert(*from); + } + } + out +} + +fn dominators( + entry: Addr, + graph: &Graph, + preds: &BTreeMap>, + order: &[Addr], + index: &BTreeMap, +) -> BTreeMap { + let mut idom: BTreeMap = BTreeMap::new(); + idom.insert(entry, entry); + let mut changed = true; + let mut rounds = 0; + while changed && rounds < 1000 { + changed = false; + rounds += 1; + for at in order.iter().skip(1) { + let mut new: Option = None; + for p in preds.get(at).into_iter().flatten() { + if !idom.contains_key(p) { + continue; + } + new = Some(match new { + None => *p, + Some(cur) => { + let mut a = cur; + let mut b = *p; + let mut guard = 0; + while a != b && guard < 10_000 { + guard += 1; + let ia = index.get(&a).copied().unwrap_or(0); + let ib = index.get(&b).copied().unwrap_or(0); + if ia > ib { + let n = idom.get(&a).copied().unwrap_or(a); + if n == a { + break; + } + a = n; + } else { + let n = idom.get(&b).copied().unwrap_or(b); + if n == b { + break; + } + b = n; + } + } + a + } + }); + } + if let Some(n) = new { + if idom.get(at) != Some(&n) { + idom.insert(*at, n); + changed = true; + } + } + } + } + let _ = graph; + idom +} + +/// True when `a` dominates `b`. +fn dominates(idom: &BTreeMap, a: Addr, b: Addr) -> bool { + let mut at = b; + let mut guard = 0; + loop { + if at == a { + return true; + } + let next = idom.get(&at).copied().unwrap_or(at); + if next == at || guard > 10_000 { + return false; + } + at = next; + guard += 1; + } +} + +// --------------------------------------------------------------------------- +// Sinking: writing a shared tail after the branch instead of inside one arm. +// --------------------------------------------------------------------------- + +/// One step from a region to one of its children. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +enum Step { + Part(usize), + Then, + Else, + Body, + Case(usize), + Default, +} + +/// Whether control can reach the bottom of a region, and by what route. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Exit { + /// Never: every path returns, breaks, continues or jumps away. + Never, + /// Only along a path that ends where something was cut out. + Hole, + /// Along some other path, which is what makes sinking unsound. + Other, +} + +fn join(a: Exit, b: Exit) -> Exit { + match (a, b) { + (Exit::Other, _) | (_, Exit::Other) => Exit::Other, + (Exit::Hole, _) | (_, Exit::Hole) => Exit::Hole, + _ => Exit::Never, + } +} + +/// Where the holes are: the tail that is being lifted out, and the gotos to it. +struct Holes<'a> { + tail: &'a [Step], + gotos: &'a BTreeSet>, +} + +impl Holes<'_> { + fn at(&self, path: &[Step]) -> bool { + path == self.tail || self.gotos.contains(path) + } + + /// True when a hole lies strictly inside a region. + fn inside(&self, path: &[Step]) -> bool { + self.def_inside(path) || self.goto_inside(path) + } + + /// True when the tail's own definition lies strictly inside a region. + /// + /// Kept apart from a goto because the two refusals want different work: + /// a goto inside a loop can become a `break`, and a tail written inside + /// one cannot be lifted out of it at all. + fn def_inside(&self, path: &[Step]) -> bool { + self.tail.len() > path.len() && self.tail.starts_with(path) + } + + /// True when some goto to the tail lies strictly inside a region. + fn goto_inside(&self, path: &[Step]) -> bool { + self.gotos + .iter() + .any(|g| g.len() > path.len() && g.starts_with(path)) + } +} + +/// Place a block several arms reach after the branch rather than inside the +/// arm that happened to reach it first. +/// +/// A compiler's tail merging leaves one block with several predecessors, and a +/// single walk can write it under only one of them, so every other predecessor +/// becomes a goto. Duplication undoes that merge where the tail is small and +/// acyclic; where it is neither, this is the other answer. Write the tail once, +/// after the construct that holds all the arms, and each arm ends naturally and +/// falls into it. +/// +/// Sound only when every path out of that construct which must *not* run the +/// tail already ends in a control transfer, which is what `exits` decides. Where +/// it does not, nothing moves: an arm quietly falling into code it used to skip +/// is exactly the wrong output a goto avoids. +/// +/// Returns one entry per goto removed, so the caller can take them back off the +/// histogram they were counted into. +fn sink_shared_tails( + root: &mut Region, + graph: &Graph, + labels: &mut BTreeSet, + refused: &mut BTreeMap<&'static str, usize>, + breaks: bool, +) -> Vec { + let mut removed: Vec = Vec::new(); + // Sinking one tail exposes the next: the arm that stopped jumping now ends + // where the following tail begins. Bounded, and each round either removes a + // goto or stops. + let mut last: BTreeMap<&'static str, usize> = BTreeMap::new(); + for _ in 0..8 { + let targets: Vec = labels.iter().copied().collect(); + let mut progress = false; + last = BTreeMap::new(); + for target in targets { + let n = sink_one(root, graph, target, &mut last, breaks); + if n == 0 { + continue; + } + progress = true; + removed.extend(std::iter::repeat_n(target, n)); + if !mentions_goto(root, target) { + labels.remove(&target); + } + } + if !progress { + break; + } + } + // Only the last round counts: the earlier ones are describing tails that + // have since been placed. + for (reason, n) in last { + *refused.entry(reason).or_default() += n; + } + if !removed.is_empty() { + tidy(root); + } + removed +} + +/// Sink the tail beginning at `target`, returning how many gotos that removed. +fn sink_one( + root: &mut Region, + graph: &Graph, + target: Addr, + refused: &mut BTreeMap<&'static str, usize>, + breaks: bool, +) -> usize { + let mut defs: Vec> = Vec::new(); + let mut gotos: Vec> = Vec::new(); + sites(root, target, &mut Vec::new(), &mut defs, &mut gotos); + if gotos.is_empty() { + return 0; + } + // Two copies of the tail already exist, so which one a goto meant is not + // knowable here. Duplication bars that case and so does this. + if defs.len() != 1 { + *refused.entry("tail written more than once").or_default() += gotos.len(); + return 0; + } + let def = defs.remove(0); + + // What moves is everything from the tail's first block to the end of the + // sequence holding it, so whatever already ran after it still does. + let (container, from) = match def.split_last() { + Some((Step::Part(i), rest)) => (rest.to_vec(), Some(*i)), + _ => (def.clone(), None), + }; + // A goto inside what is moving would be jumping to the top of its own new + // home, which is a different program. + let inside = |p: &Vec| match from { + Some(i) => { + p.len() > container.len() + && p.starts_with(&container) + && matches!(p[container.len()], Step::Part(j) if j >= i) + } + None => p.starts_with(&def), + }; + if gotos.iter().any(inside) { + *refused + .entry("jumped to backwards, into the tail") + .or_default() += gotos.len(); + return 0; + } + + // The innermost construct holding both the tail and every goto to it. + // Sinking past that would change what runs after it. + let mut common = def.clone(); + for g in &gotos { + let n = common + .iter() + .zip(g.iter()) + .take_while(|(a, b)| a == b) + .count(); + common.truncate(n); + } + + let cut = common.len(); + let goto_holes: BTreeSet> = gotos.iter().map(|g| g[cut..].to_vec()).collect(); + let holes = Holes { + tail: &def[cut..], + gotos: &goto_holes, + }; + let Some(region) = node(root, &common) else { + return 0; + }; + let mut why: Option<&'static str> = None; + match exits(region, &mut Vec::new(), &holes, graph, &mut why, breaks) { + Some(Exit::Hole) => {} + // The arms that have to skip the tail do not all leave by themselves, + // so one of them would fall into it. This is the bucket a reaching + // condition would have to take. + Some(_) => { + *refused + .entry("an arm would fall into the tail") + .or_default() += gotos.len(); + return 0; + } + // The jump is out of a loop or a switch arm, where falling through + // does not go where the goto went. + None => { + *refused + .entry(why.unwrap_or("jumped out of a loop or a switch arm")) + .or_default() += gotos.len(); + return 0; + } + } + + // Which gotos have to leave a `break` behind, decided before anything + // moves: a path into the tail being cut out would stop resolving. + let leave_break: Vec = gotos + .iter() + .map(|g| needs_break(region, &g[cut..])) + .collect(); + + // Sound: take the tail out, drop the gotos, write the tail after. + let removed = gotos.len(); + let tail = take_tail(root, &container, from); + for (g, brk) in gotos.iter().zip(leave_break) { + if let Some(slot) = node_mut(root, g) { + *slot = if brk { Region::Break } else { Region::Empty }; + } + } + let Some(slot) = node_mut(root, &common) else { + return 0; + }; + let mut parts = match std::mem::replace(slot, Region::Empty) { + Region::Seq(v) => v, + other => vec![other], + }; + match tail { + Region::Seq(v) => parts.extend(v), + other => parts.push(other), + } + *slot = match parts.len() { + 0 => Region::Empty, + 1 => parts.pop().unwrap(), + _ => Region::Seq(parts), + }; + removed +} + +/// Tidy what sinking left: the gotos it removed are holes in the tree. +/// +/// Both rewrites emit the same C, except that an `if` whose body is now empty +/// and whose `else` is not reads as the negated test with one arm, which is +/// what the source would have said. +fn tidy(r: &mut Region) { + match r { + Region::Seq(parts) => { + for p in parts.iter_mut() { + tidy(p); + } + parts.retain(|p| *p != Region::Empty); + match parts.len() { + 0 => *r = Region::Empty, + 1 => *r = parts.pop().unwrap(), + _ => {} + } + } + Region::If { + invert, + then, + otherwise, + .. + } => { + tidy(then); + if let Some(o) = otherwise.as_deref_mut() { + tidy(o); + } + if **then == Region::Empty { + if let Some(o) = otherwise.take().filter(|o| **o != Region::Empty) { + *then = o; + *invert = !*invert; + } + } else if otherwise.as_deref() == Some(&Region::Empty) { + *otherwise = None; + } + } + Region::While { body, .. } | Region::Infinite { body, .. } => tidy(body), + Region::Switch { cases, default, .. } => { + for c in cases.iter_mut() { + tidy(&mut c.body); + } + if let Some(d) = default.as_deref_mut() { + tidy(d); + } + } + _ => {} + } +} + +/// Cut the tail out of where it was written, leaving nothing behind. +fn take_tail(root: &mut Region, container: &[Step], from: Option) -> Region { + let Some(slot) = node_mut(root, container) else { + return Region::Empty; + }; + match from { + Some(i) => { + let Region::Seq(parts) = slot else { + return Region::Empty; + }; + let mut rest: Vec = parts.split_off(i); + match rest.len() { + 1 => rest.pop().unwrap(), + _ => Region::Seq(rest), + } + } + None => std::mem::replace(slot, Region::Empty), + } +} + +/// Where a block's statements are written, and where they are jumped to. +/// +/// An `if` or a `switch` head is written by the `Block` beside it, so only a +/// `Block` and a loop's own head hold statements. +fn sites( + r: &Region, + target: Addr, + path: &mut Vec, + defs: &mut Vec>, + gotos: &mut Vec>, +) { + match r { + Region::Block(at) => { + if *at == target { + defs.push(path.clone()); + } + } + Region::Goto(at) => { + if *at == target { + gotos.push(path.clone()); + } + } + Region::Seq(parts) => { + for (i, p) in parts.iter().enumerate() { + path.push(Step::Part(i)); + sites(p, target, path, defs, gotos); + path.pop(); + } + } + Region::If { + then, otherwise, .. + } => { + path.push(Step::Then); + sites(then, target, path, defs, gotos); + path.pop(); + if let Some(o) = otherwise { + path.push(Step::Else); + sites(o, target, path, defs, gotos); + path.pop(); + } + } + Region::While { head, body, .. } | Region::Infinite { head, body } => { + if *head == target { + defs.push(path.clone()); + } + path.push(Step::Body); + sites(body, target, path, defs, gotos); + path.pop(); + } + Region::Switch { cases, default, .. } => { + for (i, c) in cases.iter().enumerate() { + path.push(Step::Case(i)); + sites(&c.body, target, path, defs, gotos); + path.pop(); + } + if let Some(d) = default { + path.push(Step::Default); + sites(d, target, path, defs, gotos); + path.pop(); + } + } + Region::Break | Region::Continue | Region::Empty => {} + } +} + +/// True when every hole under `path` would bind its `break` to the construct +/// at `path`. +/// +/// C's `break` leaves the innermost loop or switch, so a goto written two +/// constructs deep cannot be turned into one break: it would leave the inner +/// construct and carry on there. Rather than emit two, refuse. +fn holes_bind_here(r: &Region, path: &mut Vec, holes: &Holes) -> bool { + if holes.at(path) { + return true; + } + match r { + Region::Seq(parts) => { + for (i, p) in parts.iter().enumerate() { + path.push(Step::Part(i)); + let ok = holes_bind_here(p, path, holes); + path.pop(); + if !ok { + return false; + } + } + true + } + Region::If { + then, otherwise, .. + } => { + path.push(Step::Then); + let ok = holes_bind_here(then, path, holes); + path.pop(); + if !ok { + return false; + } + match otherwise { + Some(o) => { + path.push(Step::Else); + let ok = holes_bind_here(o, path, holes); + path.pop(); + ok + } + None => true, + } + } + Region::While { .. } | Region::Infinite { .. } | Region::Switch { .. } => { + !holes.inside(path) + } + _ => true, + } +} + +/// True when a goto being removed has to leave a `break` behind. +/// +/// It sits inside a loop or a switch arm and the tail is being written after +/// that construct, so falling off the end of it would repeat the loop or run +/// the next case instead of reaching the tail. +fn needs_break(region: &Region, path: &[Step]) -> bool { + (0..path.len()).any(|i| { + matches!( + node(region, &path[..i]), + Some(Region::While { .. } | Region::Infinite { .. } | Region::Switch { .. }) + ) + }) +} + +/// How control leaves a region once the holes are cut out of it. +/// +/// `None` rejects the sink: a hole where falling through would not reach the +/// sunk tail. That is a hole inside a loop, where it falls to the next +/// iteration; inside a switch arm, where C falls into the next case; or with +/// statements written after it, which it would then run on the way. +/// +/// `breaks` allows the second answer to the first two: a goto written inside a +/// loop or a switch arm becomes the `break` of it, which lands exactly where +/// the sunk tail is about to be written. `why` names the refusal, so the +/// histogram says which shape is left rather than only how many. +fn exits( + r: &Region, + path: &mut Vec, + holes: &Holes, + graph: &Graph, + why: &mut Option<&'static str>, + breaks: bool, +) -> Option { + if holes.at(path) { + return Some(Exit::Hole); + } + match r { + // Statements run and control carries on, unless the block leaves the + // function: a return, a tail call, or a computed branch, all of which + // the emitter follows with a return. + Region::Block(at) => Some( + if graph + .get(at) + .is_some_and(|s| s.iter().any(|n| graph.contains_key(n))) + { + Exit::Other + } else { + Exit::Never + }, + ), + Region::Goto(_) | Region::Break | Region::Continue => Some(Exit::Never), + Region::Empty => Some(Exit::Other), + Region::Seq(parts) => { + let last = parts.len().saturating_sub(1); + for (i, p) in parts.iter().enumerate() { + path.push(Step::Part(i)); + let cut_here = path.as_slice() == holes.tail; + path.pop(); + // The tail may begin at the very next statement, in which case + // everything from there on moves and this sequence ends here + // too. A hole at this position then falls straight into the + // tail's new home rather than over anything. + path.push(Step::Part(i + 1)); + let cut_next = i < last && path.as_slice() == holes.tail; + path.pop(); + path.push(Step::Part(i)); + let e = exits(p, path, holes, graph, why, breaks); + path.pop(); + match e? { + // The tail itself: what follows it in this sequence is part + // of it and moves with it, so the sequence ends here. + Exit::Hole if cut_here || cut_next => return Some(Exit::Hole), + Exit::Hole if i < last => { + *why = Some("a goto has statements written after it"); + return None; + } + Exit::Other if i < last => { + // A goto buried in a part whose other paths carry on + // is lost here: `join` folds a hole into `Other`, and + // deleting the goto would leave it falling into what + // comes next rather than jumping over it. + path.push(Step::Part(i)); + let hidden = holes.goto_inside(path); + path.pop(); + // Unless the tail begins at the very next statement, + // where falling through and jumping land together. + if hidden && !cut_next { + *why = Some("a goto sits in a part that also falls through"); + return None; + } + } + Exit::Never if i < last => return Some(Exit::Never), + e => return Some(e), + } + } + Some(Exit::Other) + } + Region::If { + then, otherwise, .. + } => { + path.push(Step::Then); + let t = exits(then, path, holes, graph, why, breaks); + path.pop(); + let f = match otherwise { + Some(o) => { + path.push(Step::Else); + let f = exits(o, path, holes, graph, why, breaks); + path.pop(); + f? + } + // No else arm: the condition failing falls straight out. + None => Exit::Other, + }; + Some(join(t?, f)) + } + // A `while` is left whenever its test fails, and that path would land + // on the sunk tail without ever having jumped to it. No `break` helps. + Region::While { .. } => { + if !holes.inside(path) { + return Some(Exit::Other); + } + *why = Some(if holes.def_inside(path) { + "the tail is written inside a while" + } else { + "a while is also left by its test failing" + }); + None + } + Region::Infinite { body, .. } => { + if !holes.inside(path) { + return Some(if breaks_out(body) { + Exit::Other + } else { + Exit::Never + }); + } + if !breaks { + *why = Some("jumped out of a loop"); + return None; + } + // Lifting the tail out of the loop it is written in is a different + // program, whatever the jumps to it do. + if holes.def_inside(path) { + *why = Some("the tail is written inside a loop"); + return None; + } + // Another way out already lands after the loop, which is where the + // tail is going, so the two would be the same place and are not. + if breaks_out(body) { + *why = Some("the loop is also left by a break"); + return None; + } + path.push(Step::Body); + let bound = holes_bind_here(body, path, holes); + path.pop(); + if !bound { + *why = Some("the goto sits deeper than the loop it would break"); + return None; + } + Some(Exit::Hole) + } + Region::Switch { cases, default, .. } => { + if !holes.inside(path) { + return Some(Exit::Other); + } + if !breaks { + *why = Some("jumped out of a switch arm"); + return None; + } + if holes.def_inside(path) { + *why = Some("the tail is written inside a switch arm"); + return None; + } + // Every arm has to end by leaving the function or at a hole. A + // hole becomes the `break` C writes at the end of an arm anyway, + // so the switch as a whole is left only where the gotos went. + let arms = cases + .iter() + .enumerate() + .map(|(i, c)| (Step::Case(i), &c.body)) + .chain(default.as_deref().map(|d| (Step::Default, d))); + for (step, arm) in arms { + // A `break` already written in the arm leaves the switch by + // falling out of it, which is where the tail is going. + if breaks_out(arm) { + *why = Some("a switch arm is also left by a break"); + return None; + } + path.push(step); + let bound = !holes.inside(path) || holes_bind_here(arm, path, holes); + let e = exits(arm, path, holes, graph, why, breaks); + path.pop(); + if !bound { + *why = Some("the goto sits deeper than the switch arm it would break"); + return None; + } + match e? { + Exit::Hole | Exit::Never => {} + Exit::Other => { + *why = Some("a switch arm falls out of the switch"); + return None; + } + } + } + Some(Exit::Hole) + } + } +} + +/// True when a loop body leaves the loop rather than repeating. +fn breaks_out(r: &Region) -> bool { + match r { + Region::Break => true, + Region::Seq(parts) => parts.iter().any(breaks_out), + Region::If { + then, otherwise, .. + } => breaks_out(then) || otherwise.as_deref().is_some_and(breaks_out), + // A `break` further in belongs to that loop or switch, not this one. + _ => false, + } +} + +/// True when a tree still jumps to a block, so its label has to stay. +fn mentions_goto(r: &Region, target: Addr) -> bool { + match r { + Region::Goto(at) => *at == target, + Region::Seq(parts) => parts.iter().any(|p| mentions_goto(p, target)), + Region::If { + then, otherwise, .. + } => { + mentions_goto(then, target) + || otherwise + .as_deref() + .is_some_and(|o| mentions_goto(o, target)) + } + Region::While { body, .. } | Region::Infinite { body, .. } => mentions_goto(body, target), + Region::Switch { cases, default, .. } => { + cases.iter().any(|c| mentions_goto(&c.body, target)) + || default.as_deref().is_some_and(|d| mentions_goto(d, target)) + } + _ => false, + } +} + +fn node<'a>(r: &'a Region, path: &[Step]) -> Option<&'a Region> { + let mut at = r; + for s in path { + at = match (at, s) { + (Region::Seq(parts), Step::Part(i)) => parts.get(*i)?, + (Region::If { then, .. }, Step::Then) => then, + (Region::If { otherwise, .. }, Step::Else) => otherwise.as_deref()?, + (Region::While { body, .. } | Region::Infinite { body, .. }, Step::Body) => body, + (Region::Switch { cases, .. }, Step::Case(i)) => &cases.get(*i)?.body, + (Region::Switch { default, .. }, Step::Default) => default.as_deref()?, + _ => return None, + }; + } + Some(at) +} + +fn node_mut<'a>(r: &'a mut Region, path: &[Step]) -> Option<&'a mut Region> { + let mut at = r; + for s in path { + at = match (at, s) { + (Region::Seq(parts), Step::Part(i)) => parts.get_mut(*i)?, + (Region::If { then, .. }, Step::Then) => then, + (Region::If { otherwise, .. }, Step::Else) => otherwise.as_deref_mut()?, + (Region::While { body, .. } | Region::Infinite { body, .. }, Step::Body) => body, + (Region::Switch { cases, .. }, Step::Case(i)) => &mut cases.get_mut(*i)?.body, + (Region::Switch { default, .. }, Step::Default) => default.as_deref_mut()?, + _ => return None, + }; + } + Some(at) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn structure(entry: Addr, graph: &Graph) -> Structured { + super::structure(entry, graph, &Taken::new()) + } + + fn g(edges: &[(u64, &[u64])]) -> Graph { + edges + .iter() + .map(|(from, to)| (Addr(*from), to.iter().map(|a| Addr(*a)).collect())) + .collect() + } + + #[test] + fn a_straight_line_is_a_sequence_with_no_gotos() { + let graph = g(&[(0, &[1]), (1, &[2]), (2, &[])]); + let s = structure(Addr(0), &graph); + assert_eq!(s.gotos, 0); + assert!(s.labels.is_empty()); + assert!(matches!(s.root, Region::Seq(_))); + } + + #[test] + fn a_diamond_is_an_if_else() { + // 0 branches to 1 and 2, both reaching 3. + let graph = g(&[(0, &[1, 2]), (1, &[3]), (2, &[3]), (3, &[])]); + let s = structure(Addr(0), &graph); + assert_eq!(s.gotos, 0, "a diamond should need no goto"); + let Region::Seq(parts) = &s.root else { + panic!("expected a sequence, got {:?}", s.root); + }; + assert!( + parts.iter().any(|r| matches!( + r, + Region::If { + otherwise: Some(_), + .. + } + )), + "no if/else in {parts:?}" + ); + } + + #[test] + fn a_branch_with_one_arm_is_an_if_without_an_else() { + // 0 branches to 1 or straight to 2; 1 also reaches 2. + let graph = g(&[(0, &[1, 2]), (1, &[2]), (2, &[])]); + let s = structure(Addr(0), &graph); + assert_eq!(s.gotos, 0); + let found = collect(&s.root).into_iter().any(|r| { + matches!( + r, + Region::If { + otherwise: None, + .. + } + ) + }); + assert!(found, "no plain if in {:?}", s.root); + } + + #[test] + fn a_back_edge_becomes_a_loop() { + // 0 enters 1; 1 branches back to itself or out to 2. + let graph = g(&[(0, &[1]), (1, &[1, 2]), (2, &[])]); + let s = structure(Addr(0), &graph); + let found = collect(&s.root) + .into_iter() + .any(|r| matches!(r, Region::While { .. } | Region::Infinite { .. })); + assert!(found, "no loop in {:?}", s.root); + } + + #[test] + fn an_irreducible_graph_produces_gotos_rather_than_a_wrong_shape() { + // Two entries into the same loop, which no `while` describes. The + // structuring has to say so instead of inventing one. + let graph = g(&[ + (0, &[1, 2]), + (1, &[3]), + (2, &[4]), + (3, &[4]), + (4, &[3, 5]), + (5, &[]), + ]); + let s = structure(Addr(0), &graph); + // A block can appear more than once: a small tail shared by several + // arms is written out in each of them rather than jumped to. What must + // not happen is unbounded duplication. + let regions = collect(&s.root); + let blocks: Vec = regions + .iter() + .filter_map(|r| match r { + Region::Block(a) => Some(*a), + _ => None, + }) + .collect(); + assert!( + blocks.len() <= graph.len() * 4, + "{} blocks emitted for a graph of {}", + blocks.len(), + graph.len() + ); + } + + /// The shape the corpus is mostly made of: two arms reach one tail, and + /// only one of them can be the place it is written. Copying the tail into + /// the other is what a compiler's tail merging undid. + #[test] + fn a_tail_two_arms_share_is_copied_rather_than_jumped_to() { + // 0 branches to 1 or 2; 1 branches to 3 or 4; 2 falls into 4; both 3 + // and 4 reach 5. Block 4 post-dominates nothing, so no join places it. + let graph = g(&[ + (0, &[1, 2]), + (1, &[3, 4]), + (2, &[4]), + (3, &[5]), + (4, &[5]), + (5, &[]), + ]); + let s = structure(Addr(0), &graph); + assert_eq!(s.gotos, 0, "{:?}", s.root); + assert!(s.labels.is_empty()); + assert!(s.lost.is_empty()); + } + + /// Copying is bounded. A fan of arms that all end at one long tail must not + /// write the tail out once per arm. + #[test] + fn copying_a_tail_stays_within_its_budget() { + // A chain of ten two-way branches, every one of whose taken arms lands + // on the same eight-block tail. + let mut edges: Vec<(u64, Vec)> = Vec::new(); + for i in 0..10u64 { + edges.push((i, vec![i + 1, 100])); + } + edges.push((10, vec![100])); + for j in 0..7u64 { + edges.push((100 + j, vec![101 + j])); + } + edges.push((107, vec![])); + let graph: Graph = edges + .iter() + .map(|(f, t)| (Addr(*f), t.iter().map(|a| Addr(*a)).collect())) + .collect(); + let s = structure(Addr(0), &graph); + assert!(s.lost.is_empty(), "lost {:?}", s.lost); + let blocks = collect(&s.root) + .iter() + .filter(|r| matches!(r, Region::Block(_))) + .count(); + let ceiling = graph.len() + duplication_budget(graph.len()) as usize; + assert!( + blocks <= ceiling, + "{blocks} blocks emitted for a graph of {}, ceiling {ceiling}", + graph.len() + ); + } + + #[test] + fn structuring_terminates_on_a_graph_that_is_all_back_edges() { + let mut edges: Vec<(u64, Vec)> = Vec::new(); + for i in 0..64u64 { + edges.push((i, vec![(i + 1) % 64, i.saturating_sub(1)])); + } + let graph: Graph = edges + .iter() + .map(|(f, t)| (Addr(*f), t.iter().map(|a| Addr(*a)).collect())) + .collect(); + let s = structure(Addr(0), &graph); + // The only requirement is that it finished. + assert!(s.gotos < 1000); + } + + /// Sinking's shape: two arms both end at one tail, and the tail is too + /// large to copy. Writing it after the branch costs nothing and removes + /// the goto; writing it inside one arm is what needed the goto. + #[test] + fn a_tail_too_large_to_copy_is_written_after_the_branch() { + // 0 branches to 1 or 2. Both reach 3, which no join places because 2 + // can also return at 20. The tail 3..8 is six blocks, past + // MAX_COPIED_TAIL, so duplication will not take it. + let graph = g(&[ + (0, &[1, 2]), + (1, &[3]), + (2, &[3, 20]), + (20, &[]), + (3, &[4]), + (4, &[5]), + (5, &[6]), + (6, &[7]), + (7, &[8]), + (8, &[]), + ]); + let s = structure(Addr(0), &graph); + assert_eq!(s.gotos, 0, "{:?}", s.root); + assert!(s.labels.is_empty(), "{:?}", s.labels); + assert!(s.lost.is_empty(), "lost {:?}", s.lost); + // Written once, not copied into both arms. + let mut counts = BTreeMap::new(); + occurrences(&s.root, &mut counts); + assert_eq!(counts.get(&Addr(3)), Some(&1), "{:?}", s.root); + } + + /// The soundness bound. An arm that has to skip the tail and does not end + /// in a control transfer would fall into it, so nothing may move. + #[test] + fn a_tail_is_not_sunk_past_an_arm_that_would_fall_into_it() { + // 0 branches to 1 or 2; 1 reaches the tail at 3, 2 reaches the tail + // too but only through 8, and 8 can also reach 9 which returns + // separately. Block 2's other way out, 9, leaves the branch without + // ending control, so the tail cannot be written after it. + let graph = g(&[ + (0, &[1, 2]), + (1, &[3]), + (2, &[8, 9]), + (8, &[3]), + (9, &[10]), + (10, &[]), + (3, &[4]), + (4, &[5]), + (5, &[6]), + (6, &[]), + ]); + let s = structure(Addr(0), &graph); + assert!(s.lost.is_empty(), "lost {:?}", s.lost); + // Whatever it does, it must not have written 3 more than once nor + // dropped it. + let mut counts = BTreeMap::new(); + occurrences(&s.root, &mut counts); + assert!(counts.get(&Addr(3)).copied().unwrap_or(0) >= 1); + } + + /// Sinking must not reach into a loop: a fall-through there repeats + /// instead of leaving. + #[test] + fn a_tail_reached_from_inside_a_loop_is_not_sunk_out_of_it() { + let graph = g(&[ + (0, &[1]), + (1, &[2, 5]), + (2, &[1, 5]), + (5, &[6]), + (6, &[7]), + (7, &[8]), + (8, &[9]), + (9, &[]), + ]); + let s = structure(Addr(0), &graph); + assert!(s.lost.is_empty(), "lost {:?}", s.lost); + let mut counts = BTreeMap::new(); + occurrences(&s.root, &mut counts); + for at in [5u64, 6, 7, 8, 9] { + assert_eq!( + counts.get(&Addr(at)).copied().unwrap_or(0), + 1, + "block {at} written {:?} times in {:?}", + counts.get(&Addr(at)), + s.root + ); + } + } + + /// A goto out of a switch arm is the `break` C writes there anyway, so the + /// tail can be written after the switch rather than jumped to. + #[test] + fn a_goto_out_of_a_switch_arm_becomes_a_break() { + // One arm reaches the tail at 10, the other returns at 2. The tail is + // written in the `if`'s other side, so reaching it from the arm needs + // a goto unless the switch is left by falling out of it. + let graph = g(&[ + (0, &[5, 6]), + (5, &[10]), + (6, &[1, 2]), + (1, &[10]), + (2, &[]), + (10, &[11]), + (11, &[]), + ]); + let mut root = Region::Seq(vec![ + Region::Block(Addr(0)), + Region::If { + head: Addr(0), + invert: false, + then: Box::new(Region::Seq(vec![ + Region::Block(Addr(5)), + Region::Block(Addr(10)), + Region::Block(Addr(11)), + ])), + otherwise: Some(Box::new(Region::Switch { + head: Addr(6), + cases: vec![ + Case { + values: vec![0], + body: Region::Seq(vec![Region::Block(Addr(1)), Region::Goto(Addr(10))]), + }, + Case { + values: vec![1], + body: Region::Block(Addr(2)), + }, + ], + default: None, + })), + }, + ]); + let mut labels: BTreeSet = [Addr(10)].into_iter().collect(); + let mut refused = BTreeMap::new(); + let removed = sink_shared_tails(&mut root, &graph, &mut labels, &mut refused, true); + + assert_eq!(removed, vec![Addr(10)], "{root:?}\n{refused:?}"); + assert!(!mentions_goto(&root, Addr(10)), "{root:?}"); + let mut counts = BTreeMap::new(); + occurrences(&root, &mut counts); + assert_eq!(counts.get(&Addr(10)), Some(&1), "{root:?}"); + assert_eq!(counts.get(&Addr(11)), Some(&1), "{root:?}"); + // The arm has to say it leaves the switch; running off its end would + // fall into the next case. + assert!( + collect(&root).contains(&Region::Break), + "no break left behind in {root:?}" + ); + } + + /// The same for a loop with no other way out: the goto is the only exit, + /// so it becomes the `break` and the tail goes after the loop. + #[test] + fn a_goto_out_of_an_endless_loop_becomes_a_break() { + let graph = g(&[ + (0, &[5, 6]), + (5, &[10]), + (6, &[6, 10]), + (10, &[11]), + (11, &[]), + ]); + let mut root = Region::Seq(vec![ + Region::Block(Addr(0)), + Region::If { + head: Addr(0), + invert: false, + then: Box::new(Region::Seq(vec![ + Region::Block(Addr(5)), + Region::Block(Addr(10)), + Region::Block(Addr(11)), + ])), + otherwise: Some(Box::new(Region::Infinite { + head: Addr(6), + body: Box::new(Region::If { + head: Addr(6), + invert: false, + then: Box::new(Region::Goto(Addr(10))), + otherwise: None, + }), + })), + }, + ]); + let mut labels: BTreeSet = [Addr(10)].into_iter().collect(); + let mut refused = BTreeMap::new(); + let removed = sink_shared_tails(&mut root, &graph, &mut labels, &mut refused, true); + + assert_eq!(removed, vec![Addr(10)], "{root:?}\n{refused:?}"); + assert!(!mentions_goto(&root, Addr(10)), "{root:?}"); + assert!( + collect(&root).contains(&Region::Break), + "no break left behind in {root:?}" + ); + } + + /// A loop with another way out already lands after itself, which is where + /// the tail would go, so the two exits would become one place. + #[test] + fn a_loop_with_another_exit_keeps_its_goto() { + let graph = g(&[ + (0, &[5, 6]), + (5, &[10]), + (6, &[6, 10]), + (10, &[11]), + (11, &[]), + ]); + let mut root = Region::Seq(vec![ + Region::Block(Addr(0)), + Region::If { + head: Addr(0), + invert: false, + then: Box::new(Region::Seq(vec![ + Region::Block(Addr(5)), + Region::Block(Addr(10)), + Region::Block(Addr(11)), + ])), + otherwise: Some(Box::new(Region::Infinite { + head: Addr(6), + body: Box::new(Region::Seq(vec![ + Region::If { + head: Addr(6), + invert: false, + then: Box::new(Region::Break), + otherwise: None, + }, + Region::If { + head: Addr(7), + invert: false, + then: Box::new(Region::Goto(Addr(10))), + otherwise: None, + }, + ])), + })), + }, + ]); + let mut labels: BTreeSet = [Addr(10)].into_iter().collect(); + let mut refused = BTreeMap::new(); + let removed = sink_shared_tails(&mut root, &graph, &mut labels, &mut refused, true); + assert!(removed.is_empty(), "{root:?}"); + assert!(mentions_goto(&root, Addr(10)), "{root:?}"); + } + + /// The soundness hole `exits` had: a goto buried in an `if` whose other + /// side carries on folds into `Exit::Other`, and the hole goes with it. The + /// sink then deleted the goto, and what had jumped over block 20 fell into + /// it instead. Nothing downstream could tell: the output still compiled. + /// + #[test] + fn a_goto_that_jumps_over_a_statement_is_not_deleted() { + let graph = g(&[(10, &[20, 30]), (20, &[30]), (30, &[31]), (31, &[])]); + let mut root = Region::Seq(vec![ + Region::If { + head: Addr(10), + invert: false, + then: Box::new(Region::Goto(Addr(30))), + otherwise: None, + }, + Region::Block(Addr(20)), + Region::Block(Addr(30)), + Region::Block(Addr(31)), + ]); + let mut labels: BTreeSet = [Addr(30)].into_iter().collect(); + let mut refused = BTreeMap::new(); + let removed = sink_shared_tails(&mut root, &graph, &mut labels, &mut refused, true); + assert!(removed.is_empty(), "{root:?}"); + assert!( + mentions_goto(&root, Addr(30)), + "the jump over block 20 was deleted: {root:?}" + ); + } + + /// A tail that only goes back to the top of the loop is one block and a + /// `continue`, and used to be measured as the whole graph beyond the + /// header. Counting what the walk would write is what makes it copyable. + #[test] + fn a_tail_that_only_continues_the_loop_is_copied() { + // 1 is a `while` whose body branches at 2. Both arms reach 5, which + // goes back to the top. The branch's join is 20, outside the loop, so + // the walk from 5 does not stop before the header. + let graph = g(&[ + (0, &[1]), + (1, &[2, 20]), + (2, &[3, 4]), + (3, &[5]), + (4, &[5, 7]), + (5, &[1]), + (7, &[20]), + (20, &[21]), + (21, &[22]), + (22, &[23]), + (23, &[24]), + (24, &[]), + ]); + let s = structure(Addr(0), &graph); + assert_eq!(s.gotos, 0, "{:?}", s.root); + assert!(s.labels.is_empty(), "{:?}", s.labels); + assert!(s.lost.is_empty(), "lost {:?}", s.lost); + // The answer is a copy of one block, not a label. + let mut counts = BTreeMap::new(); + occurrences(&s.root, &mut counts); + assert_eq!(counts.get(&Addr(5)), Some(&2), "{:?}", s.root); + } + + /// A loop exit reached from inside a switch arm is not written as a + /// `break`: C binds that to the switch, so it leaves the switch and runs + /// what follows it inside the loop, then repeats. + /// + /// Found in `__gettextparse` of `hello.static.a64`, where the `break` sat + /// inside a `case` thirty levels deep and the loop exit is at the + /// function's top level. The output compiled either way. + #[test] + fn a_loop_exit_inside_a_switch_arm_is_not_written_as_a_break() { + // The shape that makes it visible: the switch's join is not the loop's + // exit, so falling out of the switch is not falling out of the loop. + // Two ways out of the loop, 30 and 40, put the join past both. + let graph = g(&[ + (0, &[1]), + (1, &[2]), + (2, &[3, 4]), + (3, &[30, 9]), + (4, &[9, 40]), + (9, &[1, 30]), + (30, &[31]), + (40, &[31]), + (31, &[]), + ]); + let switches: Switches = [(Addr(2), vec![(0, Addr(3)), (1, Addr(4))])] + .into_iter() + .collect(); + let s = super::structure_with(Addr(0), &graph, &Taken::new(), &switches); + + assert!(s.lost.is_empty(), "lost {:?}", s.lost); + let arms = switch_arms(&s.root).expect("a switch was built"); + for arm in &arms { + assert!( + !breaks_out(arm), + "an arm leaves the loop with a `break` C binds to the switch: {:?}", + s.root + ); + } + } + + /// The arms of the first switch in a tree. + fn switch_arms(r: &Region) -> Option> { + collect(r).into_iter().find_map(|x| match x { + Region::Switch { cases, default, .. } => Some( + cases + .into_iter() + .map(|c| c.body) + .chain(default.map(|d| *d)) + .collect(), + ), + _ => None, + }) + } + + /// Every region in a tree, flattened. + fn collect(r: &Region) -> Vec { + let mut out = vec![r.clone()]; + match r { + Region::Seq(parts) => { + for p in parts { + out.extend(collect(p)); + } + } + Region::If { + then, otherwise, .. + } => { + out.extend(collect(then)); + if let Some(o) = otherwise { + out.extend(collect(o)); + } + } + Region::While { body, .. } | Region::Infinite { body, .. } => { + out.extend(collect(body)); + } + Region::Switch { cases, default, .. } => { + for c in cases { + out.extend(collect(&c.body)); + } + if let Some(d) = default { + out.extend(collect(d)); + } + } + _ => {} + } + out + } +} diff --git a/crates/e5r-decomp/tests/corpus.rs b/crates/e5r-decomp/tests/corpus.rs new file mode 100644 index 0000000..04f98f1 --- /dev/null +++ b/crates/e5r-decomp/tests/corpus.rs @@ -0,0 +1,107 @@ +//! A measurement, not a gate. +//! +//! The structuring quality numbers the roadmap publishes are corpus numbers, +//! and the twenty fixtures `quality.rs` holds a ceiling over are too few to +//! tell a real movement from noise. This walks every built fixture and prints +//! the totals, so two builds can be compared. Ignored by default because it is +//! slow and asserts nothing. + +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program}; +use e5r_format::LoadOptions; + +fn build_dir() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(build_dir()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(e5r_analysis::analyze(obj, &Options::default())) +} + +#[test] +#[ignore = "a measurement over the whole corpus, printed rather than asserted"] +fn corpus_structuring_totals() { + let Some(dir) = build_dir() else { return }; + let mut names: Vec = std::fs::read_dir(&dir) + .into_iter() + .flatten() + .flatten() + .filter(|e| e.file_type().is_ok_and(|t| t.is_file())) + .map(|e| e.file_name().to_string_lossy().into_owned()) + .collect(); + names.sort(); + + let (mut gotos, mut lost, mut functions, mut unstructured, mut bytes) = (0usize, 0, 0, 0, 0); + for name in names { + let Some(p) = open(&name) else { continue }; + let targets: Vec<&e5r_analysis::Function> = p + .functions_by_address() + .filter(|f| f.is_complete()) + .collect(); + if targets.is_empty() { + continue; + } + // Exactly the path `no_block_is_lost` takes. Note when reading an + // `E5R_GOTO_STATS` histogram from this run that it counts more + // structurings than there are functions here: two passes, over the + // targets and over the callees around them that never reach the + // output. + for d in e5r_api::decompile_program(&p, &targets).functions { + functions += 1; + gotos += d.gotos; + lost += d.lost; + unstructured += usize::from(d.gotos > 0); + bytes += d.text.len(); + } + } + println!( + "CORPUS functions={functions} gotos={gotos} lost={lost} \ + unstructured={unstructured} share={:.4} bytes={bytes}", + unstructured as f64 / functions.max(1) as f64 + ); +} + +/// The whole corpus's decompiled text, for comparing two structurings. +/// +/// A change that moves output size has to show what moved. Dumping every +/// function twice and comparing the multiset of lines is what proves a +/// difference is the gotos and the labels rather than code appearing or +/// disappearing. Writes to the path in `E5R_DUMP`. +#[test] +#[ignore = "a dump for comparing two builds, written rather than asserted"] +fn corpus_dump() { + use std::io::Write; + + let Some(dir) = build_dir() else { return }; + let Some(out) = std::env::var_os("E5R_DUMP") else { + return; + }; + let mut names: Vec = std::fs::read_dir(&dir) + .into_iter() + .flatten() + .flatten() + .filter(|e| e.file_type().is_ok_and(|t| t.is_file())) + .map(|e| e.file_name().to_string_lossy().into_owned()) + .collect(); + names.sort(); + + let mut w = std::io::BufWriter::new(std::fs::File::create(out).unwrap()); + for name in names { + let Some(p) = open(&name) else { continue }; + let targets: Vec<&e5r_analysis::Function> = p + .functions_by_address() + .filter(|f| f.is_complete()) + .collect(); + if targets.is_empty() { + continue; + } + for d in e5r_api::decompile_program(&p, &targets).functions { + let _ = writeln!(w, "// {name} {}", d.name); + let _ = w.write_all(d.text.as_bytes()); + } + } +} diff --git a/crates/e5r-decomp/tests/datatests.rs b/crates/e5r-decomp/tests/datatests.rs new file mode 100644 index 0000000..6a2d447 --- /dev/null +++ b/crates/e5r-decomp/tests/datatests.rs @@ -0,0 +1,953 @@ +//! Ghidra's decompiler datatests, ported. +//! +//! `Ghidra/Features/Decompiler/src/decompile/datatests/` holds eighty-nine +//! saved programs, each with a list of regular expressions its output must +//! match. They are two decades of decompiler bugs written down: a loop that +//! came out as a goto, a division the algebra did not fold back, a comparison +//! that printed the wrong way round. None of them can be run here, because the +//! programs are stored in Ghidra's own XML and the assertions are against +//! Ghidra's own text. +//! +//! So the port is by behaviour, not by text. Each case here is C written to +//! make a compiler emit the shape the original pinned, built into +//! `fixtures/datatests/`, and asserted on by the property the original was +//! about: that the loop is a loop, that the switch is a switch, that the second +//! store did not merge with the first. Never by whole-text comparison, which +//! would only say that our output is not Ghidra's, which it never will be. +//! +//! Cases marked `#[ignore]` are the ones where our decompiler has the bug the +//! datatest was written to catch. They are kept, and named after what is wrong, +//! because a datatest that is deleted when it fails measures nothing. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex, OnceLock}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_format::LoadOptions; + +/// Every function of one fixture, decompiled together, indexed by name. +type Functions = BTreeMap; + +fn build_dir() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(build_dir()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// One fixture, analyzed and decompiled once however many cases ask for it. +/// +/// Fifty cases over sixteen builds is eight hundred decompilations of sixteen +/// programs if nothing remembers, and the tests run on threads, so the memo has +/// to be shared rather than thread-local. +fn functions(binary: &str) -> Option> { + static CACHE: OnceLock>>>> = OnceLock::new(); + let cache = CACHE.get_or_init(|| Mutex::new(BTreeMap::new())); + if let Some(hit) = cache.lock().unwrap().get(binary) { + return hit.clone(); + } + let built = open(binary).map(|p| { + // The same path the command line takes, so a case that passes here is a + // case a user sees pass. + let targets: Vec<&e5r_analysis::Function> = p + .functions_by_address() + .filter(|f| f.is_complete()) + .collect(); + let unit = e5r_api::decompile_program(&p, &targets); + Arc::new( + unit.functions + .into_iter() + .map(|f| (f.name.clone(), f.text)) + .collect::(), + ) + }); + cache + .lock() + .unwrap() + .insert(binary.to_string(), built.clone()); + built +} + +/// One function's output in one build, with what is being checked attached to +/// every failure so a report names the case, the build and the property. +struct Case { + build: String, + name: String, + text: String, +} + +impl Case { + fn fail(&self, what: &str) -> ! { + panic!( + "{}: {} in `{}` [{}]:\n{}", + self.name, what, self.build, self.build, self.text + ); + } + + /// The output says this. + fn has(&self, needle: &str) -> &Self { + if !self.text.contains(needle) { + self.fail(&format!("expected `{needle}`")); + } + self + } + + /// The output says at least one of these, for a property two architectures + /// spell differently. + fn has_any(&self, needles: &[&str]) -> &Self { + if !needles.iter().any(|n| self.text.contains(n)) { + self.fail(&format!("expected one of {needles:?}")); + } + self + } + + /// The output does not say this. + fn lacks(&self, needle: &str) -> &Self { + if self.text.contains(needle) { + self.fail(&format!("did not expect `{needle}`")); + } + self + } + + /// The output says this exactly this many times. + fn times(&self, needle: &str, n: usize) -> &Self { + let got = self.text.matches(needle).count(); + if got != n { + self.fail(&format!("expected `{needle}` {n} times, found {got}")); + } + self + } + + /// The output says this at least this many times. + fn at_least(&self, needle: &str, n: usize) -> &Self { + let got = self.text.matches(needle).count(); + if got < n { + self.fail(&format!( + "expected `{needle}` at least {n} times, found {got}" + )); + } + self + } +} + +/// Check one property on every named build that was actually produced. +/// +/// A missing fixture skips rather than fails: the corpus is gitignored, and a +/// checkout that has not run `scripts/build-fixtures.sh` still runs the suite. +fn case(builds: &[String], function: &str, check: impl Fn(&Case)) { + for build in builds { + let Some(fns) = functions(build) else { + continue; + }; + let Some(text) = fns.get(function) else { + panic!("{function}: not recovered from {build}"); + }; + check(&Case { + build: build.clone(), + name: function.to_string(), + text: text.clone(), + }); + } +} + +/// Every build of one fixture: two architectures, two optimization levels. +fn all(source: &str) -> Vec { + let mut out = Vec::new(); + for arch in ["a64", "x64"] { + for opt in ["O1", "O2"] { + out.push(format!("dt-{source}.{arch}.{opt}")); + } + } + out +} + +/// One architecture's builds, for a property only that architecture shows. +fn arch(source: &str, arch: &str) -> Vec { + ["O1", "O2"] + .iter() + .map(|opt| format!("dt-{source}.{arch}.{opt}")) + .collect() +} + +/// One optimization level's builds, for a case the other level compiles away. +fn opt(source: &str, opt: &str) -> Vec { + ["a64", "x64"] + .iter() + .map(|a| format!("dt-{source}.{a}.{opt}")) + .collect() +} + +// --- control flow ----------------------------------------------------------- + +/// forloop1.xml: a counted loop is a loop. +/// +/// Ghidra asks for `for (i = 0; i < max; i = i + 1)`. We have no `for` in the +/// region vocabulary, so the property is the one underneath it: the back edge +/// came out as a loop with the call inside, and nothing needed a label. +#[test] +fn forloop1() { + case(&all("control"), "forloop1", |c| { + c.has("while (").has("sink(").times("goto ", 0); + }); +} + +/// forloop_withskip.xml: a loop whose body can bump the loop variable again. +/// The extra increment must not break the loop apart. +#[test] +fn forloop_withskip() { + case(&all("control"), "forloop_withskip", |c| { + c.has("while (").has("source()").times("goto ", 0); + }); +} + +/// noforloop_globcall.xml: the loop variable is a global a call can change, so +/// it has to be re-read each time round rather than held in a register. +#[test] +fn noforloop_globcall() { + case(&all("control"), "noforloop_globcall", |c| { + c.has("while (").has("sink(1)").times("goto ", 0); + }); +} + +/// noforloop_iterused.xml: the loop variable outlives the loop, and the value +/// it carries out has to be the one the last iteration left. +#[test] +fn noforloop_iterused() { + case(&all("control"), "noforloop_iterused", |c| { + c.has("while (").has("sink(").has("100"); + }); +} + +/// copytrim.xml: a loop that counts down to zero, where the iterator is a +/// subtraction rather than an addition. +#[test] +fn countdown_loop() { + case(&all("control"), "countdown", |c| { + c.has("while (").has("- 1").times("goto ", 0); + }); +} + +/// ifnoexit.xml: a loop body whose early exits are returns. The three tested +/// values have to survive as tests, not collapse into the accumulation. +/// +/// O1 only: at O2 clang turns the whole body into a branchless accumulate, so +/// there is nothing left to test. +#[test] +fn ifnoexit() { + case(&opt("control", "O1"), "ifnoexit", |c| { + c.has("while (").has("100").has("200").has("300"); + }); +} + +/// elseif.xml: a six-way ladder stays one chain of tests and needs no label. +/// +/// Ghidra additionally asks for the `else if` spelling; ours nests the `if` +/// inside the `else` block instead, which says the same thing with more braces. +#[test] +fn elseif() { + case(&all("control"), "elseif", |c| { + c.at_least("if (", 5).times("goto ", 0); + for n in 1..=6 { + c.has(&format!("sink{n}(")); + } + }); +} + +/// orcompare.xml: two comparisons joined by a short-circuit or. +/// +/// Ghidra fuses the pair into `if (a == 10 || b == 20)`. We do not fuse, so the +/// property is what fusing is derived from: both comparisons are there, and the +/// arm they share is reached from each of them. +#[test] +fn orcompare() { + case(&all("control"), "orcompare", |c| { + c.has("10").has("20").times("sink1(", 2).times("sink2(", 1); + }); +} + +/// orcompare.xml, the three-way case: a chain of three ors. +#[test] +fn orcompare_three_terms() { + case(&all("control"), "orcompare3", |c| { + c.at_least("if (", 3).times("sink3(", 3).times("sink4(", 1); + }); +} + +/// ccmp.xml: two comparisons joined by a short-circuit and, which AArch64 +/// compiles into one `ccmp` against the flags the first left behind. +/// +/// Ghidra's assertion is that the pair reads as `ptr[1] == 0x3c && val < 10` +/// with no `SBORROW` left over. Ours keeps them as nested tests, and the +/// property is that both comparisons survive with their operands intact: the +/// load of the second element compared against 60, and the bound on the +/// argument. +#[test] +fn ccmp() { + case(&all("control"), "andcompare", |c| { + c.has("+ 4") + .has("== 60") + .times("sink5(", 1) + .times("sink6(", 2) + .lacks("__borrow"); + }); +} + +/// multiret.xml: values of one, two and eight bytes flowing into one return. +/// This used to put Ghidra into an infinite loop, so the property is that it +/// terminates with the arithmetic folded: 'a' plus 1001 plus the argument. +#[test] +fn multiret() { + case(&all("control"), "multiret", |c| { + c.has("1098"); + }); +} + +/// condconst.xml: a value known to be constant on the path that reaches its +/// use. The arm that assigns 10 has to say 10 and not name a variable. +#[test] +fn condconst() { + case(&all("control"), "condconst", |c| { + c.has("= 10").has("source()"); + }); +} + +/// condmulti.xml: a conditional constant reaching a join along two paths. +#[test] +fn condmulti() { + case(&all("control"), "condmulti", |c| { + c.has("sink1(").has("sink2(").has("sink3(").has("10"); + }); +} + +// --- arithmetic ------------------------------------------------------------- + +/// divopt.xml, the easy end: division by a power of two, which is a shift and +/// needs no reciprocal to undo. +#[test] +fn divopt_power_of_two() { + case(&all("arith"), "divu8", |c| { + c.has(">> 3"); + }); +} + +/// convert.xml: constants of several widths reaching the calls that take them. +/// +/// Ghidra's assertion is about how each one prints; ours is the step before it, +/// that the right constant arrives at the right call after the compiler has +/// spread the loads out. +#[test] +fn convert_constants_reach_their_calls() { + case(&all("arith"), "convert", |c| { + c.has("sink1(256)") + .has("sink3(1000)") + .has("sink5(0xfa56ea00)") + .has("sink6(97)"); + }); +} + +/// sbyte.xml: a signed byte compared against constants. Ghidra's point is that +/// the decompiler must not reach for `char` just because the load is one byte +/// wide; ours is the same, plus that both equality tests keep their value, with +/// -9 as the byte 0xf7 the machine actually compares. +#[test] +fn sbyte() { + case(&all("arith"), "sbyte", |c| { + c.has("247").has("== 10").lacks("char "); + for n in 1..=4 { + c.has(&format!("sink{n}(")); + } + }); +} + +/// promotecompare.xml: a byte widened before it is compared. The comparison is +/// the point and the widening is noise, so the constant has to survive as one +/// number rather than as a masked subtraction. +/// +/// AArch64 only: on x86-64 the `setb` that carries the answer out is dropped, +/// which `setcc_result_is_dropped` records. +#[test] +fn promotecompare() { + case(&arch("arith", "a64"), "promotecompare", |c| { + c.has("57").lacks("char "); + }); +} + +/// lzcount.xml: a zero test that a compiler renders without a branch. Ghidra +/// wants `return param_1 == 3` out of PowerPC's `cntlzw`; the portable half of +/// that is that a flag turned into a value stays a comparison. +#[test] +fn lzcount() { + case(&arch("arith", "a64"), "iszero", |c| { + c.has("== 0"); + }); +} + +/// statuscmp.xml and boolless.xml in spirit: seven comparisons whose results +/// are values rather than branches. The signed ones and the unsigned one have +/// to stay distinguishable, which is what a flag-word shift would lose. +#[test] +fn statuscmp() { + case(&arch("arith", "a64"), "cmpvalue", |c| { + c.has("(int64_t)").has("arg0 != arg1").has("arg1 > arg0"); + }); +} + +/// statuscmp.xml, the branch half: the signed orderings that come out right. +/// `>` and `>=` are negated into a constant-first form that says the same +/// thing; `<` and `<=` are not, which `signed_compare_is_inverted` records. +#[test] +fn signed_compare_greater() { + case(&all("arith"), "cmp_gt", |c| { + c.has_any(&["6 <= ", ">= 6"]).times("sink1(", 1); + }); + case(&all("arith"), "cmp_ge", |c| { + c.has_any(&["5 <= ", ">= 5"]).times("sink1(", 1); + }); +} + +/// nan.xml: a self-comparison is a NaN test and has to survive as one. +/// +/// AArch64 only, for the same reason as `promotecompare`. +#[test] +fn nan() { + case(&arch("arith", "a64"), "nanfn", |c| { + c.has("__isnan"); + }); +} + +/// floatconv.xml: an integer converted to floating point, scaled, and returned. +/// The subtraction has to stay on the integer side of the conversion. +#[test] +fn floatconv() { + case(&all("arith"), "i2d", |c| { + c.has("(double)").has_any(&["- 16", "+ -16"]); + }); +} + +/// mixfloatint.xml: a prototype that alternates integer and floating point +/// arguments. Ghidra recovers the source order; we recover the register +/// classes, so the property is that four integer arguments and two floating +/// point ones are found and neither kind is read as the other. +#[test] +fn mixfloatint() { + case(&all("arith"), "dldlll", |c| { + c.has("arg3").has("farg0").has("farg1").lacks("farg2"); + }); +} + +// --- memory shapes ---------------------------------------------------------- + +/// twodim.xml: a two dimensional global array read and written at one row. +/// +/// Ghidra prints `myarray[globindex][valin]` and asserts there is no ` * ` left. +/// Without a type for the global we cannot name the dimensions, so the property +/// is the one that survives: the row address is computed once from the global +/// index and both the load and the store are taken from it. +#[test] +fn twodim() { + case(&all("memory"), "twodim", |c| { + c.has("+ 10").at_least("<< 2", 1); + }); +} + +/// threedim.xml: the same with one more dimension. +#[test] +fn threedim() { + case(&all("memory"), "threedim", |c| { + c.has("+ 3").at_least("<< 2", 1); + }); +} + +/// nestedoffset.xml: an array inside a structure, indexed through a pointer, so +/// the field's offset lands inside the index expression. The decompiler has to +/// distribute the element scale to see it: the index stays `a + b` and the +/// twelve-byte offset stays a separate addend. +#[test] +fn nestedoffset() { + case(&all("memory"), "nestedoffset", |c| { + c.has("+ 12").has("<< 2"); + }); +} + +/// offsetarray.xml: an array indexed with a negative adjustment, which folds +/// the field offset and the index offset into one constant in the machine code. +/// The index must come back scaled and on its own, not merged with the +/// structure's first field. +#[test] +fn offsetarray() { + case(&all("memory"), "offsetarray", |c| { + c.has("(uint32_t)arg0 << 2"); + }); +} + +/// wayoffarray.xml: the same, with an adjustment big enough to pull the base +/// reference out of the mapped structure entirely. +#[test] +fn wayoffarray() { + case(&all("memory"), "wayoffarray", |c| { + c.has("(uint32_t)arg0 << 2"); + }); +} + +/// stackcorner.xml: a local array written through a computed index. The frame +/// has to survive an index the compiler masks rather than bounds. +/// +/// O1 only: at O2 the initializing loop is unrolled and there is no loop left. +#[test] +fn stackcorner() { + case(&opt("memory", "O1"), "arraybottom", |c| { + c.has("while (").at_least("& 15", 2).has("100"); + }); +} + +/// dupptr.xml: two accesses that share an intermediate pointer. The base has to +/// be pushed past the shared part, so the read and the write go through one +/// computed address and the arithmetic between them is not repeated. +#[test] +fn dupptr() { + case(&all("memory"), "dupptr", |c| { + c.has(">> 3").has("& 15").has("+ 37"); + }); +} + +/// pointercmp.xml: a pointer walked one byte at a time until it reaches a +/// computed bound. The increment and the bound both have to be pointers. +#[test] +fn pointercmp() { + case(&all("memory"), "pointercmp", |c| { + c.has("while (").has("+ 1"); + }); +} + +/// offcut.xml: three references into one global, two of them into its interior. +/// Each has to keep its own address: the field at offset 4 of the inner +/// structure and the array element twenty bytes past it are different places. +#[test] +fn offcut() { + case(&all("memory"), "offcut", |c| { + c.times("sink1(", 1).times("sink2(", 1).times("sink3(", 1); + }); +} + +/// condconst.xml, the global half: four constants stored to four globals. +#[test] +fn globalconst() { + case(&all("memory"), "globalconst", |c| { + c.times("= 10", 2).has("= 0"); + }); +} + +/// stackstring.xml: a string a compiler stores as immediates rather than as +/// data, which is what makes it invisible to a string scan. +/// +/// Ghidra reassembles the immediates into `builtin_strncpy(buf, "hello world")`. +/// We do not, so the property is that the packed characters reach the output as +/// the one store the machine makes, which is what such a pass would read. +/// +/// O1 and the x86-64 O2 build: AArch64 at O2 spreads the same bytes over +/// registers the store no longer names. +#[test] +fn stackstring() { + let mut builds = opt("memory", "O1"); + builds.push("dt-memory.x64.O2".to_string()); + case(&builds, "stackstring", |c| { + c.has("0x6f77206f6c6c6568"); + }); +} + +/// heapstring.xml: the same store pattern, to memory the function does not own. +/// +/// x86-64 only: the AArch64 build ends the run with a `strb` of a register, +/// whose value `byte_register_operand_reads_a_flag` records as lost. +#[test] +fn heapstring() { + case(&arch("memory", "x64"), "heapstring", |c| { + c.has("0x3a6567617373654d"); + }); +} + +/// varcross.xml: two stores to one address with a call between them. Neither +/// may be merged into the other, because the call can read what is there. +#[test] +fn varcross() { + case(&all("memory"), "varcross", |c| { + c.has("source()").has("sink1(").at_least("0x", 2); + }); +} + +/// revisit.xml: two accesses of different widths to one global, where the +/// narrower one forces the wider one's definition to be reconsidered. The +/// two-byte write has to stay two bytes wide. +#[test] +fn revisit() { + case(&all("memory"), "revisit", |c| { + c.has("(uint16_t)").has("110"); + }); +} + +/// wraprange.xml: a stack range written from high addresses to low ones, so the +/// frame offsets run through zero. The four writes and four reads have to fold +/// to the sum they add up to, which is three times the first argument plus the +/// second. +/// +/// AArch64 only: the x86-64 build loses one of the four slots. +#[test] +fn wraprange() { + case(&arch("memory", "a64"), "wraprange", |c| { + c.has("<< 1"); + }); +} + +// --- calls and prototypes --------------------------------------------------- + +/// indproto.xml: two indirect calls through two fields of one structure. Each +/// has to keep the field it came from, or both collapse onto the same target. +#[test] +fn indproto() { + case(&all("calls"), "indproto", |c| { + c.times("__callind", 2).has("field_0").has("field_8"); + }); +} + +/// retspecial.xml: a structure too big for registers, returned through the +/// hidden pointer the caller supplies. The three fields have to be written +/// through that pointer at their own offsets. +#[test] +fn retspecial() { + case(&all("calls"), "returnbig", |c| { + c.has_any(&["+ 8", "field_8"]) + .has_any(&["+ 16", "field_10"]) + .at_least(" = ", 3); + }); +} + +/// stackspill.xml: more integer arguments than there are argument registers, so +/// the tail of the list arrives on the stack and has to be found there. +#[test] +fn stackspill() { + case(&all("calls"), "manyargs", |c| { + c.has("arg_s"); + }); +} + +/// stackcorner.xml, the parameter half: a function whose last argument is past +/// the register set, beside six that are not. +#[test] +fn paramnodirect() { + case(&all("calls"), "paramnodirect", |c| { + c.has("sink(").has("arg5").has_any(&["arg6", "arg_s"]); + }); +} + +/// inline.xml: a callee small enough that a decompiler is tempted to inline it, +/// called twice with different arguments. Both calls have to stay calls. +/// +/// Three occurrences, not two: the caller's own name ends in the callee's, so +/// its signature matches as well as its two calls. The second of those is a +/// tail call, and it was missing from the output until a branch out of the +/// function was recognized as the call it is. +#[test] +fn inline() { + case(&all("calls"), "callsadd50", |c| { + c.times("add50(", 3); + }); +} + +// --- what our decompiler gets wrong ----------------------------------------- +// +// Each of these is a Ghidra datatest that fails here, kept and named after the +// defect rather than deleted. They are the list of what to fix. + +/// switchind.xml, switchmask.xml, switchloop.xml, switchmulti.xml, +/// switchhide.xml, ifswitch.xml: a jump table is a `switch`. +/// +/// `decompile_program` hands the structurer its tables keyed by the block the +/// indirect branch ends, not by the branch's own address, so the lookup +/// matches and the arms come out as cases rather than as a page of labels. +/// +/// Only the x86-64 builds are asserted on: the AArch64 compiler turns this +/// same source into a ladder of compares with no table in it, so there is +/// nothing there for a switch to be recovered from. +/// +/// The out-of-range arm is the `else` of the compare that guards the table +/// rather than a `default:` label, because that is where the machine puts it: +/// the indirect branch itself has no successor outside the table. +#[test] +fn switchind() { + case(&arch("control", "x64"), "switchind", |c| { + c.has("switch (") + .at_least("case ", 10) + .has("0xffffffff") + .lacks("__indirect_branch"); + }); +} + +/// switchind.xml on x86-64: the table is not recovered at all. The function is +/// reported incomplete, so it does not reach the decompiler, and the arms are +/// never disassembled. AArch64 recovers the same source's table. +#[test] +fn switchind_x86_64() { + case(&arch("control", "x64"), "switchind", |c| { + c.at_least("0x", 1); + }); +} + +/// Nested counted loops: both loops survive, with the call inside the inner +/// one. +/// +/// The outer loop has no test at its top, so its header is a branch like any +/// other and both of its arms have to be structured. Following only the first +/// successor used to drop every block the other arm reached, which here is the +/// whole inner loop. +#[test] +fn nested_loops() { + case(&all("control"), "nested", |c| { + c.at_least("while (", 2).has("sink("); + }); +} + +/// forloop_varused.xml: a loop whose body is guarded by a test. The guarded +/// block and the call under it stay inside the loop, which is the same +/// property `nested_loops` pins seen from a second angle: the loop header's +/// branch is structured rather than half-followed. +#[test] +fn forloop_varused() { + case(&all("control"), "forloop_varused", |c| { + c.has("while (").has("& 3").has("sink("); + }); +} + +/// divopt.xml: division by a constant, which compilers turn into a multiply by +/// a magic reciprocal and a shift. Nothing turns it back, so `x / 81` reads as +/// `x * 0xca4587e7 >> 38`. Seventeen unsigned constants and seventeen signed +/// ones in the original; three of each here. +#[test] +fn divopt() { + for (f, by) in [ + ("divu81", "/ 81"), + ("divu99", "/ 99"), + ("divu125", "/ 125"), + ("divs81", "/ 81"), + ("divs99", "/ 99"), + ("divs125", "/ 125"), + ] { + case(&all("arith"), f, |c| { + c.has(by); + }); + } +} + +/// divopt.xml, the signed power-of-two case: `x / 8` on a signed value is a +/// rounding correction and a shift, and the correction comes out as a +/// `__borrow` of the value against zero rather than as a sign test. +#[test] +fn divopt_signed_power_of_two() { + case(&all("arith"), "divs8", |c| { + c.lacks("__borrow"); + }); +} + +/// modulo.xml and modulo2.xml: the remainder forms built on the same +/// reciprocal. `x - 10 * (x / 10)` has to come back as `x % 10`. +#[test] +fn modulo() { + for (f, by) in [ + ("modu10", "% 10"), + ("modu100", "% 100"), + ("mods3", "% 3"), + ("mods7", "% 7"), + ] { + case(&all("arith"), f, |c| { + c.has(by); + }); + } +} + +/// statuscmp.xml: all four signed relations reach the output meaning what the +/// branch tests. +/// +/// `x != y && y <= x` is `y < x`, and the rewrite that recognizes it used to +/// emit `x < y`, so every `<` and `<=` in a source program came out as its +/// complement. Nothing caught it: the output still compiled, and the +/// interpreter runs the flag algebra rather than this rewrite. +#[test] +fn signed_compare_keeps_its_relation() { + case(&all("arith"), "cmp_lt", |c| { + c.has_any(&["<= 4", "4 >= "]); + }); + case(&all("arith"), "cmp_le", |c| { + c.has_any(&["<= 5", "5 >= "]); + }); +} + +/// lzcount.xml and promotecompare.xml on x86-64: a comparison whose answer is +/// moved out by `setcc` into the low byte of a zeroed register loses the +/// `setcc`, so `return x == 0` decompiles to `return 0`. Four instructions in, +/// a constant out. +#[test] +fn setcc_result_is_dropped() { + case(&arch("arith", "x64"), "iszero", |c| { + c.has("== 0"); + }); + case(&arch("arith", "x64"), "promotecompare", |c| { + c.has("57"); + }); +} + +/// sbyte.xml, the ordering test: the two equality tests against the loaded byte +/// come out right, and the `>` against 0x61 reads an uninitialized `flag0` +/// instead of the byte. An eight-bit read of a register resolves to a flag +/// varnode rather than to the register's low byte, on both architectures. +/// +/// The same defect on the store side loses the value of an AArch64 `strb` of a +/// register, which is why `heapstring` is checked on x86-64 only. +#[test] +fn byte_register_operand_reads_a_flag() { + case(&all("arith"), "sbyte", |c| { + c.lacks("flag0"); + }); +} + +/// nan.xml on x86-64: neither the NaN test nor the ordinary comparison beside +/// it survives; both calls receive the constant zero. The comparison is an +/// `ucomisd` whose answer leaves through `setnp` and `setae`, so this is the +/// floating point face of `setcc_result_is_dropped`. +#[test] +fn nan_x86_64() { + case(&arch("arith", "x64"), "nanfn", |c| { + c.has("__isnan"); + }); +} + +/// floatprint.xml: a `float` constant prints as a decimal with three hundred +/// digits of mantissa, because the 32-bit pattern is widened as if it were a +/// double. Ghidra asks for `0.33333334`; we produce +/// `0.000000...00015460065587`. +#[test] +fn floatprint() { + case(&all("arith"), "i2f", |c| { + c.lacks("0.0000000000000000000000000000000"); + }); +} + +/// floatcast.xml: a `float` widened to `double`, computed on, and narrowed +/// back. +/// +/// The arguments are right now: they arrive as `float` and read as +/// `(double)farg0`. What is left is the other end. The function returns a +/// `float` and is declared returning `double`, so the result goes back through +/// `(uint64_t)(uint32_t)__bits32(...)` to fill the register it is declared to +/// fill. Fixing it needs the width of the value that reaches the return, which +/// the recovery does not have: a narrow write is a masked merge with the old +/// whole register, so the definition it finds is eight bytes wide whatever was +/// computed. +#[test] +#[ignore = "a float result is returned through its bit pattern: the return width is not recovered"] +fn floatcast() { + case(&all("arith"), "floatcast", |c| { + c.lacks("(uint32_t)__bits"); + }); +} + +/// convert.xml: a negative constant prints as an unsigned hexadecimal, so +/// `recv_signed(-512)` reads as `sink2(0xfffffe00)`. Ghidra decides the base +/// and the signedness from the parameter's declared type; we have no type for +/// the parameter, but the narrower fact that a 32-bit constant with the top bit +/// set is more often a small negative number than four billion is still +/// available and unused. +#[test] +fn convert() { + case(&all("arith"), "convert", |c| { + c.has("-512").has("-3000"); + }); +} + +/// deindirect.xml and indproto.xml, the argument half: an indirect call comes +/// out as `__callind(target)` with no arguments at all, so `f(b + 3)` loses the +/// `b + 3`. The two calls here also share a target that is a single global +/// holding one function, which Ghidra collapses to a direct call to it. +#[test] +#[ignore = "an indirect call is emitted without its arguments and is never devirtualized"] +fn deindirect() { + case(&all("calls"), "deindirect", |c| { + c.has("realfunc").has("+ 3").has("+ 5"); + }); +} + +/// retstruct.xml: a structure returned in two registers. The two fields are +/// packed into one 64-bit constant expression, `(x * 100) | 0x1e00000000`, so +/// the second field is a bit pattern rather than the value 30. +#[test] +#[ignore = "a structure returned in registers is packed into one integer"] +fn retstruct() { + case(&all("calls"), "retpair", |c| { + c.lacks("0x1e00000000"); + }); +} + +/// switchreturn.xml in the portable form it has here: a function whose last act +/// is a tail call says so. A branch whose target is no block of this function +/// is the callee returning on this function's behalf, and the emitter writes +/// it out as the call and the return it stands for. Nothing marks it in the +/// IR, and a branch is not otherwise a statement, so both used to vanish and +/// the body came out empty. +#[test] +fn tailcall() { + case(&all("calls"), "tailcall", |c| { + c.has("realfunc"); + }); +} + +// --- structuring covers the graph ------------------------------------------- + +/// Every reachable block of every function of every fixture reaches the output. +/// +/// Code that disappears between the control flow graph and the text is the +/// worst thing the structurer can do, because nothing downstream can tell: the +/// output still compiles, still reads as a function, and does less than the +/// function does. `nested_loops`, `forloop_varused` and `tailcall` above are +/// three shapes that lost blocks; this is the check that finds the fourth +/// rather than waiting for someone to notice it. +#[test] +fn no_block_is_lost() { + let Some(dir) = build_dir() else { return }; + let mut names: Vec = std::fs::read_dir(&dir) + .into_iter() + .flatten() + .flatten() + .filter(|e| e.file_type().is_ok_and(|t| t.is_file())) + .map(|e| e.file_name().to_string_lossy().into_owned()) + .collect(); + names.sort(); + + let mut lost = 0usize; + let mut where_from: Vec = Vec::new(); + let mut checked = 0usize; + for name in names { + let Some(p) = open(&name) else { continue }; + let targets: Vec<&e5r_analysis::Function> = p + .functions_by_address() + .filter(|f| f.is_complete()) + .collect(); + if targets.is_empty() { + continue; + } + for d in e5r_api::decompile_program(&p, &targets).functions { + checked += 1; + if d.lost > 0 { + lost += d.lost; + where_from.push(format!("{name} {}: {} block(s)", d.name, d.lost)); + } + } + } + assert!( + lost == 0, + "{lost} block(s) missing from the output of {checked} functions:\n{}", + where_from.join("\n") + ); +} diff --git a/crates/e5r-decomp/tests/quality.rs b/crates/e5r-decomp/tests/quality.rs new file mode 100644 index 0000000..1a3a391 --- /dev/null +++ b/crates/e5r-decomp/tests/quality.rs @@ -0,0 +1,142 @@ +//! The decompiler measured by whether a compiler accepts what it wrote. +//! +//! Reading decompiler output cannot tell you whether it is well formed. Handing +//! it to a C compiler can, and that is the gate here: every function recovered +//! from the fixtures is decompiled, the whole lot is written as one translation +//! unit, and `clang -c` has to accept it. A structuring bug that drops a brace +//! or an expression rebuilder that emits an unbalanced cast fails loudly. +//! +//! The second measure is the goto count. Structuring that gives up produces a +//! labelled goto, which is honest but is also the thing to improve, so the +//! density is held under a ceiling that only comes down. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_format::LoadOptions; + +/// Ceiling on the share of functions the structuring could not express without +/// a label. Only lowered. +/// +/// The share rather than the count: one function of forty nested loops can +/// need twenty labels, and averaging those over the corpus says more about +/// that one function than about the structuring. +const MAX_UNSTRUCTURED: f64 = 0.07; + +fn build_dir() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(build_dir()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// Decompile every recovered function into one translation unit, by exactly +/// the path the command line takes. +fn decompile_all(p: &Program) -> e5r_api::Unit { + let targets: Vec<&e5r_analysis::Function> = p + .functions_by_address() + .filter(|f| f.is_complete() && f.cfg.blocks.len() <= 300) + .collect(); + e5r_api::decompile_program(p, &targets) +} + +fn clang() -> Option { + for name in ["clang", "cc", "gcc"] { + if Command::new(name).arg("--version").output().is_ok() { + return Some(name.to_string()); + } + } + None +} + +/// Compile one translation unit and return what the compiler said. +fn compile(text: &str, tag: &str) -> Option<(bool, String)> { + let cc = clang()?; + let dir = std::env::temp_dir().join(format!("e5r-decomp-{tag}")); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("out.c"); + std::fs::write(&src, text).ok()?; + let output = Command::new(cc) + .args(["-c", "-w", "-ffreestanding", "-o"]) + .arg(dir.join("out.o")) + .arg(&src) + .output() + .ok()?; + Some(( + output.status.success(), + String::from_utf8_lossy(&output.stderr).into_owned(), + )) +} + +fn check(binary: &str) { + let Some(p) = open(binary) else { return }; + let unit = decompile_all(&p); + let count = unit.functions.len(); + if count == 0 { + return; + } + let Some((ok, errors)) = compile(&unit.text(), binary) else { + return; // no compiler here + }; + assert!( + ok, + "{binary}: the decompiled output of {count} functions does not compile:\n{}", + errors.lines().take(20).collect::>().join("\n") + ); + + let gotos = unit.gotos(); + let unstructured = unit.functions.iter().filter(|f| f.gotos > 0).count(); + let share = unstructured as f64 / count as f64; + assert!( + share <= MAX_UNSTRUCTURED, + "{binary}: {unstructured} of {count} functions needed a label \ + ({share:.2}), ceiling is {MAX_UNSTRUCTURED}; {gotos} gotos in total" + ); + println!( + "{binary}: {count} functions, {unstructured} needed a label ({share:.2}), \ + {gotos} gotos" + ); +} + +#[test] +fn aarch64_output_compiles() { + for opt in ["O0", "O1", "O2", "O3", "Os"] { + check(&format!("driver.a64.{opt}")); + } +} + +#[test] +fn x86_output_compiles() { + for opt in ["O0", "O1", "O2", "O3", "Os"] { + check(&format!("driver.x64.{opt}")); + } +} + +#[test] +fn a_stripped_binary_decompiles_too() { + check("hello.a64.O2.stripped"); +} + +/// The fixtures built with debug information, where the output is typed from +/// what the compiler recorded rather than from the calling convention. +#[test] +fn output_typed_from_debug_information_compiles() { + for name in [ + "wide.a64.O0.o", + "wide.a64.O1.o", + "wide.a64.O2.o", + "wide.x64.O0.o", + "wide.x64.O2.o", + "shapes.a64.O0.o", + "shapes.x64.O2.o", + "hello.a64.O0", + "hello.a64.O2", + ] { + check(name); + } +} diff --git a/crates/e5r-decomp/tests/roundtrip.rs b/crates/e5r-decomp/tests/roundtrip.rs new file mode 100644 index 0000000..d497fd1 --- /dev/null +++ b/crates/e5r-decomp/tests/roundtrip.rs @@ -0,0 +1,332 @@ +//! The decompiler measured by running what it wrote. +//! +//! Compiling the output proves it is well formed and nothing more. An inverted +//! comparison, a dropped operand, a shift by the wrong amount: all of those +//! compile. The only way to know that the C says what the machine says is to +//! run both on the same inputs and compare, so that is the gate here. +//! +//! The machine's side comes from the interpreter, which is itself measured +//! against what real hardware did with the same instructions, so nothing in +//! this file is the decompiler being asked to check itself. +//! +//! Only functions whose whole behaviour is their return value are eligible: no +//! calls, no memory, no helpers the C cannot honestly define. That is a small +//! part of a real program and it is the part where a silent arithmetic bug +//! hides, which is exactly what this is for. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_analysis::{Function, Options, Program, analyze}; +use e5r_api::emulate::{Setup, run}; +use e5r_format::LoadOptions; + +/// How many functions may still return something the machine does not. +/// +/// Zero, and it was a ratchet until it got here. A decompiler that produces a +/// wrong answer is worse than one that refuses, so nothing above zero was ever +/// going to be the end state; the number came down as the defects behind it +/// were found, and the gate failed both when it rose and when it fell without +/// being recorded, so that neither direction could pass unnoticed. +/// +/// Every function that can be compiled and run is now run, on every argument +/// vector, and agrees with the interpreter. Any future disagreement is a +/// regression and the failure message names the function and the arguments. +const MAX_DISAGREEING: usize = 0; + +/// Argument vectors every eligible function is run on. +/// +/// The edges first, because that is where a sign extension or an off-by-one in +/// a comparison shows, then a deterministic spread. +fn vectors(arity: usize) -> Vec> { + const EDGES: [u64; 12] = [ + 0, + 1, + 2, + 5, + 0x7f, + 0x80, + 0xffff_ffff, + 0x8000_0000, + 0x7fff_ffff, + u64::MAX, + 0x1_0000_0000, + 81, + ]; + let mut out: Vec> = EDGES.iter().map(|e| vec![*e; arity.max(1)]).collect(); + // A linear congruential sequence, so the corpus is the same on every run + // and a failure can be reproduced from the printed arguments alone. + let mut seed: u64 = 0x2545_f491_4f6c_dd1d; + for _ in 0..24 { + let mut v = Vec::with_capacity(arity); + for _ in 0..arity.max(1) { + seed = seed + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + v.push(seed >> 11); + } + out.push(v); + } + out +} + +/// Anything in a decompiled body that means its behaviour is not just its +/// return value, or that the C cannot be given an honest definition for. +/// +/// Every helper the emitter can reach is named with a leading `__`, so one +/// test covers them all and a helper added later cannot slip past. +const DISQUALIFYING: &[&str] = &["sub_", "__", "*(", "goto"]; + +fn build_dir() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(path: &Path) -> Option { + let data = std::fs::read(path).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +fn cc() -> Option { + for name in ["clang", "cc", "gcc"] { + if Command::new(name).arg("--version").output().is_ok() { + return Some(name.to_string()); + } + } + None +} + +/// How many parameters a decompiled signature or definition declares. +/// +/// Counted from the `argN` names rather than from the commas, because a +/// parameter's type can itself be a generated name containing `arg`. +/// +/// The ones the convention ran out of registers for are named `arg_s` +/// by where the caller left them, not by their position, so they are counted +/// rather than maximised over. They come after the register arguments in the +/// declaration, which is the order a caller passes them in. +fn arity(text: &str) -> usize { + let head = &text[..text.find(')').map(|i| i + 1).unwrap_or(text.len())]; + let b = head.as_bytes(); + let mut highest = None; + for (i, _) in head.match_indices("arg") { + if i > 0 && (b[i - 1].is_ascii_alphanumeric() || b[i - 1] == b'_') { + continue; + } + let digits: String = head[i + 3..] + .chars() + .take_while(char::is_ascii_digit) + .collect(); + if let Ok(n) = digits.parse::() { + highest = Some(highest.map_or(n, |h: usize| h.max(n))); + } + } + let on_stack = head + .match_indices("arg_s") + .filter(|(i, _)| *i == 0 || !(b[*i - 1].is_ascii_alphanumeric() || b[*i - 1] == b'_')); + highest.map_or(0, |h| h + 1) + on_stack.count() +} + +/// One function the gate can run: its C, its name, and what the interpreter +/// said it returns for each argument vector. +struct Candidate { + name: String, + text: String, + args: Vec>, + expected: Vec, +} + +/// Decompile one function and run it, if it is eligible. +fn candidate(p: &Program, f: &Function) -> Option { + let d = e5r_api::decompile_function(p, f); + if d.text.is_empty() || DISQUALIFYING.iter().any(|bad| d.text.contains(bad)) { + return None; + } + // A function whose C returns nothing has no answer to compare, and one + // that takes a pointer reaches memory the harness cannot set up. + // A floating point parameter arrives in a vector register, which the + // interpreter is not being asked to set up here. + if !d.signature.starts_with("uint64_t") + || d.signature.contains('*') + || d.signature.contains("farg") + { + return None; + } + let n = arity(&d.signature); + if n > 6 { + return None; + } + let args = vectors(n); + let mut expected = Vec::with_capacity(args.len()); + for a in &args { + let r = run( + p, + f, + &Setup { + arguments: a.clone(), + ..Setup::default() + }, + ); + if !r.clean() { + return None; + } + expected.push(r.result); + } + Some(Candidate { + name: d.name.clone(), + text: d.text, + args, + expected, + }) +} + +/// Write a program that calls each candidate on each vector and prints what it +/// returned, one line per call. +fn harness(cands: &[Candidate]) -> String { + let mut out = String::from("#include \n#include \n\n"); + for c in cands { + out.push_str(&c.text); + out.push('\n'); + } + out.push_str("int main(void) {\n"); + for (i, c) in cands.iter().enumerate() { + for (j, a) in c.args.iter().enumerate() { + let n = arity(&c.text); + let call: Vec = a + .iter() + .take(n) + .map(|v| format!("(uint64_t){v}ULL")) + .collect(); + out.push_str(&format!( + " printf(\"{i} {j} %llu\\n\", (unsigned long long){}({}));\n", + c.name, + call.join(", ") + )); + } + } + out.push_str(" return 0;\n}\n"); + out +} + +/// Compile and run the harness, returning `(function, vector) -> result`. +fn execute(text: &str, tag: &str) -> Option> { + let cc = cc()?; + let dir = std::env::temp_dir().join(format!("e5r-roundtrip-{tag}-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("r.c"); + let bin = dir.join("r"); + std::fs::write(&src, text).ok()?; + let built = Command::new(&cc) + .arg("-O0") + .arg("-w") + .arg("-o") + .arg(&bin) + .arg(&src) + .output() + .ok()?; + assert!( + built.status.success(), + "the decompiled output did not compile:\n{}", + String::from_utf8_lossy(&built.stderr) + ); + let ran = Command::new(&bin).output().ok()?; + let _ = std::fs::remove_dir_all(&dir); + let mut out = Vec::new(); + for line in String::from_utf8_lossy(&ran.stdout).lines() { + let mut it = line.split_whitespace(); + let (Some(i), Some(j), Some(v)) = (it.next(), it.next(), it.next()) else { + continue; + }; + out.push((i.parse().ok()?, j.parse().ok()?, v.parse().ok()?)); + } + Some(out) +} + +/// Every pure function in the corpus computes, when compiled and run, what the +/// machine computes. +#[test] +// `<= 0` is `== 0`, and the comparison is written against the constant on +// purpose: it is the ratchet, and the assertion should still read as a ceiling +// the day one has to be allowed again. +#[allow(clippy::absurd_extreme_comparisons)] +fn decompiled_c_computes_what_the_machine_computes() { + let Some(dir) = build_dir() else { return }; + if cc().is_none() { + return; + } + let mut checked = 0usize; + let mut functions = 0usize; + let mut wrong: Vec = Vec::new(); + + let mut files: Vec = std::fs::read_dir(&dir) + .into_iter() + .flatten() + .flatten() + .map(|e| e.path()) + .filter(|p| { + p.file_name() + .map(|n| { + let n = n.to_string_lossy(); + n.starts_with("dt-") || n.starts_with("driver.") + }) + .unwrap_or(false) + }) + .filter(|p| p.extension().is_none_or(|e| e != "c" && e != "out")) + .collect(); + files.sort(); + + for path in &files { + let Some(p) = open(path) else { continue }; + let cands: Vec = p + .functions_by_address() + .filter(|f| f.is_complete()) + .filter_map(|f| candidate(&p, f)) + .collect(); + if cands.is_empty() { + continue; + } + functions += cands.len(); + let tag = path.file_name().unwrap_or_default().to_string_lossy(); + let Some(results) = execute(&harness(&cands), &tag) else { + continue; + }; + for (i, j, got) in results { + let Some(c) = cands.get(i) else { continue }; + let Some(want) = c.expected.get(j) else { + continue; + }; + checked += 1; + if got != *want { + wrong.push(format!( + "{tag} {}({:?}): the machine returns {want:#x}, the C returns {got:#x}", + c.name, c.args[j] + )); + } + } + } + + assert!( + functions > 20, + "only {functions} function(s) were eligible; the gate is not measuring anything" + ); + let names: std::collections::BTreeSet = wrong + .iter() + .filter_map(|l| l.split_once('(')) + .map(|(head, _)| head.to_string()) + .collect(); + assert!( + names.len() <= MAX_DISAGREEING, + "{} function(s) return something the machine does not, against a ceiling of \ + {MAX_DISAGREEING}. {} of {checked} call(s) over {functions} function(s).\nfirst 20:\n{}", + names.len(), + wrong.len(), + wrong + .iter() + .take(20) + .cloned() + .collect::>() + .join("\n") + ); + eprintln!("{checked} call(s) over {functions} function(s) agree"); +} diff --git a/crates/e5r-diff/Cargo.toml b/crates/e5r-diff/Cargo.toml new file mode 100644 index 0000000..3336e96 --- /dev/null +++ b/crates/e5r-diff/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "e5r-diff" +description = "e5r — matching functions across two builds." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +e5r-core = { workspace = true } +e5r-arch = { workspace = true } +e5r-analysis = { workspace = true } +e5r-db = { workspace = true } +serde = { workspace = true } + +[dev-dependencies] +e5r-format = { workspace = true } diff --git a/crates/e5r-diff/src/align.rs b/crates/e5r-diff/src/align.rs new file mode 100644 index 0000000..2a3dc37 --- /dev/null +++ b/crates/e5r-diff/src/align.rs @@ -0,0 +1,647 @@ +//! Diffing two instruction streams, so the answer is the line that changed. +//! +//! Function matching says *which* function a patch touched. This says which +//! instruction, which is the question a patch diff is actually asking. +//! +//! The thing that makes this hard is that inserting one instruction shifts +//! every address after it and changes every relative displacement, so a naive +//! comparison of two streams reports the whole tail of the function as +//! changed. The way out is to align on what does not move. That is exactly +//! what [`e5r_db::anchor::shape_token`] computes: everything about an +//! instruction except the addresses it names. +//! +//! Alignment is patience diff over those tokens. Tokens that occur exactly +//! once on each side are unambiguous correspondences, the longest increasing +//! subsequence of them fixes an order-preserving skeleton, and the gaps +//! between them are aligned the same way recursively. Where a gap has no +//! unique token left it falls back to a bounded longest-common-subsequence, +//! and a gap too large for that is reported as a wholesale replacement rather +//! than aligned badly. +//! +//! An aligned pair whose tokens agree still has to be classified, because the +//! tokens deliberately left the addresses out: +//! +//! - the addresses agree, so nothing happened; +//! - they differ, and the difference is exactly what the alignment says the +//! code moved by, so the instruction did not change, its target was pushed +//! along by an edit before it ([`EditKind::Displaced`]); +//! - they differ and nothing available accounts for it, which is a real +//! retarget and is reported as one rather than folded into either bucket. + +use std::collections::{BTreeMap, HashMap}; + +use e5r_analysis::Program; +use e5r_arch::Insn; +use e5r_core::Addr; +use e5r_db::anchor::{addresses, shape_token}; +use serde::Serialize; + +/// What happened at one position in the stream. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum EditKind { + /// The new build has an instruction here the old one does not. + Insert, + /// The old build has an instruction here the new one does not. + Delete, + /// Both builds have an instruction here and they are different. + Replace, + /// The same instruction, naming an address that moved by exactly as much + /// as the code it points at moved. Not a change: it is the shadow of a + /// change somewhere before it. + Displaced, + /// The same instruction, naming a different address, where nothing in + /// either build accounts for the difference as a displacement. A claim + /// about the program rather than about the layout. + Retargeted, +} + +impl EditKind { + /// The word shown in output. + pub fn as_str(self) -> &'static str { + match self { + EditKind::Insert => "insert", + EditKind::Delete => "delete", + EditKind::Replace => "replace", + EditKind::Displaced => "displaced", + EditKind::Retargeted => "retargeted", + } + } + + /// True when this is a change to the program rather than to the layout. + pub fn is_change(self) -> bool { + !matches!(self, EditKind::Displaced) + } +} + +/// One instruction-level edit. +#[derive(Debug, Clone, Serialize)] +pub struct Edit { + /// What happened. + pub kind: EditKind, + /// Where it was in the old build, absent for an insertion. + pub old: Option, + /// Where it is in the new build, absent for a deletion. + pub new: Option, + /// The old instruction as a listing shows it, empty for an insertion. + pub old_text: String, + /// The new instruction, empty for a deletion. + pub new_text: String, +} + +/// What changed inside one matched pair of functions. +#[derive(Debug, Clone, Serialize)] +pub struct FunctionDiff { + /// Entry in the old build. + pub old: Addr, + /// Entry in the new build. + pub new: Addr, + /// The name to show. + pub name: String, + /// The edits, in stream order. Includes the displaced ones, which are + /// evidence that the alignment held rather than findings in themselves. + pub edits: Vec, + /// Instructions that aligned and are identical down to their addresses. + pub same: u32, + /// Instructions in the old build. + pub old_insns: u32, + /// Instructions in the new build. + pub new_insns: u32, + /// True when a gap was too large to align and was reported as a wholesale + /// replacement. The edit list is then a bound on the change rather than + /// the change itself, and saying so is cheaper than being believed. + pub truncated: bool, +} + +impl FunctionDiff { + /// The edits that are changes, which is what a report shows. + pub fn changes(&self) -> impl Iterator { + self.edits.iter().filter(|e| e.kind.is_change()) + } + + /// How many edits are changes. + pub fn change_count(&self) -> usize { + self.changes().count() + } + + /// How many instructions moved without changing. + pub fn displaced_count(&self) -> usize { + self.edits + .iter() + .filter(|e| e.kind == EditKind::Displaced) + .count() + } + + /// True when anything at all changed. + pub fn changed(&self) -> bool { + self.edits.iter().any(|e| e.kind.is_change()) + } +} + +/// Diff the instructions of one matched pair. +/// +/// `functions` maps old function entries to new ones, which is what lets a +/// call whose callee moved be told apart from a call that now goes somewhere +/// else. [`crate::Diff`] supplies one through [`mapping`]; an empty map is +/// valid and only costs precision, since an unexplained target is reported as +/// retargeted rather than guessed at. +pub fn compare_function( + old: &Program, + old_entry: Addr, + new: &Program, + new_entry: Addr, + functions: &BTreeMap, +) -> Option { + let fa = old.function(old_entry)?; + let fb = new.function(new_entry)?; + let ia = old.instructions(fa); + let ib = new.instructions(fb); + let ta: Vec = ia.iter().map(shape_token).collect(); + let tb: Vec = ib.iter().map(shape_token).collect(); + + let (steps, truncated) = align(&ta, &tb); + + // Where each old instruction ended up, so a branch target inside the + // function can be followed through the alignment. + let mut moved: HashMap = HashMap::with_capacity(steps.len()); + for s in &steps { + if let Step::Pair(i, j) = *s { + moved.insert(ia[i].addr, ib[j].addr); + } + } + + let name = { + let n = fa.display_name(); + if n.starts_with("sub_") { + fb.display_name() + } else { + n + } + }; + let mut out = FunctionDiff { + old: old_entry, + new: new_entry, + name, + edits: Vec::new(), + same: 0, + old_insns: ia.len() as u32, + new_insns: ib.len() as u32, + truncated, + }; + + let render = |p: &Program, i: &Insn| e5r_arch::format(&p.object.arch, i, false); + + // Unpaired steps come out of the alignment as separate deletions and + // insertions. A run of them next to each other is what a person reads as + // one modification, so the run is paired up positionally and only the + // overhang stays a pure insertion or deletion. + let mut group_old: Vec = Vec::new(); + let mut group_new: Vec = Vec::new(); + let flush = |out: &mut FunctionDiff, go: &mut Vec, gn: &mut Vec| { + for n in 0..go.len().max(gn.len()) { + match (go.get(n), gn.get(n)) { + (Some(i), Some(j)) => { + let (a, b) = (&ia[*i], &ib[*j]); + // The alignment declined to pair these, but if the tokens + // agree after all then only the addresses differ and the + // honest answer is the same one a pair would get. + let kind = if ta[*i] == tb[*j] { + match classify(a, b, &moved, functions) { + Verdict::Same => { + out.same += 1; + continue; + } + Verdict::Displaced => EditKind::Displaced, + Verdict::Retargeted => EditKind::Retargeted, + } + } else { + EditKind::Replace + }; + out.edits.push(Edit { + kind, + old: Some(a.addr), + new: Some(b.addr), + old_text: render(old, a), + new_text: render(new, b), + }); + } + (Some(i), None) => out.edits.push(Edit { + kind: EditKind::Delete, + old: Some(ia[*i].addr), + new: None, + old_text: render(old, &ia[*i]), + new_text: String::new(), + }), + (None, Some(j)) => out.edits.push(Edit { + kind: EditKind::Insert, + old: None, + new: Some(ib[*j].addr), + old_text: String::new(), + new_text: render(new, &ib[*j]), + }), + (None, None) => {} + } + } + go.clear(); + gn.clear(); + }; + + for s in steps { + match s { + Step::Old(i) => group_old.push(i), + Step::New(j) => group_new.push(j), + Step::Pair(i, j) => { + flush(&mut out, &mut group_old, &mut group_new); + let (a, b) = (&ia[i], &ib[j]); + let kind = match classify(a, b, &moved, functions) { + Verdict::Same => { + out.same += 1; + continue; + } + Verdict::Displaced => EditKind::Displaced, + Verdict::Retargeted => EditKind::Retargeted, + }; + out.edits.push(Edit { + kind, + old: Some(a.addr), + new: Some(b.addr), + old_text: render(old, a), + new_text: render(new, b), + }); + } + } + } + flush(&mut out, &mut group_old, &mut group_new); + + Some(out) +} + +/// The old-to-new function map a whole-program diff already worked out. +pub fn mapping(d: &crate::Diff) -> BTreeMap { + d.matched.iter().map(|m| (m.old, m.new)).collect() +} + +/// Instruction-level diffs for every pair the whole-program diff calls changed. +pub fn detail(old: &Program, new: &Program, d: &crate::Diff) -> Vec { + let map = mapping(d); + d.changed() + .filter_map(|m| compare_function(old, m.old, new, m.new, &map)) + .collect() +} + +/// What an aligned pair with equal tokens turned out to be. +enum Verdict { + Same, + Displaced, + Retargeted, +} + +fn classify( + a: &Insn, + b: &Insn, + moved: &HashMap, + functions: &BTreeMap, +) -> Verdict { + let (aa, ba) = (addresses(a), addresses(b)); + // Equal tokens guarantee equal length; a mismatch would mean the token + // folded something the address list does not depend on, so refuse to + // guess rather than compare ragged lists. + if aa.len() != ba.len() { + return Verdict::Retargeted; + } + let mut shifted = false; + for (x, y) in aa.iter().zip(&ba) { + if x == y { + continue; + } + // Inside the function the alignment itself says where the target + // went; outside it, the function matcher does. + let explained = moved.get(x) == Some(y) || functions.get(x) == Some(y); + if !explained { + return Verdict::Retargeted; + } + shifted = true; + } + if shifted { + Verdict::Displaced + } else { + Verdict::Same + } +} + +/// One step of an alignment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Step { + /// Old index and new index correspond. + Pair(usize, usize), + /// The old build has this index and the new one has nothing for it. + Old(usize), + /// The new build has this index and the old one has nothing for it. + New(usize), +} + +/// How deep the patience recursion goes before it stops splitting. +/// +/// Each level strips at least one common instruction or one anchor, so a +/// bound here is what keeps the work linear in the worst case rather than +/// quadratic in the number of anchors. +const MAX_DEPTH: u32 = 32; + +/// The largest rectangle the fallback will fill in, in cells. +/// +/// A million cells is four megabytes of table and a few milliseconds. Beyond +/// it the region is reported as replaced wholesale, which is a worse answer +/// but a bounded one. +const MAX_AREA: usize = 1 << 20; + +/// Align two token streams. The flag says whether any region was too large to +/// align and had to be reported as a wholesale replacement. +fn align(a: &[u64], b: &[u64]) -> (Vec, bool) { + let mut out = Vec::with_capacity(a.len() + b.len()); + let mut truncated = false; + patience(a, b, 0, a.len(), 0, b.len(), 0, &mut out, &mut truncated); + (out, truncated) +} + +#[allow(clippy::too_many_arguments)] +fn patience( + a: &[u64], + b: &[u64], + mut alo: usize, + mut ahi: usize, + mut blo: usize, + mut bhi: usize, + depth: u32, + out: &mut Vec, + truncated: &mut bool, +) { + // Common ends first. They are the cheapest correspondences there are and + // stripping them is what keeps the recursion shallow on a small edit. + while alo < ahi && blo < bhi && a[alo] == b[blo] { + out.push(Step::Pair(alo, blo)); + alo += 1; + blo += 1; + } + let mut suffix = 0usize; + while alo < ahi && blo < bhi && a[ahi - 1] == b[bhi - 1] { + ahi -= 1; + bhi -= 1; + suffix += 1; + } + + if alo == ahi || blo == bhi { + out.extend((alo..ahi).map(Step::Old)); + out.extend((blo..bhi).map(Step::New)); + } else { + let anchors = if depth < MAX_DEPTH { + unique_common(a, b, alo, ahi, blo, bhi) + } else { + Vec::new() + }; + if anchors.is_empty() { + fallback(a, b, alo, ahi, blo, bhi, out, truncated); + } else { + let (mut pa, mut pb) = (alo, blo); + for (ia, ib) in anchors { + patience(a, b, pa, ia, pb, ib, depth + 1, out, truncated); + out.push(Step::Pair(ia, ib)); + pa = ia + 1; + pb = ib + 1; + } + patience(a, b, pa, ahi, pb, bhi, depth + 1, out, truncated); + } + } + + for k in 0..suffix { + out.push(Step::Pair(ahi + k, bhi + k)); + } +} + +/// Tokens that occur exactly once on each side, paired and put in an order +/// both streams agree on. +/// +/// A token appearing once in each stream is the only kind of correspondence +/// that needs no further evidence: there is nothing else it could be. The +/// longest increasing subsequence then throws away the pairs that would ask +/// for a reordering, because a diff that crosses itself is not a diff. +fn unique_common( + a: &[u64], + b: &[u64], + alo: usize, + ahi: usize, + blo: usize, + bhi: usize, +) -> Vec<(usize, usize)> { + let mut counts: HashMap = HashMap::with_capacity(ahi - alo); + for (i, t) in a[alo..ahi].iter().enumerate() { + let e = counts.entry(*t).or_insert((0, 0, 0, 0)); + e.0 += 1; + e.2 = alo + i; + } + for (j, t) in b[blo..bhi].iter().enumerate() { + let Some(e) = counts.get_mut(t) else { + continue; + }; + e.1 += 1; + e.3 = blo + j; + } + let mut pairs: Vec<(usize, usize)> = counts + .values() + .filter(|(ca, cb, _, _)| *ca == 1 && *cb == 1) + .map(|(_, _, i, j)| (*i, *j)) + .collect(); + pairs.sort_unstable(); + longest_increasing(&pairs) +} + +/// The longest subsequence of pairs whose second components also increase. +fn longest_increasing(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> { + if pairs.is_empty() { + return Vec::new(); + } + // Patience sorting: `tails[k]` is the index into `pairs` of the smallest + // possible tail of an increasing run of length k + 1. + let mut tails: Vec = Vec::new(); + let mut back: Vec> = vec![None; pairs.len()]; + for (n, p) in pairs.iter().enumerate() { + let at = tails.partition_point(|t| pairs[*t].1 < p.1); + back[n] = (at > 0).then(|| tails[at - 1]); + if at == tails.len() { + tails.push(n); + } else { + tails[at] = n; + } + } + let mut chain = Vec::with_capacity(tails.len()); + let mut cur = tails.last().copied(); + while let Some(n) = cur { + chain.push(pairs[n]); + cur = back[n]; + } + chain.reverse(); + chain +} + +/// A region with no unique token left in it. +#[allow(clippy::too_many_arguments)] +fn fallback( + a: &[u64], + b: &[u64], + alo: usize, + ahi: usize, + blo: usize, + bhi: usize, + out: &mut Vec, + truncated: &mut bool, +) { + let (n, m) = (ahi - alo, bhi - blo); + if n.saturating_mul(m) > MAX_AREA { + *truncated = true; + out.extend((alo..ahi).map(Step::Old)); + out.extend((blo..bhi).map(Step::New)); + return; + } + // Longest common subsequence, which is the right answer here and is only + // affordable because the region is bounded. + let mut len = vec![0u32; (n + 1) * (m + 1)]; + for i in (0..n).rev() { + for j in (0..m).rev() { + len[i * (m + 1) + j] = if a[alo + i] == b[blo + j] { + len[(i + 1) * (m + 1) + j + 1] + 1 + } else { + len[(i + 1) * (m + 1) + j].max(len[i * (m + 1) + j + 1]) + }; + } + } + let (mut i, mut j) = (0, 0); + while i < n && j < m { + if a[alo + i] == b[blo + j] { + out.push(Step::Pair(alo + i, blo + j)); + i += 1; + j += 1; + } else if len[(i + 1) * (m + 1) + j] >= len[i * (m + 1) + j + 1] { + out.push(Step::Old(alo + i)); + i += 1; + } else { + out.push(Step::New(blo + j)); + j += 1; + } + } + out.extend((alo + i..ahi).map(Step::Old)); + out.extend((blo + j..bhi).map(Step::New)); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn steps(a: &[u64], b: &[u64]) -> Vec { + let (s, truncated) = align(a, b); + assert!(!truncated); + // An alignment must consume each side exactly once, in order. + let mut ia: Vec = Vec::new(); + let mut ib: Vec = Vec::new(); + for st in &s { + match *st { + Step::Pair(i, j) => { + ia.push(i); + ib.push(j); + } + Step::Old(i) => ia.push(i), + Step::New(j) => ib.push(j), + } + } + assert_eq!(ia, (0..a.len()).collect::>(), "old side {s:?}"); + assert_eq!(ib, (0..b.len()).collect::>(), "new side {s:?}"); + s + } + + #[test] + fn identical_streams_align_completely() { + let a = [1, 2, 3, 4, 5]; + assert!(steps(&a, &a).iter().all(|s| matches!(s, Step::Pair(..)))); + } + + #[test] + fn an_insertion_is_one_step_and_the_rest_still_lines_up() { + let a = [1u64, 2, 3, 4, 5]; + let b = [1u64, 2, 9, 3, 4, 5]; + let s = steps(&a, &b); + assert_eq!( + s.iter().filter(|x| matches!(x, Step::New(_))).count(), + 1, + "{s:?}" + ); + assert_eq!(s.iter().filter(|x| matches!(x, Step::Old(_))).count(), 0); + } + + #[test] + fn a_deletion_is_one_step() { + let a = [1u64, 2, 9, 3, 4, 5]; + let b = [1u64, 2, 3, 4, 5]; + let s = steps(&a, &b); + assert_eq!(s.iter().filter(|x| matches!(x, Step::Old(_))).count(), 1); + assert_eq!(s.iter().filter(|x| matches!(x, Step::New(_))).count(), 0); + } + + #[test] + fn a_substitution_pairs_one_against_one() { + let a = [1u64, 2, 3, 4, 5]; + let b = [1u64, 2, 9, 4, 5]; + let s = steps(&a, &b); + assert_eq!(s.iter().filter(|x| matches!(x, Step::Old(_))).count(), 1); + assert_eq!(s.iter().filter(|x| matches!(x, Step::New(_))).count(), 1); + } + + #[test] + fn a_repeated_token_does_not_stop_the_alignment() { + // Nothing here is unique, so the whole thing goes through the + // fallback. It still has to find the one inserted token. + let a = [7u64, 7, 7, 7, 7, 7]; + let b = [7u64, 7, 7, 8, 7, 7, 7]; + let s = steps(&a, &b); + assert_eq!(s.iter().filter(|x| matches!(x, Step::Old(_))).count(), 0); + assert_eq!(s.iter().filter(|x| matches!(x, Step::New(_))).count(), 1); + } + + #[test] + fn an_insertion_in_the_middle_of_a_long_run_keeps_the_tail() { + // The shape the whole thing exists for: everything after the edit + // moved, and none of it is a change. + let a: Vec = (0..500).collect(); + let mut b = a.clone(); + b.insert(200, 9999); + let s = steps(&a, &b); + assert_eq!(s.iter().filter(|x| matches!(x, Step::New(_))).count(), 1); + assert_eq!(s.iter().filter(|x| matches!(x, Step::Old(_))).count(), 0); + } + + #[test] + fn an_empty_side_is_all_insertion() { + let s = steps(&[], &[1, 2, 3]); + assert_eq!(s.len(), 3); + assert!(s.iter().all(|x| matches!(x, Step::New(_)))); + assert!(steps(&[], &[]).is_empty()); + } + + #[test] + fn a_reordering_is_reported_rather_than_crossed() { + // Two blocks swapped. A diff that crossed itself would claim both + // sides matched; this has to spend real edits on it. + let a = [1u64, 2, 3, 10, 11, 12]; + let b = [10u64, 11, 12, 1, 2, 3]; + let s = steps(&a, &b); + let edits = s.iter().filter(|x| !matches!(x, Step::Pair(..))).count(); + assert_eq!(edits, 6, "{s:?}"); + } + + #[test] + fn a_region_too_large_to_align_says_so() { + // Every token identical means no anchor anywhere, and a region this + // wide is past the fallback's budget. + let a: Vec = (0..1200).map(|n| 1 + (n % 2)).collect(); + let b: Vec = (0..1200).map(|n| 2 - (n % 2)).collect(); + let (_, truncated) = align(&a, &b); + assert!(truncated); + } +} diff --git a/crates/e5r-diff/src/lib.rs b/crates/e5r-diff/src/lib.rs new file mode 100644 index 0000000..dbc5b86 --- /dev/null +++ b/crates/e5r-diff/src/lib.rs @@ -0,0 +1,505 @@ +//! Matching functions across two builds. +//! +//! The use case that has to work is patch diffing: given a vulnerable build and +//! a fixed one, say which function changed. That means the answer has to be +//! *ranked*, not just listed, because a compiler moves every function and a +//! report that says ten thousand things changed is a report nobody reads. +//! +//! Matching runs in passes, strongest evidence first, and each pass only +//! considers what earlier passes left over: +//! +//! 1. Exact body hash. The same bytes are the same function, whatever moved. +//! 2. Instruction-shape hash, which ignores branch targets and so survives +//! relocation. A unique shape on both sides is a confident match. +//! 3. Name, for the functions a symbol table names. +//! 4. Call-graph neighbourhood: an unmatched function whose callers and callees +//! are already matched, uniquely, to a function whose callers and callees +//! are the same. +//! +//! Anything still unmatched is an addition or a removal, which is itself a +//! finding. +//! +//! [`align`] takes it one level down: inside a matched pair, which +//! instruction changed. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod align; + +pub use align::{Edit, EditKind, FunctionDiff, compare_function, detail, mapping}; + +use std::collections::{BTreeMap, BTreeSet, HashMap}; + +use e5r_analysis::{Function, Program}; +use e5r_core::Addr; +use e5r_db::Anchor; +use serde::Serialize; + +/// How a pair of functions was matched. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MatchKind { + /// The same bytes. + Identical, + /// The same instruction shape, so the same code at a different address. + Shape, + /// The same name. + Name, + /// The same position in the call graph. + CallGraph, +} + +impl MatchKind { + /// The word shown in output. + pub fn as_str(self) -> &'static str { + match self { + MatchKind::Identical => "identical", + MatchKind::Shape => "shape", + MatchKind::Name => "name", + MatchKind::CallGraph => "call graph", + } + } +} + +/// One matched pair. +#[derive(Debug, Clone, Serialize)] +pub struct Match { + /// Where it is in the old build. + pub old: Addr, + /// Where it is in the new build. + pub new: Addr, + /// The name to show, from whichever side has one. + pub name: String, + /// How the pair was found. + pub kind: MatchKind, + /// How alike the two are, from 0 to 1. Exactly 1 means byte-identical. + pub similarity: f64, + /// Instructions on each side, for the report. + pub old_insns: u32, + /// Instructions in the new build. + pub new_insns: u32, +} + +impl Match { + /// True when the two sides differ at all. + pub fn changed(&self) -> bool { + self.similarity < 1.0 + } +} + +/// The result of comparing two builds. +#[derive(Debug, Clone, Default, Serialize)] +pub struct Diff { + /// Pairs, changed ones first and most changed at the top. + pub matched: Vec, + /// Functions only the old build has. + pub removed: Vec<(Addr, String)>, + /// Functions only the new build has. + pub added: Vec<(Addr, String)>, +} + +impl Diff { + /// How many pairs differ. + pub fn changed_count(&self) -> usize { + self.matched.iter().filter(|m| m.changed()).count() + } + + /// How many pairs are byte-identical. + pub fn identical_count(&self) -> usize { + self.matched.iter().filter(|m| !m.changed()).count() + } + + /// The changed pairs, most changed first. This is the answer to "what did + /// the patch touch". + pub fn changed(&self) -> impl Iterator { + self.matched.iter().filter(|m| m.changed()) + } +} + +/// One side's functions, indexed the ways matching needs. +struct Side<'a> { + program: &'a Program, + anchors: BTreeMap, + /// Mnemonic multiset per function, for scoring. + profiles: BTreeMap>, + by_bytes: HashMap>, + by_shape: HashMap>, + by_name: HashMap>, + /// Who calls whom, and who is called by whom. + callees: BTreeMap>, + callers: BTreeMap>, +} + +impl<'a> Side<'a> { + fn build(program: &'a Program) -> Side<'a> { + let mut anchors = BTreeMap::new(); + let mut profiles = BTreeMap::new(); + let mut by_bytes: HashMap> = HashMap::new(); + let mut by_shape: HashMap> = HashMap::new(); + let mut by_name: HashMap> = HashMap::new(); + let mut callees: BTreeMap> = BTreeMap::new(); + let mut callers: BTreeMap> = BTreeMap::new(); + + for f in program.functions_by_address() { + let insns = program.instructions(f); + let mut body = Vec::with_capacity(f.cfg.covered_bytes() as usize); + for b in f.cfg.blocks.values() { + if let Some(bytes) = program.object.memory.slice(b.range.start(), b.range.len()) { + body.extend_from_slice(bytes); + } + } + let anchor = Anchor::function(f.entry, &insns, &body); + by_bytes.entry(anchor.bytes).or_default().push(f.entry); + by_shape.entry(anchor.shape).or_default().push(f.entry); + if let Some(n) = f.raw_name() { + by_name.entry(n.to_string()).or_default().push(f.entry); + } + anchors.insert(f.entry, anchor); + + let mut profile: BTreeMap<&'static str, u32> = BTreeMap::new(); + for i in &insns { + *profile.entry(i.mnemonic).or_default() += 1; + } + profiles.insert(f.entry, profile); + + let out: BTreeSet = f.cfg.calls.iter().copied().collect(); + for c in &out { + callers.entry(*c).or_default().insert(f.entry); + } + callees.insert(f.entry, out); + } + + Side { + program, + anchors, + profiles, + by_bytes, + by_shape, + by_name, + callees, + callers, + } + } + + fn name(&self, at: Addr) -> String { + self.program + .function(at) + .map(|f| f.display_name()) + .unwrap_or_else(|| format!("sub_{:x}", at.get())) + } + + fn insns(&self, at: Addr) -> u32 { + self.program + .function(at) + .map(|f| f.cfg.insns()) + .unwrap_or(0) + } +} + +/// How alike two functions are, by their mnemonic multisets. +/// +/// A multiset rather than a sequence: a compiler reorders blocks freely between +/// builds, and a sequence comparison would call every function changed. It does +/// mean a pure reordering scores 1.0 on this measure, which is why an exact +/// byte match is what sets similarity to 1 rather than this score. +fn similarity(a: &BTreeMap<&'static str, u32>, b: &BTreeMap<&'static str, u32>) -> f64 { + if a.is_empty() && b.is_empty() { + return 1.0; + } + let mut shared = 0u32; + let mut total = 0u32; + for (m, n) in a { + let other = b.get(m).copied().unwrap_or(0); + shared += (*n).min(other); + total += (*n).max(other); + } + for (m, n) in b { + if !a.contains_key(m) { + total += *n; + } + } + if total == 0 { + return 1.0; + } + shared as f64 / total as f64 +} + +/// Compare two analyzed builds. +pub fn compare(old: &Program, new: &Program) -> Diff { + let a = Side::build(old); + let b = Side::build(new); + + let mut pairs: Vec = Vec::new(); + let mut used_old: BTreeSet = BTreeSet::new(); + let mut used_new: BTreeSet = BTreeSet::new(); + + // Pass 1 and 2: unique hashes on both sides. + for (kind, index_a, index_b) in [ + (MatchKind::Identical, &a.by_bytes, &b.by_bytes), + (MatchKind::Shape, &a.by_shape, &b.by_shape), + ] { + for (key, olds) in index_a { + let Some(news) = index_b.get(key) else { + continue; + }; + // Only a one-to-one hash is evidence; several functions sharing a + // hash means the hash did not identify anything. + let olds: Vec = olds + .iter() + .copied() + .filter(|x| !used_old.contains(x)) + .collect(); + let news: Vec = news + .iter() + .copied() + .filter(|x| !used_new.contains(x)) + .collect(); + if olds.len() != 1 || news.len() != 1 { + continue; + } + record( + &a, + &b, + olds[0], + news[0], + kind, + &mut pairs, + &mut used_old, + &mut used_new, + ); + } + } + + // Pass 3: names. + for (name, olds) in &a.by_name { + let Some(news) = b.by_name.get(name) else { + continue; + }; + let olds: Vec = olds + .iter() + .copied() + .filter(|x| !used_old.contains(x)) + .collect(); + let news: Vec = news + .iter() + .copied() + .filter(|x| !used_new.contains(x)) + .collect(); + if olds.len() != 1 || news.len() != 1 { + continue; + } + record( + &a, + &b, + olds[0], + news[0], + MatchKind::Name, + &mut pairs, + &mut used_old, + &mut used_new, + ); + } + + // Pass 4: call-graph position. Repeated, because each match makes more + // neighbourhoods decidable. + for _ in 0..4 { + let mapping: BTreeMap = pairs.iter().map(|m| (m.old, m.new)).collect(); + let mut found = Vec::new(); + for f in a.program.functions_by_address() { + if used_old.contains(&f.entry) { + continue; + } + let Some(candidate) = neighbourhood_match(&a, &b, f, &mapping, &used_new) else { + continue; + }; + found.push((f.entry, candidate)); + } + if found.is_empty() { + break; + } + for (o, n) in found { + if used_old.contains(&o) || used_new.contains(&n) { + continue; + } + record( + &a, + &b, + o, + n, + MatchKind::CallGraph, + &mut pairs, + &mut used_old, + &mut used_new, + ); + } + } + + // Most changed first: that is the order a patch diff is read in. + pairs.sort_by(|x, y| { + x.similarity + .partial_cmp(&y.similarity) + .unwrap_or(std::cmp::Ordering::Equal) + .then(x.old.cmp(&y.old)) + }); + + let removed = a + .program + .functions_by_address() + .filter(|f| !used_old.contains(&f.entry)) + .map(|f| (f.entry, f.display_name())) + .collect(); + let added = b + .program + .functions_by_address() + .filter(|f| !used_new.contains(&f.entry)) + .map(|f| (f.entry, f.display_name())) + .collect(); + + Diff { + matched: pairs, + removed, + added, + } +} + +#[allow(clippy::too_many_arguments)] +fn record( + a: &Side<'_>, + b: &Side<'_>, + old: Addr, + new: Addr, + kind: MatchKind, + pairs: &mut Vec, + used_old: &mut BTreeSet, + used_new: &mut BTreeSet, +) { + let identical = a.anchors.get(&old).map(|x| x.bytes) == b.anchors.get(&new).map(|x| x.bytes); + let score = if identical { + 1.0 + } else { + // Never report a non-identical pair as a perfect match: the multiset + // score cannot see a reordering, and claiming no change when the bytes + // differ is exactly the mistake a patch diff must not make. + similarity(&a.profiles[&old], &b.profiles[&new]).min(0.999) + }; + used_old.insert(old); + used_new.insert(new); + pairs.push(Match { + old, + new, + name: { + let n = a.name(old); + if n.starts_with("sub_") { + b.name(new) + } else { + n + } + }, + kind, + similarity: score, + old_insns: a.insns(old), + new_insns: b.insns(new), + }); +} + +/// The one new-build function whose neighbours are the images of this one's. +fn neighbourhood_match( + a: &Side<'_>, + b: &Side<'_>, + f: &Function, + mapping: &BTreeMap, + used_new: &BTreeSet, +) -> Option { + // Project the known callers and callees into the new build. + let want_callees: BTreeSet = a + .callees + .get(&f.entry)? + .iter() + .filter_map(|c| mapping.get(c).copied()) + .collect(); + let want_callers: BTreeSet = a + .callers + .get(&f.entry) + .into_iter() + .flatten() + .filter_map(|c| mapping.get(c).copied()) + .collect(); + // Too little context to be evidence of anything. + if want_callees.len() + want_callers.len() < 2 { + return None; + } + + // A candidate is any unmatched function called by one of the projected + // callers, or calling one of the projected callees. + let mut candidates: BTreeSet = BTreeSet::new(); + for caller in &want_callers { + if let Some(cs) = b.callees.get(caller) { + candidates.extend(cs.iter().filter(|x| !used_new.contains(x))); + } + } + for callee in &want_callees { + if let Some(cs) = b.callers.get(callee) { + candidates.extend(cs.iter().filter(|x| !used_new.contains(x))); + } + } + + let mut best: Option<(Addr, usize)> = None; + let mut tied = false; + for c in candidates { + let has_callees = b.callees.get(&c).cloned().unwrap_or_default(); + let has_callers = b.callers.get(&c).cloned().unwrap_or_default(); + let score = want_callees.intersection(&has_callees).count() + + want_callers.intersection(&has_callers).count(); + if score < 2 { + continue; + } + match best { + Some((_, s)) if s > score => {} + Some((_, s)) if s == score => tied = true, + _ => { + best = Some((c, score)); + tied = false; + } + } + } + // A tie is not evidence. + if tied { None } else { best.map(|(c, _)| c) } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn profile(pairs: &[(&'static str, u32)]) -> BTreeMap<&'static str, u32> { + pairs.iter().copied().collect() + } + + #[test] + fn identical_profiles_score_one() { + let p = profile(&[("mov", 3), ("add", 1)]); + assert_eq!(similarity(&p, &p), 1.0); + } + + #[test] + fn disjoint_profiles_score_zero() { + let a = profile(&[("mov", 3)]); + let b = profile(&[("add", 3)]); + assert_eq!(similarity(&a, &b), 0.0); + } + + #[test] + fn one_extra_instruction_scores_close_to_one() { + let a = profile(&[("mov", 10), ("add", 10)]); + let b = profile(&[("mov", 10), ("add", 10), ("cmp", 1)]); + let s = similarity(&a, &b); + assert!(s > 0.9 && s < 1.0, "{s}"); + } + + #[test] + fn the_empty_case_is_not_a_division_by_zero() { + let empty = BTreeMap::new(); + assert_eq!(similarity(&empty, &empty), 1.0); + assert_eq!(similarity(&profile(&[("mov", 1)]), &empty), 0.0); + } +} diff --git a/crates/e5r-diff/tests/insn_diff.rs b/crates/e5r-diff/tests/insn_diff.rs new file mode 100644 index 0000000..7c03137 --- /dev/null +++ b/crates/e5r-diff/tests/insn_diff.rs @@ -0,0 +1,273 @@ +//! Instruction-granularity diff, measured on a pair of builds whose source +//! difference is known exactly. +//! +//! `insndiff.a64` and its three siblings are the same program with one source +//! line changed each time: one operator substituted, one statement added, one +//! statement removed. The compiler is gcc at `-O0`, so what it emits is a +//! transcription of the source rather than a product of the scheduler, and the +//! expected edit list is exact. These tests assert that list, instruction for +//! instruction, rather than asserting that some difference was found. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::Addr; +use e5r_diff::{Edit, EditKind, FunctionDiff}; +use e5r_format::LoadOptions; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +fn entry(p: &Program, name: &str) -> Option { + p.functions_by_address() + .find(|f| f.raw_name() == Some(name)) + .map(|f| f.entry) +} + +/// The instruction diff of one function, with the whole-program match as the +/// map that explains where a call's target went. +fn function_diff(old: &Program, new: &Program, name: &str) -> Option { + let d = e5r_diff::compare(old, new); + let map = e5r_diff::mapping(&d); + e5r_diff::compare_function(old, entry(old, name)?, new, entry(new, name)?, &map) +} + +fn kinds(edits: &[Edit]) -> Vec<(EditKind, &str, &str)> { + edits + .iter() + .map(|e| (e.kind, e.old_text.as_str(), e.new_text.as_str())) + .collect() +} + +fn mnemonic(text: &str) -> &str { + text.split_whitespace().next().unwrap_or("") +} + +#[test] +fn a_build_against_itself_has_no_edits() { + let (Some(a), Some(b)) = (open("insndiff.a64"), open("insndiff.a64")) else { + return; + }; + let d = function_diff(&a, &b, "mix").expect("mix in both builds"); + assert_eq!(kinds(&d.edits), vec![], "a build differs from itself"); + assert_eq!(d.same, d.old_insns); + assert!(!d.truncated); +} + +#[test] +fn one_substituted_operator_is_one_replaced_instruction() { + // `acc = acc + i` became `acc = acc - i`. One instruction, same length, so + // nothing after it moved and there is nothing else in the list. + let (Some(a), Some(b)) = (open("insndiff.a64"), open("insndiff-sub.a64")) else { + return; + }; + let d = function_diff(&a, &b, "mix").expect("mix in both builds"); + assert_eq!(d.edits.len(), 1, "{:?}", kinds(&d.edits)); + let e = &d.edits[0]; + assert_eq!(e.kind, EditKind::Replace); + assert_eq!(mnemonic(&e.old_text), "add", "{}", e.old_text); + assert_eq!(mnemonic(&e.new_text), "sub", "{}", e.new_text); + assert_eq!(d.old_insns, d.new_insns); + assert_eq!(d.same, d.old_insns - 1); + // Both addresses, which is the whole point of doing this at instruction + // rather than function granularity. They are the same address here + // because a substitution of equal length moves nothing. + assert_eq!(e.old, Some(Addr(0x400164))); + assert_eq!(e.new, e.old); +} + +#[test] +fn one_added_statement_is_four_inserted_instructions_and_one_displacement() { + // `acc = acc ^ 21` added inside the loop. At -O0 that is a load, a + // constant, the operation and a store; the forward branch that skips into + // the loop test then points four instructions further along, which is a + // displacement and not a change. + let (Some(a), Some(b)) = (open("insndiff.a64"), open("insndiff-ins.a64")) else { + return; + }; + let d = function_diff(&a, &b, "mix").expect("mix in both builds"); + assert_eq!(d.new_insns, d.old_insns + 4); + + let inserted: Vec<&str> = d + .changes() + .map(|e| { + assert_eq!(e.kind, EditKind::Insert, "{:?}", kinds(&d.edits)); + mnemonic(&e.new_text) + }) + .collect(); + assert_eq!( + inserted, + ["ldr", "mov", "eor", "str"], + "{:?}", + kinds(&d.edits) + ); + + // Exactly one instruction moved without changing, and it is a branch. + let displaced: Vec<&Edit> = d + .edits + .iter() + .filter(|e| e.kind == EditKind::Displaced) + .collect(); + assert_eq!(displaced.len(), 1, "{:?}", kinds(&d.edits)); + assert_eq!(mnemonic(&displaced[0].old_text), "b"); + assert_ne!(displaced[0].old_text, displaced[0].new_text); + + // And nothing was called retargeted, which would mean the alignment could + // not account for where a target went. + assert!(!d.edits.iter().any(|e| e.kind == EditKind::Retargeted)); + assert_eq!(d.same, d.old_insns - 1); +} + +#[test] +fn one_removed_statement_is_three_deleted_instructions() { + // The same test in the other direction: `acc = acc - 7` removed. + let (Some(a), Some(b)) = (open("insndiff.a64"), open("insndiff-del.a64")) else { + return; + }; + let d = function_diff(&a, &b, "mix").expect("mix in both builds"); + assert_eq!(d.old_insns, d.new_insns + 3); + + let deleted: Vec<&str> = d + .changes() + .map(|e| { + assert_eq!(e.kind, EditKind::Delete, "{:?}", kinds(&d.edits)); + mnemonic(&e.old_text) + }) + .collect(); + assert_eq!(deleted, ["ldr", "sub", "str"], "{:?}", kinds(&d.edits)); + assert_eq!(d.displaced_count(), 1, "{:?}", kinds(&d.edits)); + assert!(!d.edits.iter().any(|e| e.kind == EditKind::Retargeted)); +} + +#[test] +fn a_function_that_only_moved_has_no_changes_in_it() { + // This is the claim the whole alignment exists to support. `_start` calls + // `mix` and `other`; inserting instructions into `mix` pushes `other` + // along, so every byte of the call in `_start` differs and the + // function-level diff calls `_start` changed. Nothing in it did change, + // and the instruction-level answer has to say so. + let (Some(a), Some(b)) = (open("insndiff.a64"), open("insndiff-ins.a64")) else { + return; + }; + let d = e5r_diff::compare(&a, &b); + let changed: Vec<&str> = d.changed().map(|m| m.name.as_str()).collect(); + assert!( + changed.contains(&"_start"), + "the function-level diff did not flag _start: {changed:?}" + ); + + let detail = function_diff(&a, &b, "_start").expect("_start in both builds"); + assert_eq!( + detail.change_count(), + 0, + "_start reported changes: {:?}", + kinds(&detail.edits) + ); + assert!( + detail.displaced_count() >= 1, + "nothing in _start was recognized as displaced" + ); + // The call is one of them, and it is only explicable through the function + // matching: the callee moved and the matcher says where to. + assert!( + detail + .edits + .iter() + .any(|e| e.kind == EditKind::Displaced && mnemonic(&e.old_text) == "bl"), + "{:?}", + kinds(&detail.edits) + ); +} + +#[test] +fn without_the_function_map_a_moved_callee_is_not_claimed_to_be_displaced() { + // The negative of the test above. With no map, nothing accounts for the + // call's new target, and the honest answer is that it was retargeted + // rather than that it merely moved. + let (Some(a), Some(b)) = (open("insndiff.a64"), open("insndiff-ins.a64")) else { + return; + }; + let empty = BTreeMap::new(); + let d = e5r_diff::compare_function( + &a, + entry(&a, "_start").unwrap(), + &b, + entry(&b, "_start").unwrap(), + &empty, + ) + .expect("_start in both builds"); + assert!( + d.edits + .iter() + .any(|e| e.kind == EditKind::Retargeted && mnemonic(&e.old_text) == "bl"), + "{:?}", + kinds(&d.edits) + ); +} + +#[test] +fn the_detail_pass_covers_every_function_the_summary_flagged() { + let (Some(a), Some(b)) = (open("insndiff.a64"), open("insndiff-ins.a64")) else { + return; + }; + let d = e5r_diff::compare(&a, &b); + let detail = e5r_diff::detail(&a, &b, &d); + assert_eq!(detail.len(), d.changed_count()); + // Only `mix` actually changed; everything else the summary flagged moved. + let with_changes: Vec<&str> = detail + .iter() + .filter(|f| f.changed()) + .map(|f| f.name.as_str()) + .collect(); + assert_eq!(with_changes, ["mix"], "{with_changes:?}"); +} + +#[test] +fn the_answer_does_not_depend_on_which_run_produced_it() { + // Gate G5 applies here too: the alignment walks hash maps, and an answer + // that came out of an iteration order would be a different answer on the + // next run. + let (Some(a), Some(b)) = (open("insndiff.a64"), open("insndiff-ins.a64")) else { + return; + }; + let first = e5r_diff::detail(&a, &b, &e5r_diff::compare(&a, &b)); + for _ in 0..4 { + let again = e5r_diff::detail(&a, &b, &e5r_diff::compare(&a, &b)); + assert_eq!(first.len(), again.len()); + for (x, y) in first.iter().zip(&again) { + assert_eq!(kinds(&x.edits), kinds(&y.edits), "{} is not stable", x.name); + } + } +} + +#[test] +fn an_optimization_level_change_is_reported_without_blowing_up() { + // Not a known-difference fixture: -O0 against -O2 is a rewrite, and the + // only thing worth asserting is that the alignment terminates, stays + // inside its budget and never claims more edits than there are + // instructions. + let (Some(a), Some(b)) = (open("hello.a64.O0"), open("hello.a64.O2")) else { + return; + }; + let d = e5r_diff::compare(&a, &b); + for f in e5r_diff::detail(&a, &b, &d) { + assert!( + f.edits.len() <= (f.old_insns + f.new_insns) as usize, + "{} reported {} edits for {} and {} instructions", + f.name, + f.edits.len(), + f.old_insns, + f.new_insns + ); + assert!(f.same <= f.old_insns.min(f.new_insns)); + } +} diff --git a/crates/e5r-diff/tests/patch_diff.rs b/crates/e5r-diff/tests/patch_diff.rs new file mode 100644 index 0000000..c45975c --- /dev/null +++ b/crates/e5r-diff/tests/patch_diff.rs @@ -0,0 +1,131 @@ +//! The use case the matcher exists for: given two builds, say what the patch +//! touched, and say it without burying the answer. + +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_format::LoadOptions; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +#[test] +fn a_build_against_itself_has_nothing_changed() { + let (Some(a), Some(b)) = (open("hello.a64.O2"), open("hello.a64.O2")) else { + return; + }; + let d = e5r_diff::compare(&a, &b); + assert!(d.matched.len() > 5, "only {} matched", d.matched.len()); + assert_eq!(d.changed_count(), 0, "a build differs from itself"); + assert!(d.added.is_empty() && d.removed.is_empty()); +} + +#[test] +fn a_one_line_patch_names_one_function() { + // The whole point: the answer has to be short. A report that says ten + // thousand things changed is a report nobody reads. + let (Some(a), Some(b)) = (open("hello.a64.O2"), open("hello-patched.a64.O2")) else { + return; + }; + let d = e5r_diff::compare(&a, &b); + assert!( + d.changed_count() <= 2, + "a one-line change reported {} changed functions: {:?}", + d.changed_count(), + d.changed().map(|m| m.name.clone()).collect::>() + ); + assert!(d.changed_count() >= 1, "the change was not noticed at all"); + assert!( + d.identical_count() > 5, + "only {} functions matched as identical", + d.identical_count() + ); + // Most changed first, which is the order this is read in. + let scores: Vec = d.matched.iter().map(|m| m.similarity).collect(); + assert!( + scores.windows(2).all(|w| w[0] <= w[1]), + "the report is not ranked" + ); +} + +#[test] +fn added_functions_are_reported_as_added() { + let (Some(a), Some(b)) = (open("hello.a64.O2"), open("hello-grown.a64.O2")) else { + return; + }; + let d = e5r_diff::compare(&a, &b); + let names: Vec<&str> = d.added.iter().map(|(_, n)| n.as_str()).collect(); + for want in ["extra_helper", "another_one"] { + assert!( + names.contains(&want), + "{want} not reported as added: {names:?}" + ); + } + assert!(d.removed.is_empty(), "nothing was removed: {:?}", d.removed); +} + +#[test] +fn a_match_that_is_not_byte_identical_is_never_called_unchanged() { + // The mistake a patch diff must not make. The mnemonic multiset cannot see + // a reordering, so a perfect score has to come from the bytes. + let (Some(a), Some(b)) = (open("hello.a64.O2"), open("hello-patched.a64.O2")) else { + return; + }; + let d = e5r_diff::compare(&a, &b); + for m in &d.matched { + if m.similarity == 1.0 { + let (Some(fa), Some(fb)) = (a.function(m.old), b.function(m.new)) else { + continue; + }; + let bytes = |p: &Program, f: &e5r_analysis::Function| -> Vec { + f.cfg + .blocks + .values() + .filter_map(|blk| p.object.memory.slice(blk.range.start(), blk.range.len())) + .flatten() + .copied() + .collect() + }; + assert_eq!( + bytes(&a, fa), + bytes(&b, fb), + "{} scored 1.0 without identical bytes", + m.name + ); + } + } +} + +#[test] +fn optimization_levels_differ_where_the_source_is_ours() { + // A check in the other direction. Only our own functions: the C runtime + // stubs come from prebuilt objects and really are identical whatever + // optimization level the program was compiled at. + let (Some(a), Some(b)) = (open("hello.a64.O0"), open("hello.a64.O2")) else { + return; + }; + let d = e5r_diff::compare(&a, &b); + let ours: Vec<&e5r_diff::Match> = d + .matched + .iter() + .filter(|m| matches!(m.name.as_str(), "main" | "sum_to" | "classify")) + .collect(); + if ours.is_empty() { + return; + } + for m in &ours { + assert!( + m.changed(), + "{} called identical across -O0 and -O2", + m.name + ); + } +} diff --git a/crates/e5r-format/Cargo.toml b/crates/e5r-format/Cargo.toml new file mode 100644 index 0000000..aed42a0 --- /dev/null +++ b/crates/e5r-format/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "e5r-format" +description = "e5r — container loaders: ELF, PE, Mach-O, raw." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +e5r-core = { workspace = true } +e5r-types = { workspace = true } +serde = { workspace = true } diff --git a/crates/e5r-format/src/archive.rs b/crates/e5r-format/src/archive.rs new file mode 100644 index 0000000..ede38e0 --- /dev/null +++ b/crates/e5r-format/src/archive.rs @@ -0,0 +1,776 @@ +//! `ar` archives: a container of containers. +//! +//! A `.a` is a directory, not an image. It has no architecture, no entry point +//! and no memory map, so it cannot become an [`Object`]; what it has is a list +//! of members, each of which is a container in its own right, and a symbol +//! index mapping a name to the member that defines it. That index is the thing +//! worth having: "which object in `libc.a` defines `memcpy`" is a question the +//! archive answers directly and a linker asks constantly. +//! +//! So [`open`] is separate from [`crate::load`] rather than folded into it, and +//! [`crate::load`] refuses an archive with a message saying to open it as one. +//! The alternative, picking a member and pretending it was the file, would make +//! every later answer silently about the wrong bytes. +//! +//! Three dialects share the 60-byte header and disagree about names: GNU spells +//! a long name as an offset into a `//` member, BSD stores it in the member's +//! own data behind a `#1/` marker, and a plain archive has neither. All three +//! are read here, because the flavour is a property of the `ar` that wrote the +//! file and not of the platform it targets. + +use e5r_core::{Caps, Error, Reader, Result}; + +use crate::{LoadOptions, Object}; + +/// The global header an archive starts with. +const MAGIC: &[u8; 8] = b"!\n"; + +/// GNU's thin archive: members name external files and carry no bytes here. +const THIN_MAGIC: &[u8; 8] = b"!\n"; + +/// One member header: sixteen bytes of name, five ASCII numbers, a two-byte +/// terminator. Fixed width, which is what makes the walk bounded. +const HEADER_LEN: u64 = 60; + +/// Longest member name accepted from a long-name table, so an unterminated +/// table cannot become one enormous name. +const MAX_NAME: u64 = 1 << 12; + +/// Which dialect wrote the archive. They differ only in how a long name and +/// the symbol index are spelled. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Flavor { + /// Names end in `/`, long ones live in a `//` member, index member is `/`. + Gnu, + /// Long names sit in the member data behind `#1/`, index is `__.SYMDEF`. + Bsd, + /// Neither marker appeared, which a short-named archive is allowed to do. + Plain, +} + +impl Flavor { + /// The name used in output and JSON. + pub fn as_str(self) -> &'static str { + match self { + Flavor::Gnu => "gnu", + Flavor::Bsd => "bsd", + Flavor::Plain => "plain", + } + } +} + +impl std::fmt::Display for Flavor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// One archive member: a name and the bytes a caller can hand to [`crate::load`]. +#[derive(Clone)] +pub struct Member<'a> { + /// The member name with its dialect's decoration removed. + pub name: String, + /// File offset of the member's 60-byte header, which is what the symbol + /// index points at. + pub header_offset: u64, + /// File offset of the member's first data byte. + pub offset: u64, + /// Size of the data, after any BSD long name in front of it. + pub size: u64, + /// Modification time, seconds since the epoch, zero when unset. + pub mtime: u64, + /// Owner, zero when unset. + pub uid: u32, + /// Group, zero when unset. + pub gid: u32, + /// Permission bits, as an octal field in the header. + pub mode: u32, + /// True for a member whose bytes are not in this file: a thin archive + /// records the name and the size and leaves the content on disk. + pub thin: bool, + data: &'a [u8], +} + +impl<'a> Member<'a> { + /// The member's bytes, empty for a thin archive's external member. + pub fn data(&self) -> &'a [u8] { + self.data + } + + /// True when this member is bookkeeping rather than an object: the symbol + /// index or the long-name table. + pub fn is_special(&self) -> bool { + matches!(self.name.as_str(), "/" | "//" | "/SYM64/") || self.name.starts_with("__.SYMDEF") + } +} + +impl std::fmt::Debug for Member<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // The bytes are omitted on purpose: a debug print of a member is a + // debug print of an object file. + f.debug_struct("Member") + .field("name", &self.name) + .field("offset", &self.offset) + .field("size", &self.size) + .field("thin", &self.thin) + .finish() + } +} + +/// One entry of the symbol index: a name and the member that defines it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IndexEntry { + /// The symbol, as the index spells it, still mangled. + pub name: String, + /// Header offset the index recorded, kept even when it matched no member + /// so a broken index is visible rather than dropped. + pub header_offset: u64, + /// Index into [`Archive::members`], absent when the offset named none. + pub member: Option, +} + +/// A parsed archive. +#[derive(Debug, Clone)] +pub struct Archive<'a> { + /// Which dialect wrote it. + pub flavor: Flavor, + /// True when the members' bytes live in separate files. + pub thin: bool, + /// Members in file order, bookkeeping members included, so an offset in + /// the symbol index resolves to the same numbering `ar t` prints minus the + /// special entries. + pub members: Vec>, + /// The symbol index, in index order. Empty when the archive has none, + /// which is legal and means a linker has to scan every member. + pub index: Vec, + /// Problems that did not stop the parse. A damaged archive still lists the + /// members before the damage. + pub warnings: Vec, +} + +impl<'a> Archive<'a> { + /// The first member with this name. + pub fn member(&self, name: &str) -> Option<&Member<'a>> { + self.members.iter().find(|m| m.name == name) + } + + /// Members that are objects rather than bookkeeping, with their index. A + /// thin archive's members are listed too: they are objects, they are just + /// not in this file, and [`Archive::load_member`] says so. + pub fn objects(&self) -> impl Iterator)> { + self.members + .iter() + .enumerate() + .filter(|(_, m)| !m.is_special()) + } + + /// Every member the symbol index says defines `symbol`. A well-formed + /// archive names one; a plural answer is itself worth reporting. + pub fn definers(&self, symbol: &str) -> Vec { + let mut out: Vec = self + .index + .iter() + .filter(|e| e.name == symbol) + .filter_map(|e| e.member) + .collect(); + out.dedup(); + out + } + + /// Symbols the index attributes to one member. + pub fn symbols_of(&self, member: usize) -> Vec<&str> { + self.index + .iter() + .filter(|e| e.member == Some(member)) + .map(|e| e.name.as_str()) + .collect() + } + + /// Load one member through the ordinary container probe. + pub fn load_member(&self, member: usize, opts: &LoadOptions) -> Result { + let m = self.members.get(member).ok_or(Error::BadField { + field: "archive member index", + value: member as u64, + reason: "is past the end of the member list", + })?; + if m.thin { + return Err(Error::unsupported(format!( + "thin archive member {}: its bytes are in a separate file", + m.name + ))); + } + crate::load(m.data, opts) + } +} + +/// True when the bytes begin with either archive magic. +pub fn is_archive(data: &[u8]) -> bool { + data.len() >= 8 && (&data[..8] == MAGIC || &data[..8] == THIN_MAGIC) +} + +/// The error [`crate::load`] returns for an archive, so a `.a` reaching the +/// single-object path says what it is instead of "not a recognized container". +pub fn refuse_in_load(data: &[u8]) -> Result<()> { + if is_archive(data) { + return Err(Error::unsupported( + "an `ar` archive, which holds objects rather than being one; open it with \ + archive::open and load a member", + )); + } + Ok(()) +} + +/// Parse an archive with the default caps. +pub fn open(data: &[u8]) -> Result> { + open_with(data, &Caps::default()) +} + +/// A member as the walk found it, before names are resolved. The name field is +/// kept raw because resolving it needs the `//` member, which may come later. +struct Raw<'a> { + name_field: [u8; 16], + header_offset: u64, + offset: u64, + size: u64, + mtime: u64, + uid: u32, + gid: u32, + mode: u32, + thin: bool, + data: &'a [u8], +} + +/// Parse an archive, bounding every count against `caps`. +pub fn open_with<'a>(data: &'a [u8], caps: &Caps) -> Result> { + if !is_archive(data) { + return Err(Error::NotRecognized { + expected: "ar archive", + }); + } + let thin = data[..8] == *THIN_MAGIC; + let r = Reader::le(data); + let end = data.len() as u64; + let mut warnings = Vec::new(); + let mut raws: Vec> = Vec::new(); + let mut pos = 8u64; + + // Every member costs a 60-byte header, so the file length already bounds + // this loop; the cap turns a merely huge archive into a typed error. + while pos + HEADER_LEN <= end { + caps.check("archive members", raws.len() as u64 + 1, caps.sections)?; + let mut h = r.slice_at("member header", pos, HEADER_LEN)?; + let name_field: [u8; 16] = h.array("member name")?; + let mtime = ascii_num(h.bytes("member mtime", 12)?, 10).unwrap_or(0); + let uid = ascii_num(h.bytes("member uid", 6)?, 10).unwrap_or(0) as u32; + let gid = ascii_num(h.bytes("member gid", 6)?, 10).unwrap_or(0) as u32; + let mode = ascii_num(h.bytes("member mode", 8)?, 8).unwrap_or(0) as u32; + let size = ascii_num(h.bytes("member size", 10)?, 10); + let terminator = h.bytes("member header terminator", 2)?; + if terminator != b"`\n" { + warnings.push(format!( + "member header at {pos:#x} does not end in the `\\n terminator; stopping there" + )); + break; + } + let Some(size) = size else { + warnings.push(format!( + "member header at {pos:#x} has an unreadable size field; stopping there" + )); + break; + }; + let body = pos + HEADER_LEN; + // A thin archive still stores its index and long-name table inline; + // only the members that name external files have no bytes. + let inline = !thin || is_bookkeeping(&name_field); + let stored = if inline { size } else { 0 }; + let data_slice = match r.bytes_at("member data", body, stored) { + Ok(b) => b, + Err(_) => { + warnings.push(format!( + "member at {pos:#x} claims {size} bytes, which runs past the end of the file" + )); + break; + } + }; + raws.push(Raw { + name_field, + header_offset: pos, + offset: body, + size: stored, + mtime, + uid, + gid, + mode, + thin: !inline, + data: data_slice, + }); + // Members are two-byte aligned, the padding being a newline. + pos = body + stored + (stored & 1); + } + + if pos < end && pos + HEADER_LEN > end && end - pos > 1 { + warnings.push(format!( + "{} trailing bytes after the last member are too few for a header", + end - pos + )); + } + + let long_names = raws + .iter() + .find(|m| trim_field(&m.name_field) == b"//") + .map(|m| m.data) + .unwrap_or(&[]); + + let mut flavor = Flavor::Plain; + let mut members: Vec> = Vec::with_capacity(raws.len()); + for raw in &raws { + let (mut name, skip) = + resolve_name(&raw.name_field, long_names, &mut flavor, &mut warnings); + // A BSD long name is stored inside the member data and counted in the + // member size, so the name is read from there and both move past it. + let skip = skip.min(raw.size); + if skip > 0 { + let mut bytes = &raw.data[..skip as usize]; + // Padded to a multiple of eight with NULs, which are not the name. + while let Some((&0, rest)) = bytes.split_last() { + bytes = rest; + } + name = text(bytes); + } + members.push(Member { + name, + header_offset: raw.header_offset, + offset: raw.offset + skip, + size: raw.size - skip, + mtime: raw.mtime, + uid: raw.uid, + gid: raw.gid, + mode: raw.mode, + thin: raw.thin, + data: &raw.data[skip as usize..], + }); + } + + let index = read_index(&members, caps, &mut warnings)?; + Ok(Archive { + flavor, + thin, + members, + index, + warnings, + }) +} + +/// True for the name fields that hold the archive's own tables. +fn is_bookkeeping(field: &[u8; 16]) -> bool { + let t = trim_field(field); + t == b"/" || t == b"//" || t == b"/SYM64/" || t.starts_with(b"__.SYMDEF") +} + +/// Drop the trailing spaces the fixed-width header pads with. +fn trim_field(field: &[u8]) -> &[u8] { + let mut end = field.len(); + while end > 0 && (field[end - 1] == b' ' || field[end - 1] == 0) { + end -= 1; + } + &field[..end] +} + +/// An ASCII number in a fixed-width field. Blank means unset, not zero-length +/// garbage, and a non-digit stops the parse rather than being skipped. +fn ascii_num(field: &[u8], radix: u32) -> Option { + let t = trim_field(field); + let first = t.iter().position(|&b| b != b' ')?; + let t = &t[first..]; + let mut out: u64 = 0; + for &b in t { + let d = (b as char).to_digit(radix)?; + out = out.checked_mul(radix as u64)?.checked_add(d as u64)?; + } + Some(out) +} + +/// Resolve a raw name field, returning the name and how many data bytes the +/// name itself occupied. +fn resolve_name( + field: &[u8; 16], + long_names: &[u8], + flavor: &mut Flavor, + warnings: &mut Vec, +) -> (String, u64) { + let t = trim_field(field); + if t == b"/" || t == b"//" || t == b"/SYM64/" { + *flavor = Flavor::Gnu; + return (text(t), 0); + } + if t.starts_with(b"__.SYMDEF") { + *flavor = Flavor::Bsd; + return (text(t), 0); + } + // GNU terminates a name with a slash so a trailing space can be part of + // it. A thin archive writes both the slash and the long-name reference, + // as `/17 /`, so the terminator comes off before anything else + // is read. + let t = if t.ends_with(b"/") { + *flavor = Flavor::Gnu; + trim_field(&t[..t.len() - 1]) + } else { + t + }; + // BSD: `#1/N` says the next N data bytes are the name. + if let Some(rest) = t.strip_prefix(b"#1/") { + if let Some(n) = ascii_num(rest, 10) { + *flavor = Flavor::Bsd; + return (String::new(), n.min(MAX_NAME)); + } + } + // GNU: `/N` is an offset into the `//` member. + if let Some(rest) = t.strip_prefix(b"/") { + if let Some(off) = ascii_num(rest, 10) { + *flavor = Flavor::Gnu; + return match long_name_at(long_names, off) { + Some(n) => (n, 0), + None => { + warnings.push(format!( + "member name points at offset {off} of the long-name table, which \ + is {} bytes", + long_names.len() + )); + (format!("/{off}"), 0) + } + }; + } + } + (text(t), 0) +} + +/// A name from the `//` member, terminated by `/\n` or a NUL. +fn long_name_at(table: &[u8], offset: u64) -> Option { + let start = usize::try_from(offset).ok()?; + if start >= table.len() { + return None; + } + let limit = (start + MAX_NAME as usize).min(table.len()); + let slice = &table[start..limit]; + let n = slice + .iter() + .position(|&b| b == b'/' || b == 0 || b == b'\n') + .unwrap_or(slice.len()); + Some(text(&slice[..n])) +} + +/// Bytes to a name. Archive names are not required to be UTF-8 and a lossy +/// name is better than refusing the member. +fn text(b: &[u8]) -> String { + String::from_utf8_lossy(b).into_owned() +} + +/// Read whichever symbol index the archive carries, resolving each recorded +/// header offset to a member. +fn read_index( + members: &[Member<'_>], + caps: &Caps, + warnings: &mut Vec, +) -> Result> { + let mut raw: Vec<(String, u64)> = Vec::new(); + for m in members { + match m.name.as_str() { + "/" => raw = gnu_index(m.data, false, caps, warnings)?, + "/SYM64/" => raw = gnu_index(m.data, true, caps, warnings)?, + n if n.starts_with("__.SYMDEF") => { + raw = bsd_index(m.data, n.ends_with("_64"), caps, warnings)? + } + _ => continue, + } + break; + } + let mut out = Vec::with_capacity(raw.len()); + for (name, header_offset) in raw { + let member = members + .iter() + .position(|m| m.header_offset == header_offset); + if member.is_none() { + warnings.push(format!( + "symbol {name} is indexed at member offset {header_offset:#x}, where no \ + member header starts" + )); + } + out.push(IndexEntry { + name, + header_offset, + member, + }); + } + Ok(out) +} + +/// GNU's index: a big-endian count, that many member offsets, then that many +/// NUL-terminated names. `/SYM64/` is the same shape at eight bytes wide. +fn gnu_index( + data: &[u8], + wide: bool, + caps: &Caps, + warnings: &mut Vec, +) -> Result> { + let r = Reader::new(data, e5r_core::Endian::Big); + let mut c = r; + let count = c.uword("archive index count", wide)?; + caps.check("archive index symbols", count, caps.symbols)?; + // The declared count is not trusted for the allocation: the offsets are + // fixed width and have to fit in what is left of the member. + let width = if wide { 8 } else { 4 }; + let fits = (c.remaining() as u64) / width; + let n = count.min(fits); + if n < count { + warnings.push(format!( + "archive index claims {count} symbols, which is more than the {} bytes of the \ + index member hold", + data.len() + )); + } + let mut offsets = Vec::with_capacity(n as usize); + for _ in 0..n { + offsets.push(c.uword("archive index offset", wide)?); + } + let strings_at = c.pos(); + let mut out = Vec::with_capacity(n as usize); + let mut at = strings_at; + for off in offsets { + let Ok(name) = r.cstr_at("archive index name", at, caps.string_len) else { + warnings.push("archive index string table ends before its names do".into()); + break; + }; + at += name.len() as u64 + 1; + out.push((text(name), off)); + } + Ok(out) +} + +/// BSD's `__.SYMDEF`: a byte length, that many eight-byte ranlib pairs of +/// (string offset, member offset), a second byte length, then the strings. +/// +/// The file is written in the producing host's byte order with nothing to say +/// which, so the order is chosen by whichever reading of the first length +/// fits inside the member. +fn bsd_index( + data: &[u8], + wide: bool, + caps: &Caps, + warnings: &mut Vec, +) -> Result> { + let width = if wide { 8 } else { 4 }; + let endian = bsd_endian(data, wide).ok_or(Error::BadField { + field: "__.SYMDEF ranlib byte count", + value: data.len() as u64, + reason: "does not fit the member in either byte order", + })?; + let r = Reader::new(data, endian); + let mut c = r; + let ranlib_bytes = c.uword("__.SYMDEF ranlib bytes", wide)?; + let entry = width * 2; + let count = ranlib_bytes / entry; + caps.check("archive index symbols", count, caps.symbols)?; + let fits = (c.remaining() as u64) / entry; + let n = count.min(fits); + if n < count { + warnings.push(format!( + "__.SYMDEF claims {count} entries, which is more than the member holds" + )); + } + let mut pairs = Vec::with_capacity(n as usize); + for _ in 0..n { + let name_off = c.uword("__.SYMDEF name offset", wide)?; + let member_off = c.uword("__.SYMDEF member offset", wide)?; + pairs.push((name_off, member_off)); + } + // Whatever the count said, the string table starts after the declared + // ranlib bytes, so a clamped read still finds the names. + c.seek( + "__.SYMDEF string table", + width + ranlib_bytes.min(fits * entry), + )?; + let _string_bytes = c.uword("__.SYMDEF string bytes", wide)?; + let strings_at = c.pos(); + let mut out = Vec::with_capacity(pairs.len()); + for (name_off, member_off) in pairs { + let at = strings_at.saturating_add(name_off); + match r.cstr_at("__.SYMDEF name", at, caps.string_len) { + Ok(name) => out.push((text(name), member_off)), + Err(_) => { + warnings.push(format!( + "__.SYMDEF name offset {name_off} is outside its string table" + )); + } + } + } + Ok(out) +} + +/// Pick the byte order whose reading of the leading length fits the member. +fn bsd_endian(data: &[u8], wide: bool) -> Option { + let avail = data.len() as u64; + for endian in [e5r_core::Endian::Little, e5r_core::Endian::Big] { + let mut c = Reader::new(data, endian); + if let Ok(n) = c.uword("__.SYMDEF ranlib bytes", wide) { + if n <= avail.saturating_sub(if wide { 16 } else { 8 }) { + return Some(endian); + } + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Build a minimal GNU archive in memory, so the parser is exercised + /// without a toolchain present. + fn push_member(out: &mut Vec, name: &str, body: &[u8]) { + let h = format!( + "{name:<16}{:<12}{:<6}{:<6}{:<8}{:<10}`\n", + 0, + 0, + 0, + "644", + body.len() + ); + out.extend_from_slice(h.as_bytes()); + out.extend_from_slice(body); + if body.len() % 2 == 1 { + out.push(b'\n'); + } + } + + fn gnu_archive() -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(MAGIC); + push_member(&mut out, "one.o/", b"hello"); + push_member(&mut out, "two.o/", b"world!"); + out + } + + #[test] + fn members_and_padding() { + let bytes = gnu_archive(); + let a = open(&bytes).unwrap(); + assert_eq!(a.flavor, Flavor::Gnu); + let names: Vec<&str> = a.members.iter().map(|m| m.name.as_str()).collect(); + assert_eq!(names, ["one.o", "two.o"]); + assert_eq!(a.members[0].data(), b"hello"); + // The odd-sized first member is padded, so the second still parses. + assert_eq!(a.members[1].data(), b"world!"); + assert!(a.warnings.is_empty(), "{:?}", a.warnings); + } + + #[test] + fn not_an_archive_is_not_recognized() { + assert!( + open(b"not an archive at all") + .unwrap_err() + .is_not_recognized() + ); + assert!(open(b"").unwrap_err().is_not_recognized()); + assert!(refuse_in_load(b"\x7fELF").is_ok()); + assert!(refuse_in_load(MAGIC).is_err()); + } + + #[test] + fn a_lying_size_stops_the_walk() { + let mut a = gnu_archive(); + // Overwrite the first member's size field with something enormous. + let at = 8 + 16 + 12 + 6 + 6 + 8; + a[at..at + 10].copy_from_slice(b"4294967295"); + let parsed = open(&a).unwrap(); + assert!(parsed.members.is_empty()); + assert!(!parsed.warnings.is_empty()); + } + + #[test] + fn every_truncation_returns() { + let full = gnu_archive(); + for n in 0..full.len() { + let _ = open(&full[..n]); + } + } + + /// An archive holding nothing but a symbol index whose count is `claimed`. + fn lying_index(claimed: u32) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(&claimed.to_be_bytes()); + body.extend_from_slice(&8u32.to_be_bytes()); + let mut out = Vec::new(); + out.extend_from_slice(MAGIC); + push_member(&mut out, "/", &body); + out + } + + #[test] + fn an_index_count_past_the_member_is_clamped() { + let bytes = lying_index(1000); + let a = open(&bytes).unwrap(); + // One offset fits and its name does not, so nothing is claimed. + assert!(a.index.is_empty()); + assert!(!a.warnings.is_empty()); + } + + /// A BSD archive: `#1/N` long names and a `__.SYMDEF` index, which no + /// GNU `ar` on this machine can produce. + fn bsd_archive() -> Vec { + let long = b"a-name-longer-than-sixteen.o\0\0\0\0"; + let body = b"body"; + // The index member comes first, so the object's header offset is the + // magic plus the index header plus the index body. + let names = b"first\0second\0"; + let mut index = Vec::new(); + index.extend_from_slice(&16u32.to_le_bytes()); + for name_off in [0u32, 6] { + index.extend_from_slice(&name_off.to_le_bytes()); + index.extend_from_slice(&0u32.to_le_bytes()); + } + index.extend_from_slice(&(names.len() as u32).to_le_bytes()); + index.extend_from_slice(names); + let mut out = Vec::new(); + out.extend_from_slice(MAGIC); + push_member(&mut out, "__.SYMDEF", &index); + // The object's header starts wherever the index member ended, padding + // included, so the offsets are filled in once that is known. + let member_at = (out.len() as u32).to_le_bytes(); + let pairs_at = 8 + HEADER_LEN as usize + 4; + out[pairs_at + 4..pairs_at + 8].copy_from_slice(&member_at); + out[pairs_at + 12..pairs_at + 16].copy_from_slice(&member_at); + let mut data = long.to_vec(); + data.extend_from_slice(body); + push_member(&mut out, "#1/32", &data); + out + } + + #[test] + fn bsd_names_and_symdef() { + let bytes = bsd_archive(); + let a = open(&bytes).unwrap(); + assert_eq!(a.flavor, Flavor::Bsd); + assert!(a.warnings.is_empty(), "{:?}", a.warnings); + let obj: Vec<&Member<'_>> = a.objects().map(|(_, m)| m).collect(); + assert_eq!(obj.len(), 1); + assert_eq!(obj[0].name, "a-name-longer-than-sixteen.o"); + // The name came out of the data, so the data starts after it. + assert_eq!(obj[0].data(), b"body"); + let names: Vec<&str> = a.index.iter().map(|e| e.name.as_str()).collect(); + assert_eq!(names, ["first", "second"]); + assert!(a.index.iter().all(|e| e.member == Some(1))); + assert_eq!(a.definers("second"), vec![1]); + } + + #[test] + fn an_index_count_past_the_cap_is_an_error() { + let bytes = lying_index(u32::MAX); + assert!(matches!( + open(&bytes), + Err(Error::CapExceeded { + what: "archive index symbols", + .. + }) + )); + } +} diff --git a/crates/e5r-format/src/dwarf.rs b/crates/e5r-format/src/dwarf.rs new file mode 100644 index 0000000..2172b4e --- /dev/null +++ b/crates/e5r-format/src/dwarf.rs @@ -0,0 +1,2303 @@ +//! Reading DWARF debug information. +//! +//! What a binary with debug information tells you is what the compiler knew: +//! the names of functions and their parameters, the types of both, and which +//! source line each address came from. None of that can be recovered from +//! machine code, so when it is present it is worth more than any inference. +//! +//! Covers DWARF 4 and 5, both endiannesses, 32-bit and 64-bit formats. Forms +//! this does not know are skipped by their declared length rather than +//! guessed at, so an unfamiliar producer costs coverage and not correctness. + +use std::collections::BTreeMap; + +use e5r_core::{Addr, AddrRange, Endian, Evidence, Provenance, Reader}; +use e5r_types::ctype::{Composite, Enumeration, Field, Signature, Type, TypeId, Types}; + +/// A cursor over a debug section. +/// +/// The core reader labels every read for its error messages; DWARF reads +/// thousands of fields whose label would be the same word, so this wraps it +/// and turns a failure into `None`, which is what every caller here does with +/// it anyway. +struct Cur<'a>(Reader<'a>); + +impl<'a> Cur<'a> { + fn new(data: &'a [u8], endian: Endian) -> Cur<'a> { + Cur(Reader::new(data, endian)) + } + fn position(&self) -> usize { + self.0.pos() as usize + } + /// Move to an offset, clamped to the end of the data. Returns false when + /// the cursor did not actually move forward, which is how a length field + /// that points backwards or past the end is caught. + fn seek(&mut self, at: usize) -> bool { + let before = self.position(); + let target = at.min(self.0.len()); + let _ = self.0.seek("dwarf", target as u64); + self.position() > before + } + fn is_empty(&self) -> bool { + self.0.remaining() == 0 + } + fn u8(&mut self) -> Option { + self.0.u8("dwarf").ok() + } + fn u16(&mut self) -> Option { + self.0.u16("dwarf").ok() + } + fn u32(&mut self) -> Option { + self.0.u32("dwarf").ok() + } + fn u64(&mut self) -> Option { + self.0.u64("dwarf").ok() + } + fn uleb128(&mut self) -> Option { + self.0.uleb128("dwarf").ok() + } + fn sleb128(&mut self) -> Option { + self.0.sleb128("dwarf").ok() + } + fn bytes(&mut self, n: usize) -> Option<&'a [u8]> { + self.0.bytes("dwarf", n as u64).ok() + } + fn cstr(&mut self) -> Option { + let at = self.0.pos(); + let bytes = self + .0 + .cstr_at("dwarf", at, self.0.remaining() as u64) + .ok()?; + let out = String::from_utf8_lossy(bytes).into_owned(); + let _ = self.0.seek("dwarf", at + bytes.len() as u64 + 1); + Some(out) + } +} + +/// What the debug information said. +#[derive(Debug, Clone, Default)] +pub struct DebugInfo { + /// The types it defined. + pub types: Types, + /// The functions it described, by entry address. + pub functions: BTreeMap, + /// Global variables, by address. + pub variables: BTreeMap, + /// Compilation units seen, for reporting. + pub units: usize, + /// Where each address came from in the source. + pub lines: Vec, +} + +impl DebugInfo { + /// True when nothing was found. + pub fn is_empty(&self) -> bool { + self.functions.is_empty() && self.variables.is_empty() && self.lines.is_empty() + } + + /// The source position of an address, if the line table covers it. + pub fn line_for(&self, addr: Addr) -> Option<&LineRow> { + let n = self.lines.partition_point(|r| r.addr <= addr); + self.lines.get(n.checked_sub(1)?).filter(|r| !r.end) + } + + /// The function whose body covers `addr`. + pub fn function_at(&self, addr: Addr) -> Option<&DebugFunction> { + let n = self.functions.range(..=addr).next_back().map(|(_, f)| f); + n.filter(|f| f.covers(addr)) + } + + /// The inlined frames covering `addr`, outermost first. + /// + /// The last one is the function whose source the instruction was written + /// in; the others are the calls that brought it here. + pub fn inlined_at(&self, addr: Addr) -> Vec<&InlinedFrame> { + let Some(f) = self.function_at(addr) else { + return Vec::new(); + }; + let mut found: Vec<&InlinedFrame> = f.inlines.iter().filter(|i| i.covers(addr)).collect(); + found.sort_by_key(|i| i.depth); + found + } + + /// The call the compiler recorded as returning to `addr`. + pub fn call_site_returning_to(&self, addr: Addr) -> Option<&CallSite> { + self.function_at(addr)? + .call_sites + .iter() + .find(|c| c.return_pc == Some(addr)) + } + + /// Function entries the debug information proves, including the callees + /// call sites name at an address. + /// + /// Not wired into the loaders: `elf.rs` emits its own hints from + /// [`DebugInfo::functions`]. This is the same list plus the call targets, + /// for a caller that wants both. + pub fn hints(&self) -> Vec { + let mut out = Vec::new(); + for (addr, f) in &self.functions { + if f.name.is_empty() { + continue; + } + out.push(crate::FunctionHint { + addr: *addr, + size: f.size.filter(|s| *s > 0), + name: Some(f.name.clone()), + provenance: Provenance::new(Evidence::DebugInfo), + }); + } + for f in self.functions.values() { + for c in &f.call_sites { + let Some(target) = c.target else { continue }; + out.push(crate::FunctionHint { + addr: target, + size: None, + name: c.target_name.clone(), + provenance: Provenance::new(Evidence::DebugInfo), + }); + } + } + out + } +} + +/// A function the compiler described. +#[derive(Debug, Clone, Default)] +pub struct DebugFunction { + /// Its name in the source. + pub name: String, + /// The linkage name, when it differs. + pub linkage_name: Option, + /// Where it starts. + pub low_pc: Addr, + /// How long it is, when the entry said. + pub size: Option, + /// Its signature. + pub signature: Signature, + /// Local variables with a stack offset, keyed by that offset. + pub locals: Vec, + /// The source file it was declared in. + pub decl_file: Option, + /// The line it was declared on. + pub decl_line: Option, + /// True when the compiler inlined it somewhere. + pub inlined: bool, + /// The ranges the body occupies when it is not one contiguous piece, which + /// is what `DW_AT_ranges` says. Empty when `low_pc` and `size` describe it. + pub ranges: Vec, + /// Functions inlined into this one, innermost frames carrying the larger + /// `depth`. Sorted by start address then depth. + pub inlines: Vec, + /// Calls the compiler described, sorted by the address each returns to. + pub call_sites: Vec, +} + +impl DebugFunction { + /// True when `addr` is inside the body. + pub fn covers(&self, addr: Addr) -> bool { + if !self.ranges.is_empty() { + return self.ranges.iter().any(|r| r.contains(addr)); + } + match self.size { + Some(n) => AddrRange::sized(self.low_pc, n).is_some_and(|r| r.contains(addr)), + // Without a size the entry address is all that is known, and + // claiming the rest of the image would be an invention. + None => addr == self.low_pc, + } + } +} + +/// A call the compiler inlined, and the source it came from. +/// +/// An inlined call has no function of its own: the instructions belong to the +/// caller, and only this says a different function's source is sitting inside +/// it. +#[derive(Debug, Clone, Default)] +pub struct InlinedFrame { + /// The inlined function's name, from the entry its abstract origin names. + pub name: String, + /// Its linkage name, when the abstract origin carried one. + pub linkage_name: Option, + /// Offset in `.debug_info` of the abstract origin, so a caller can join + /// this to the out-of-line copy when there is one. + pub abstract_origin: Option, + /// The file the call was written in, resolved through the unit's line + /// program file table. + pub call_file: Option, + /// The line the call was written on. + pub call_line: Option, + /// The column the call was written at. + pub call_column: Option, + /// Where the inlined body starts, when the entry said so separately from + /// its ranges. + pub entry_pc: Option, + /// Every range of addresses the inlined body occupies. + pub ranges: Vec, + /// How deep inside other inlined frames this one sits; zero for a call + /// inlined directly into the function itself. + pub depth: u32, + /// Its parameters and locals, in the caller's frame. + pub locals: Vec, +} + +impl InlinedFrame { + /// True when `addr` is inside the inlined body. + pub fn covers(&self, addr: Addr) -> bool { + self.ranges.iter().any(|r| r.contains(addr)) + } + + /// The lowest address the frame covers. + pub fn low_pc(&self) -> Option { + self.ranges.iter().map(|r| r.start()).min() + } +} + +/// One call, as the compiler described it. +/// +/// This is the prototype information our own analysis tries to derive: which +/// registers and stack slots hold the arguments at the call, and what the +/// caller put in them. +#[derive(Debug, Clone, Default)] +pub struct CallSite { + /// The address the call returns to, which is what DWARF keys a call site + /// by. Absent only for a tail call that never returns here. + pub return_pc: Option, + /// The address of the call instruction, when the producer recorded one. + pub call_pc: Option, + /// The callee's entry address, when the entry it names has one. + pub target: Option, + /// The callee's name, when the entry names a callee at all. + pub target_name: Option, + /// Where the callee is found at run time, for an indirect call. + pub target_location: Option, + /// True when the call is a tail call, so control does not come back. + pub tail_call: bool, + /// What the caller puts where, one per argument the producer described. + pub parameters: Vec, +} + +/// One argument of a call, as the compiler described it. +#[derive(Debug, Clone, Default)] +pub struct CallSiteParameter { + /// Where the callee reads the argument from: a register, or a slot. + pub location: Option, + /// The value the caller passes, as far as the expression says. + pub value: Option, + /// The same value expressed so the callee can still recover it after the + /// call has clobbered the register: `DW_AT_call_data_value`. + pub data_value: Option, +} + +/// Where a value lives, as far as a location expression says. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Location { + /// In a DWARF register number, which is architecture specific. + Register(u16), + /// At an offset from the frame base. + FrameOffset(i64), + /// At an offset from a register's value. + RegisterOffset(u16, i64), + /// At a fixed address. + Address(Addr), + /// A constant: the value itself, not storage holding it. + Constant(i64), + /// An expression this does not model, kept as the file spells it so a + /// caller with a full evaluator can still use it. + Expression(Vec), +} + +/// Where a value lives over one range of the program counter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LocationRange { + /// The addresses this entry applies to. + pub range: AddrRange, + /// Where the value is while the program counter is in that range. + pub location: Location, +} + +/// A local variable with a known frame offset. +#[derive(Debug, Clone)] +pub struct DebugLocal { + /// Its name. + pub name: String, + /// Its type. + pub ty: TypeId, + /// Offset from the frame base, when the location was a simple one. + pub frame_offset: Option, + /// Where it lives, when one expression covers the whole function. + pub location: Option, + /// Where it lives per range of the program counter, from a location list. + /// At -O2 a variable that is in a register for part of a function and on + /// the stack for the rest is the normal case, and this is the only honest + /// answer for it; `location` is then `None`. + pub locations: Vec, +} + +impl Default for DebugLocal { + fn default() -> DebugLocal { + DebugLocal { + name: String::new(), + ty: Types::VOID, + frame_offset: None, + location: None, + locations: Vec::new(), + } + } +} + +impl DebugLocal { + /// Where the variable is while the program counter is at `pc`. + /// + /// A location list wins over a single location when there is one, because + /// a single location on a variable that also has a list would be a + /// contradiction rather than a default. + pub fn location_at(&self, pc: Addr) -> Option<&Location> { + if let Some(e) = self.locations.iter().find(|e| e.range.contains(pc)) { + return Some(&e.location); + } + if self.locations.is_empty() { + return self.location.as_ref(); + } + None + } +} + +/// A variable at a fixed address. +#[derive(Debug, Clone)] +pub struct DebugVariable { + /// Its name. + pub name: String, + /// Its type. + pub ty: TypeId, + /// Where it lives. + pub addr: Addr, +} + +/// One row of the line table. +#[derive(Debug, Clone)] +pub struct LineRow { + /// The address this row starts at. + pub addr: Addr, + /// The file, as an index into the unit's file names. + pub file: String, + /// The source line. + pub line: u32, + /// The source column. + pub column: u32, + /// True when this row marks the end of a sequence rather than code. + pub end: bool, + /// True when this address is a statement boundary. + pub statement: bool, +} + +/// The sections DWARF lives in. +#[derive(Debug, Clone, Copy, Default)] +pub struct Sections<'a> { + /// `.debug_info`. + pub info: &'a [u8], + /// `.debug_abbrev`. + pub abbrev: &'a [u8], + /// `.debug_str`. + pub str: &'a [u8], + /// `.debug_line_str`, DWARF 5. + pub line_str: &'a [u8], + /// `.debug_str_offsets`, DWARF 5. + pub str_offsets: &'a [u8], + /// `.debug_addr`, DWARF 5. + pub addr: &'a [u8], + /// `.debug_rnglists`, DWARF 5. + pub rnglists: &'a [u8], + /// `.debug_ranges`, DWARF 4. + pub ranges: &'a [u8], + /// `.debug_loclists`, DWARF 5. + pub loclists: &'a [u8], + /// `.debug_loc`, DWARF 4. + pub loc: &'a [u8], + /// `.debug_line`. + pub line: &'a [u8], +} + +impl Sections<'_> { + /// True when there is nothing to read. + pub fn is_empty(&self) -> bool { + self.info.is_empty() && self.line.is_empty() + } +} + +/// Parse what the sections carry. +pub fn parse(sections: &Sections<'_>, endian: Endian) -> DebugInfo { + let mut out = DebugInfo { + types: Types::new(), + ..Default::default() + }; + if sections.is_empty() { + return out; + } + let mut reader = Cur::new(sections.info, endian); + // A malformed unit header stops the walk rather than resynchronizing on a + // guess, because a wrong guess produces confident nonsense. A unit that + // does not advance the cursor stops it too: a length field is a number + // from the file and can say anything, including nothing. + while !reader.is_empty() { + let before = reader.position(); + match unit(&mut reader, sections, endian, &mut out) { + Some(()) => out.units += 1, + None => break, + } + if reader.position() <= before { + break; + } + } + if !sections.line.is_empty() { + out.lines = lines(sections, endian); + out.lines.sort_by_key(|r| r.addr); + } + out +} + +/// One compilation unit. +fn unit( + reader: &mut Cur<'_>, + sections: &Sections<'_>, + endian: Endian, + out: &mut DebugInfo, +) -> Option<()> { + let start = reader.position(); + let mut length = reader.u32()? as u64; + let sixty_four = length == 0xffff_ffff; + if sixty_four { + length = reader.u64()?; + } + // The length is from the file: a unit that claims to run past the end of + // the section is malformed, and believing it would walk off the data. + let end = reader + .position() + .checked_add(length as usize) + .filter(|e| *e <= sections.info.len() && *e > reader.position())?; + let version = reader.u16()?; + if !(2..=5).contains(&version) { + return None; + } + + // DWARF 5 moved the address size and put a unit type first. + let (address_size, abbrev_offset) = if version >= 5 { + let _unit_type = reader.u8()?; + let address_size = reader.u8()?; + let abbrev_offset = offset(reader, sixty_four)?; + (address_size, abbrev_offset) + } else { + let abbrev_offset = offset(reader, sixty_four)?; + let address_size = reader.u8()?; + (address_size, abbrev_offset) + }; + + let abbrevs = abbreviations(sections.abbrev, abbrev_offset as usize)?; + let mut unit = Unit { + address_size, + sixty_four, + version, + base: start, + str_offsets_base: 8, + addr_base: 8, + // The default is the size of the list section's own header, which is + // what an index is measured from when the unit declares no base. + rnglists_base: if sixty_four { 20 } else { 12 }, + loclists_base: if sixty_four { 20 } else { 12 }, + low_pc: Addr::ZERO, + endian, + sections: *sections, + }; + + // Walk the tree, collecting every entry with its children's indices, so a + // type can be resolved by its offset without a second pass. + let mut entries: BTreeMap = BTreeMap::new(); + let mut stack: Vec = Vec::new(); + while reader.position() < end { + // One entry is at least one byte, so a unit cannot hold more entries + // than it has bytes; anything beyond that is a cursor going nowhere. + if entries.len() > length as usize { + return None; + } + let at = reader.position(); + let code = reader.uleb128()?; + if code == 0 { + stack.pop(); + continue; + } + let Some(abbrev) = abbrevs.get(&code) else { + return Some(()); // an abbreviation this unit did not declare + }; + let mut entry = Entry { + tag: abbrev.tag, + parent: stack.last().copied(), + attributes: Vec::new(), + }; + for (attribute, form, implicit) in &abbrev.attributes { + let value = read_form(reader, *form, *implicit, &mut unit)?; + entry.attributes.push((*attribute, value)); + } + if abbrev.children { + stack.push(at); + } + entries.insert(at, entry); + } + + // The bases DWARF 5 indirects through live on the unit's own entry, and so + // does the base address that a range or location pair is measured from. + let mut stmt_list = None; + if let Some((_, root)) = entries.iter().next() { + if let Some(Value::Unsigned(v)) = root.get(DW_AT_STR_OFFSETS_BASE) { + unit.str_offsets_base = *v as usize; + } + if let Some(Value::Unsigned(v)) = root.get(DW_AT_ADDR_BASE) { + unit.addr_base = *v as usize; + } + if let Some(Value::Unsigned(v)) = root.get(DW_AT_RNGLISTS_BASE) { + unit.rnglists_base = *v as usize; + } + if let Some(Value::Unsigned(v)) = root.get(DW_AT_LOCLISTS_BASE) { + unit.loclists_base = *v as usize; + } + if let Some(a) = root.addr_of(DW_AT_LOW_PC) { + unit.low_pc = a; + } + stmt_list = root.unsigned(DW_AT_STMT_LIST); + } + + // Children by parent, built once. Every walk below wants them, and a scan + // of the whole unit per entry is what makes a large unit quadratic. + let mut children: BTreeMap> = BTreeMap::new(); + for (at, entry) in &entries { + if let Some(parent) = entry.parent { + children.entry(parent).or_default().push(*at); + } + } + + // `DW_AT_decl_file` and `DW_AT_call_file` index the file table of this + // unit's own line program, so it has to be read before either can be a name. + let files = stmt_list + .and_then(|at| file_table(sections, endian, at as usize)) + .unwrap_or_default(); + + let mut resolver = Resolver { + entries: &entries, + children: &children, + files: &files, + types: &mut out.types, + made: BTreeMap::new(), + unit: &unit, + }; + + // Functions and globals. + let mut functions = Vec::new(); + let mut variables = Vec::new(); + for (at, entry) in &entries { + match entry.tag { + DW_TAG_SUBPROGRAM => { + if let Some(f) = resolver.function(*at, entry) { + functions.push(f); + } + } + DW_TAG_VARIABLE if entry.parent.is_none_or(|p| is_unit(&entries, p)) => { + if let Some(v) = resolver.variable(entry) { + variables.push(v); + } + } + _ => {} + } + } + for f in functions { + out.functions.insert(f.low_pc, f); + } + for v in variables { + out.variables.insert(v.addr, v); + } + + reader.seek(end); + Some(()) +} + +fn is_unit(entries: &BTreeMap, at: usize) -> bool { + entries.get(&at).map(|e| e.tag) == Some(DW_TAG_COMPILE_UNIT) +} + +/// One debugging information entry. +#[derive(Debug, Clone)] +struct Entry { + tag: u64, + parent: Option, + attributes: Vec<(u64, Value)>, +} + +impl Entry { + fn get(&self, attribute: u64) -> Option<&Value> { + self.attributes + .iter() + .find(|(a, _)| *a == attribute) + .map(|(_, v)| v) + } + + fn unsigned(&self, attribute: u64) -> Option { + match self.get(attribute)? { + Value::Unsigned(v) => Some(*v), + Value::Signed(v) => Some(*v as u64), + Value::Address(a) => Some(a.get()), + _ => None, + } + } + + fn string(&self) -> Option<&str> { + match self.get(DW_AT_NAME)? { + Value::String(s) => Some(s), + _ => None, + } + } + + /// An attribute that names an address, in either of the two forms a + /// producer writes one. + fn addr_of(&self, attribute: u64) -> Option { + match self.get(attribute)? { + Value::Address(a) => Some(*a), + Value::Unsigned(v) => Some(Addr(*v)), + _ => None, + } + } + + fn reference(&self, attribute: u64) -> Option { + match self.get(attribute)? { + Value::Reference(r) => Some(*r), + _ => None, + } + } +} + +/// An attribute's value. +#[derive(Debug, Clone)] +enum Value { + Unsigned(u64), + Signed(i64), + Address(Addr), + String(String), + Reference(usize), + Block(Vec), + Flag(#[allow(dead_code)] bool), + /// An index into a unit's range or location list offset table, which is a + /// different thing from a section offset and cannot be told apart later. + ListIndex(u64), +} + +struct Unit<'a> { + address_size: u8, + sixty_four: bool, + version: u16, + base: usize, + str_offsets_base: usize, + addr_base: usize, + rnglists_base: usize, + loclists_base: usize, + /// The unit's own `DW_AT_low_pc`, which is what an offset pair in a range + /// or location list is measured from until an entry says otherwise. + low_pc: Addr, + endian: Endian, + sections: Sections<'a>, +} + +/// Build C types from the entries that describe them. +struct Resolver<'a> { + entries: &'a BTreeMap, + children: &'a BTreeMap>, + files: &'a [String], + types: &'a mut Types, + made: BTreeMap, + unit: &'a Unit<'a>, +} + +impl Resolver<'_> { + fn function(&mut self, at: usize, entry: &Entry) -> Option { + // A function need not be contiguous: `DW_AT_ranges` describes one that + // the compiler split, and its lowest range is then where it starts. + let ranges = self.ranges_of(entry); + let low = match entry.addr_of(DW_AT_LOW_PC) { + // A relocatable object legitimately puts a function at offset + // zero, so zero is an address here and not a missing one. + Some(a) => a, + // A function split into a hot and a cold piece has ranges and an + // entry point, and the lowest range is the cold piece as often as + // not, so the declared entry wins over it. + None => match entry.addr_of(DW_AT_ENTRY_PC) { + Some(a) => a, + None => ranges.iter().map(|r| r.start()).min()?, + }, + }; + // `high_pc` is an address in DWARF 2 and 3 and a length in 4 and 5, + // told apart by the form class rather than by the version. + let size = match entry.get(DW_AT_HIGH_PC) { + Some(Value::Address(a)) => Some(a.get().saturating_sub(low.get())), + Some(Value::Unsigned(v)) => Some(*v), + _ => None, + }; + let returns = entry.reference(DW_AT_TYPE).and_then(|r| self.type_at(r, 0)); + let mut signature = Signature { + returns, + ..Default::default() + }; + let mut locals = Vec::new(); + for child_at in self.kids(at) { + let Some(child) = self.entries.get(&child_at).cloned() else { + continue; + }; + match child.tag { + DW_TAG_FORMAL_PARAMETER => { + let ty = child + .reference(DW_AT_TYPE) + .and_then(|r| self.type_at(r, 0)) + .unwrap_or(Types::VOID); + signature + .parameters + .push((child.string().map(str::to_string), ty)); + if let Some(local) = self.local(&child) { + locals.push(local); + } + } + DW_TAG_UNSPECIFIED_PARAMETERS => signature.varargs = true, + DW_TAG_VARIABLE => { + if let Some(local) = self.local(&child) { + locals.push(local); + } + } + _ => {} + } + } + let (inlines, call_sites) = self.subtree(at); + Some(DebugFunction { + name: entry.string().unwrap_or_default().to_string(), + linkage_name: match entry.get(DW_AT_LINKAGE_NAME) { + Some(Value::String(s)) => Some(s.clone()), + _ => None, + }, + low_pc: low, + size, + signature, + locals, + decl_file: self.file(entry.unsigned(DW_AT_DECL_FILE)), + decl_line: entry.unsigned(DW_AT_DECL_LINE), + inlined: entry.get(DW_AT_INLINE).is_some(), + ranges, + inlines, + call_sites, + }) + } + + /// One named variable or parameter, with whatever its location says. + fn local(&mut self, entry: &Entry) -> Option { + // A parameter of an inlined call carries nothing of its own but a + // reference to the abstract copy, which is where its name and its + // type live. Without following that, an inlined frame has anonymous + // arguments, which is most of what makes the frame worth having. + let origin = entry.reference(DW_AT_ABSTRACT_ORIGIN); + let name = match entry.string() { + Some(n) => n.to_string(), + None => match self.origin_names(origin) { + (n, _) if !n.is_empty() => n, + _ => return None, + }, + }; + let ty = entry + .reference(DW_AT_TYPE) + .or_else(|| self.entries.get(&origin?)?.reference(DW_AT_TYPE)) + .and_then(|r| self.type_at(r, 0)) + .unwrap_or(Types::VOID); + let value = entry.get(DW_AT_LOCATION); + let (location, locations) = self.locations_of(value); + Some(DebugLocal { + name, + ty, + frame_offset: frame_offset(value), + location, + locations, + }) + } + + /// The children of an entry, in the order the file wrote them. + fn kids(&self, at: usize) -> Vec { + self.children.get(&at).cloned().unwrap_or_default() + } + + /// Every inlined frame and call site under a subprogram. + /// + /// Both nest: a call site sits inside the inlined body it was made from, + /// and an inlined body inside another. The walk is iterative because the + /// nesting depth is a number from the file. + fn subtree(&mut self, root: usize) -> (Vec, Vec) { + // (entry, inline depth). Reversed on push so siblings come out in + // document order, which is what makes the result deterministic. + let mut stack: Vec<(usize, u32)> = vec![(root, 0)]; + let mut found: Vec<(usize, u64, u32)> = Vec::new(); + let mut visited = 0usize; + while let Some((at, depth)) = stack.pop() { + // The tree cannot have more nodes than the unit has entries, so a + // parent chain that somehow loops still terminates here. + visited += 1; + if visited > self.entries.len() { + break; + } + for child_at in self.kids(at).into_iter().rev() { + let Some(tag) = self.entries.get(&child_at).map(|e| e.tag) else { + continue; + }; + match tag { + // A nested function is a function of its own, and its call + // sites belong to it rather than to this one. + DW_TAG_SUBPROGRAM => continue, + DW_TAG_INLINED_SUBROUTINE => { + found.push((child_at, tag, depth)); + stack.push((child_at, depth + 1)); + } + DW_TAG_CALL_SITE | DW_TAG_GNU_CALL_SITE => { + found.push((child_at, tag, depth)); + } + _ => stack.push((child_at, depth)), + } + } + } + + let mut inlines = Vec::new(); + let mut call_sites = Vec::new(); + for (at, tag, depth) in found { + let Some(entry) = self.entries.get(&at).cloned() else { + continue; + }; + if tag == DW_TAG_INLINED_SUBROUTINE { + if let Some(f) = self.inlined(at, &entry, depth) { + inlines.push(f); + } + } else if let Some(c) = self.call_site(at, &entry) { + call_sites.push(c); + } + } + inlines.sort_by_key(|i| (i.low_pc().unwrap_or(Addr::MAX), i.depth)); + call_sites.sort_by_key(|c| (c.return_pc.unwrap_or(Addr::MAX), c.call_pc)); + (inlines, call_sites) + } + + /// One `DW_TAG_inlined_subroutine`. + fn inlined(&mut self, at: usize, entry: &Entry, depth: u32) -> Option { + let mut ranges = self.ranges_of(entry); + if ranges.is_empty() { + // The contiguous form, which is what a producer writes when the + // inlined body was not split. + let low = entry.addr_of(DW_AT_LOW_PC)?; + let end = match entry.get(DW_AT_HIGH_PC) { + Some(Value::Address(a)) => *a, + Some(Value::Unsigned(v)) => low.checked_add(*v)?, + _ => return None, + }; + ranges.push(AddrRange::new(low, end)?); + } + let origin = entry.reference(DW_AT_ABSTRACT_ORIGIN); + let (name, linkage_name) = self.origin_names(origin); + let mut locals = Vec::new(); + for child_at in self.kids(at) { + let Some(child) = self.entries.get(&child_at).cloned() else { + continue; + }; + if matches!(child.tag, DW_TAG_FORMAL_PARAMETER | DW_TAG_VARIABLE) { + if let Some(local) = self.local(&child) { + locals.push(local); + } + } + } + Some(InlinedFrame { + name, + linkage_name, + abstract_origin: origin.map(|r| r as u64), + call_file: self.file(entry.unsigned(DW_AT_CALL_FILE)), + call_line: entry.unsigned(DW_AT_CALL_LINE).map(|v| v as u32), + call_column: entry.unsigned(DW_AT_CALL_COLUMN).map(|v| v as u32), + entry_pc: entry.addr_of(DW_AT_ENTRY_PC), + ranges, + depth, + locals, + }) + } + + /// One `DW_TAG_call_site`, or the GNU spelling of the same thing. + fn call_site(&mut self, at: usize, entry: &Entry) -> Option { + // The GNU form predates the standard one and puts the return address + // in `DW_AT_low_pc` rather than in an attribute of its own. + let gnu = entry.tag == DW_TAG_GNU_CALL_SITE; + let return_pc = entry + .addr_of(DW_AT_CALL_RETURN_PC) + .or_else(|| gnu.then(|| entry.addr_of(DW_AT_LOW_PC)).flatten()); + let call_pc = entry.addr_of(DW_AT_CALL_PC); + if return_pc.is_none() && call_pc.is_none() { + // Without an address the entry says nothing an analysis can use. + return None; + } + let origin = entry + .reference(DW_AT_CALL_ORIGIN) + .or_else(|| entry.reference(DW_AT_ABSTRACT_ORIGIN)); + let (name, _) = self.origin_names(origin); + let target = origin.and_then(|r| self.entries.get(&r)?.addr_of(DW_AT_LOW_PC)); + let target_location = self + .single_location(entry.get(DW_AT_CALL_TARGET)) + .or_else(|| self.single_location(entry.get(DW_AT_GNU_CALL_SITE_TARGET))); + let tail_call = + entry.get(DW_AT_CALL_TAIL_CALL).is_some() || entry.get(DW_AT_GNU_TAIL_CALL).is_some(); + + let mut parameters = Vec::new(); + for child_at in self.kids(at) { + let Some(child) = self.entries.get(&child_at) else { + continue; + }; + if !matches!( + child.tag, + DW_TAG_CALL_SITE_PARAMETER | DW_TAG_GNU_CALL_SITE_PARAMETER + ) { + continue; + } + parameters.push(CallSiteParameter { + location: self.single_location(child.get(DW_AT_LOCATION)), + value: self + .single_location(child.get(DW_AT_CALL_VALUE)) + .or_else(|| self.single_location(child.get(DW_AT_GNU_CALL_SITE_VALUE))), + data_value: self + .single_location(child.get(DW_AT_CALL_DATA_VALUE)) + .or_else(|| self.single_location(child.get(DW_AT_GNU_CALL_SITE_DATA_VALUE))), + }); + } + Some(CallSite { + return_pc, + call_pc, + target, + target_name: (!name.is_empty()).then_some(name), + target_location, + tail_call, + parameters, + }) + } + + /// The name and linkage name of the entry a reference points at, following + /// the chain an abstract instance or a declaration adds. + fn origin_names(&self, origin: Option) -> (String, Option) { + let mut at = origin; + let mut name = String::new(); + let mut linkage = None; + // Eight hops: a chain longer than that is a file playing games. + for _ in 0..8 { + let Some(entry) = at.and_then(|r| self.entries.get(&r)) else { + break; + }; + if name.is_empty() { + if let Some(n) = entry.string() { + name = n.to_string(); + } + } + if linkage.is_none() { + if let Some(Value::String(s)) = entry.get(DW_AT_LINKAGE_NAME) { + linkage = Some(s.clone()); + } + } + if !name.is_empty() && linkage.is_some() { + break; + } + at = entry + .reference(DW_AT_ABSTRACT_ORIGIN) + .or_else(|| entry.reference(DW_AT_SPECIFICATION)); + } + (name, linkage) + } + + /// A file index as the unit's line program spells it. + fn file(&self, index: Option) -> Option { + let name = self.files.get(usize::try_from(index?).ok()?)?; + (!name.is_empty()).then(|| name.clone()) + } + + /// A location attribute that is one expression rather than a list. + fn single_location(&self, value: Option<&Value>) -> Option { + match value? { + Value::Block(bytes) => decode_location(bytes, self.unit), + _ => None, + } + } + + /// A location attribute in either shape: one expression, or a list keyed + /// by program counter. + fn locations_of(&self, value: Option<&Value>) -> (Option, Vec) { + match value { + Some(Value::Block(bytes)) => (decode_location(bytes, self.unit), Vec::new()), + Some(Value::Unsigned(at)) => (None, location_list(self.unit, *at as usize)), + Some(Value::ListIndex(i)) => ( + None, + match list_offset(self.unit, *i, true) { + Some(at) => location_list(self.unit, at), + None => Vec::new(), + }, + ), + _ => (None, Vec::new()), + } + } + + /// The address ranges an entry's `DW_AT_ranges` names. + fn ranges_of(&self, entry: &Entry) -> Vec { + match entry.get(DW_AT_RANGES) { + Some(Value::Unsigned(at)) => range_list(self.unit, *at as usize), + Some(Value::ListIndex(i)) => match list_offset(self.unit, *i, false) { + Some(at) => range_list(self.unit, at), + None => Vec::new(), + }, + _ => Vec::new(), + } + } + + fn variable(&mut self, entry: &Entry) -> Option { + let name = entry.string()?.to_string(); + let ty = entry + .reference(DW_AT_TYPE) + .and_then(|r| self.type_at(r, 0)) + .unwrap_or(Types::VOID); + let addr = static_address(entry.get(DW_AT_LOCATION), self.unit)?; + Some(DebugVariable { name, ty, addr }) + } + + /// The C type an entry describes, built once and remembered. + fn type_at(&mut self, at: usize, depth: u32) -> Option { + if depth > 32 { + return None; + } + if let Some(id) = self.made.get(&at) { + return Some(*id); + } + let entry = self.entries.get(&at)?.clone(); + let name = entry.string().map(str::to_string); + let id = match entry.tag { + DW_TAG_BASE_TYPE => { + let size = entry.unsigned(DW_AT_BYTE_SIZE).unwrap_or(4) as u8; + let encoding = entry.unsigned(DW_AT_ENCODING).unwrap_or(DW_ATE_SIGNED); + self.types.add(match encoding { + DW_ATE_BOOLEAN => Type::Bool, + DW_ATE_FLOAT => Type::Float { size }, + DW_ATE_SIGNED | DW_ATE_SIGNED_CHAR => Type::Int { size, signed: true }, + _ => Type::Int { + size, + signed: false, + }, + }) + } + DW_TAG_POINTER_TYPE => { + let inner = entry + .reference(DW_AT_TYPE) + .and_then(|r| self.type_at(r, depth + 1)) + .unwrap_or(Types::VOID); + self.types.add(Type::Pointer(inner)) + } + // Qualifiers do not change the layout, and carrying them through + // the decompiler would clutter every declaration. + DW_TAG_CONST_TYPE | DW_TAG_VOLATILE_TYPE | DW_TAG_RESTRICT_TYPE => { + match entry.reference(DW_AT_TYPE) { + Some(r) => self.type_at(r, depth + 1).unwrap_or(Types::VOID), + None => Types::VOID, + } + } + DW_TAG_TYPEDEF => { + let inner = entry + .reference(DW_AT_TYPE) + .and_then(|r| self.type_at(r, depth + 1)) + .unwrap_or(Types::VOID); + match name { + Some(n) => self.types.add(Type::Typedef(n, inner)), + None => inner, + } + } + DW_TAG_ARRAY_TYPE => { + let inner = entry + .reference(DW_AT_TYPE) + .and_then(|r| self.type_at(r, depth + 1)) + .unwrap_or(Types::VOID); + // The count lives on a subrange child, as either a count or an + // upper bound. + let count = self + .kids(at) + .into_iter() + .filter_map(|k| self.entries.get(&k)) + .find(|c| c.tag == DW_TAG_SUBRANGE_TYPE) + .and_then(|c| { + c.unsigned(DW_AT_COUNT) + .or_else(|| c.unsigned(DW_AT_UPPER_BOUND).map(|n| n + 1)) + }); + self.types.add(Type::Array(inner, count)) + } + DW_TAG_STRUCTURE_TYPE | DW_TAG_UNION_TYPE | DW_TAG_CLASS_TYPE => { + // Reserved first, so a field pointing back at this structure + // finds it instead of recursing forever. + let placeholder = self.types.reserve(name.as_deref().unwrap_or("anonymous")); + self.made.insert(at, placeholder); + let mut fields = Vec::new(); + let children: Vec = self + .kids(at) + .into_iter() + .filter_map(|k| self.entries.get(&k)) + .filter(|c| c.tag == DW_TAG_MEMBER) + .cloned() + .collect(); + for child in children { + let ty = child + .reference(DW_AT_TYPE) + .and_then(|r| self.type_at(r, depth + 1)) + .unwrap_or(Types::VOID); + fields.push(Field { + name: child.string().unwrap_or("field").to_string(), + ty, + offset: child.unsigned(DW_AT_DATA_MEMBER_LOCATION).unwrap_or(0), + bits: child.unsigned(DW_AT_BIT_SIZE).map(|b| b as u8), + }); + } + fields.sort_by_key(|f| f.offset); + let composite = Type::Composite(Composite { + name, + union: entry.tag == DW_TAG_UNION_TYPE, + size: entry.unsigned(DW_AT_BYTE_SIZE), + fields, + }); + self.types.define(placeholder, composite); + placeholder + } + DW_TAG_ENUMERATION_TYPE => { + let values: Vec<(String, i64)> = self + .kids(at) + .into_iter() + .filter_map(|k| self.entries.get(&k)) + .filter(|c| c.tag == DW_TAG_ENUMERATOR) + .filter_map(|c| { + Some(( + c.string()?.to_string(), + match c.get(DW_AT_CONST_VALUE)? { + Value::Signed(v) => *v, + Value::Unsigned(v) => *v as i64, + _ => return None, + }, + )) + }) + .collect(); + self.types.add(Type::Enum(Enumeration { + name, + size: entry.unsigned(DW_AT_BYTE_SIZE).unwrap_or(4) as u8, + values, + })) + } + DW_TAG_SUBROUTINE_TYPE => { + let returns = entry + .reference(DW_AT_TYPE) + .and_then(|r| self.type_at(r, depth + 1)); + let mut signature = Signature { + returns, + ..Default::default() + }; + let children: Vec = self + .kids(at) + .into_iter() + .filter_map(|k| self.entries.get(&k)) + .cloned() + .collect(); + for child in children { + match child.tag { + DW_TAG_FORMAL_PARAMETER => { + let ty = child + .reference(DW_AT_TYPE) + .and_then(|r| self.type_at(r, depth + 1)) + .unwrap_or(Types::VOID); + signature.parameters.push((None, ty)); + } + DW_TAG_UNSPECIFIED_PARAMETERS => signature.varargs = true, + _ => {} + } + } + self.types.add(Type::Function(signature)) + } + _ => return None, + }; + self.made.insert(at, id); + Some(id) + } +} + +/// A location expression that is just a frame offset. +fn frame_offset(value: Option<&Value>) -> Option { + let Value::Block(bytes) = value? else { + return None; + }; + // `DW_OP_fbreg ` and nothing else: anything longer is a location + // this does not model. + if bytes.first() != Some(&DW_OP_FBREG) { + return None; + } + let mut reader = Cur::new(&bytes[1..], Endian::Little); + let offset = reader.sleb128()?; + reader.is_empty().then_some(offset) +} + +/// A location expression that is a fixed address. +fn static_address(value: Option<&Value>, unit: &Unit<'_>) -> Option { + let Value::Block(bytes) = value? else { + return None; + }; + match bytes.first() { + Some(&DW_OP_ADDR) => { + let mut reader = Cur::new(&bytes[1..], unit.endian); + Some(Addr(if unit.address_size == 4 { + reader.u32()? as u64 + } else { + reader.u64()? + })) + } + Some(&DW_OP_ADDRX) => { + let mut reader = Cur::new(&bytes[1..], unit.endian); + let index = reader.uleb128()?; + address_from_table(unit, index) + } + _ => None, + } +} + +fn address_from_table(unit: &Unit<'_>, index: u64) -> Option { + let size = unit.address_size.max(1) as usize; + // Checked: `index` comes out of the file, so a corrupt one multiplies + // past `usize` and lands the read somewhere it was never meant to be. + let at = (index as usize) + .checked_mul(size) + .and_then(|off| unit.addr_base.checked_add(off))?; + let bytes = unit.sections.addr.get(at..at.checked_add(size)?)?; + let mut reader = Cur::new(bytes, unit.endian); + Some(Addr(if size == 4 { + reader.u32()? as u64 + } else { + reader.u64()? + })) +} + +/// One abbreviation: what an entry's tag and attributes are. +#[derive(Debug, Clone)] +struct Abbreviation { + tag: u64, + children: bool, + attributes: Vec<(u64, u64, i64)>, +} + +fn abbreviations(data: &[u8], at: usize) -> Option> { + let rest = data.get(at..)?; + let mut reader = Cur::new(rest, Endian::Little); + let mut out = BTreeMap::new(); + loop { + // Every declaration costs bytes, so there cannot be more of them than + // the table has. + if out.len() > rest.len() { + return None; + } + let code = reader.uleb128()?; + if code == 0 { + break; + } + let tag = reader.uleb128()?; + let children = reader.u8()? != 0; + let mut attributes = Vec::new(); + loop { + if attributes.len() > rest.len() { + return None; + } + let attribute = reader.uleb128()?; + let form = reader.uleb128()?; + // `implicit_const` carries its value in the abbreviation itself. + let implicit = if form == DW_FORM_IMPLICIT_CONST { + reader.sleb128()? + } else { + 0 + }; + if attribute == 0 && form == 0 { + break; + } + attributes.push((attribute, form, implicit)); + } + out.insert( + code, + Abbreviation { + tag, + children, + attributes, + }, + ); + } + Some(out) +} + +fn offset(reader: &mut Cur<'_>, sixty_four: bool) -> Option { + if sixty_four { + reader.u64() + } else { + reader.u32().map(|v| v as u64) + } +} + +/// Read one attribute value in its form. +fn read_form(reader: &mut Cur<'_>, form: u64, implicit: i64, unit: &mut Unit<'_>) -> Option { + Some(match form { + DW_FORM_ADDR => Value::Address(Addr(match unit.address_size { + 4 => reader.u32()? as u64, + _ => reader.u64()?, + })), + DW_FORM_BLOCK1 => { + let n = reader.u8()? as usize; + Value::Block(reader.bytes(n)?.to_vec()) + } + DW_FORM_BLOCK2 => { + let n = reader.u16()? as usize; + Value::Block(reader.bytes(n)?.to_vec()) + } + DW_FORM_BLOCK4 => { + let n = reader.u32()? as usize; + Value::Block(reader.bytes(n)?.to_vec()) + } + DW_FORM_BLOCK | DW_FORM_EXPRLOC => { + let n = reader.uleb128()? as usize; + Value::Block(reader.bytes(n)?.to_vec()) + } + DW_FORM_DATA1 => Value::Unsigned(reader.u8()? as u64), + DW_FORM_DATA2 => Value::Unsigned(reader.u16()? as u64), + DW_FORM_DATA4 => Value::Unsigned(reader.u32()? as u64), + DW_FORM_DATA8 => Value::Unsigned(reader.u64()?), + DW_FORM_DATA16 => Value::Block(reader.bytes(16)?.to_vec()), + DW_FORM_SDATA => Value::Signed(reader.sleb128()?), + DW_FORM_UDATA => Value::Unsigned(reader.uleb128()?), + DW_FORM_STRING => Value::String(reader.cstr()?), + DW_FORM_STRP => { + let at = offset(reader, unit.sixty_four)? as usize; + Value::String(string_at(unit.sections.str, at)) + } + DW_FORM_LINE_STRP => { + let at = offset(reader, unit.sixty_four)? as usize; + Value::String(string_at(unit.sections.line_str, at)) + } + DW_FORM_STRX | DW_FORM_STRX1 | DW_FORM_STRX2 | DW_FORM_STRX3 | DW_FORM_STRX4 => { + let index = match form { + DW_FORM_STRX1 => reader.u8()? as u64, + DW_FORM_STRX2 => reader.u16()? as u64, + DW_FORM_STRX3 => { + let b = reader.bytes(3)?; + (b[0] as u64) | ((b[1] as u64) << 8) | ((b[2] as u64) << 16) + } + DW_FORM_STRX4 => reader.u32()? as u64, + _ => reader.uleb128()?, + }; + Value::String(indexed_string(unit, index)) + } + DW_FORM_ADDRX | DW_FORM_ADDRX1 | DW_FORM_ADDRX2 | DW_FORM_ADDRX3 | DW_FORM_ADDRX4 => { + let index = match form { + DW_FORM_ADDRX1 => reader.u8()? as u64, + DW_FORM_ADDRX2 => reader.u16()? as u64, + DW_FORM_ADDRX3 => { + let b = reader.bytes(3)?; + (b[0] as u64) | ((b[1] as u64) << 8) | ((b[2] as u64) << 16) + } + DW_FORM_ADDRX4 => reader.u32()? as u64, + _ => reader.uleb128()?, + }; + Value::Address(address_from_table(unit, index).unwrap_or(Addr::ZERO)) + } + DW_FORM_REF1 => Value::Reference(unit.base + reader.u8()? as usize), + DW_FORM_REF2 => Value::Reference(unit.base + reader.u16()? as usize), + DW_FORM_REF4 => Value::Reference(unit.base + reader.u32()? as usize), + DW_FORM_REF8 => Value::Reference(unit.base + reader.u64()? as usize), + DW_FORM_REF_UDATA => Value::Reference(unit.base + reader.uleb128()? as usize), + DW_FORM_REF_ADDR => Value::Reference(offset(reader, unit.sixty_four)? as usize), + DW_FORM_SEC_OFFSET => Value::Unsigned(offset(reader, unit.sixty_four)?), + DW_FORM_FLAG => Value::Flag(reader.u8()? != 0), + DW_FORM_FLAG_PRESENT => Value::Flag(true), + DW_FORM_IMPLICIT_CONST => Value::Signed(implicit), + DW_FORM_INDIRECT => { + let actual = reader.uleb128()?; + return read_form(reader, actual, 0, unit); + } + DW_FORM_LOCLISTX | DW_FORM_RNGLISTX => Value::ListIndex(reader.uleb128()?), + DW_FORM_REF_SUP4 | DW_FORM_STRP_SUP => Value::Unsigned(offset(reader, unit.sixty_four)?), + // A form this does not know cannot be skipped by length, so the unit + // stops here rather than reading garbage as attributes. + _ => return None, + }) +} + +fn string_at(section: &[u8], at: usize) -> String { + let Some(rest) = section.get(at..) else { + return String::new(); + }; + let end = rest.iter().position(|b| *b == 0).unwrap_or(rest.len()); + String::from_utf8_lossy(&rest[..end]).into_owned() +} + +fn indexed_string(unit: &Unit<'_>, index: u64) -> String { + let size = if unit.sixty_four { 8 } else { 4 }; + let at = unit.str_offsets_base + index as usize * size; + let Some(bytes) = unit.sections.str_offsets.get(at..at + size) else { + return String::new(); + }; + let mut reader = Cur::new(bytes, unit.endian); + let offset = if unit.sixty_four { + reader.u64().unwrap_or(0) as usize + } else { + reader.u32().unwrap_or(0) as usize + }; + string_at(unit.sections.str, offset) +} + +/// Decode the line number program into rows. +fn lines(sections: &Sections<'_>, endian: Endian) -> Vec { + let mut out = Vec::new(); + let mut reader = Cur::new(sections.line, endian); + while !reader.is_empty() { + let before = reader.position(); + if line_program(&mut reader, sections, endian, &mut out).is_none() { + break; + } + if reader.position() <= before { + break; + } + } + out +} + +fn line_program( + reader: &mut Cur<'_>, + sections: &Sections<'_>, + endian: Endian, + out: &mut Vec, +) -> Option<()> { + let mut length = reader.u32()? as u64; + let sixty_four = length == 0xffff_ffff; + if sixty_four { + length = reader.u64()?; + } + let end = reader + .position() + .checked_add(length as usize) + .filter(|e| *e <= sections.line.len() && *e > reader.position())?; + let version = reader.u16()?; + if !(2..=5).contains(&version) { + reader.seek(end); + return Some(()); + } + if version >= 5 { + let _address_size = reader.u8()?; + let _segment_selector = reader.u8()?; + } + let header_length = offset(reader, sixty_four)?; + let program_start = reader.position() + header_length as usize; + + let minimum_instruction_length = reader.u8()?; + let max_ops = if version >= 4 { reader.u8()? } else { 1 }; + let _ = max_ops; + let default_is_stmt = reader.u8()? != 0; + let line_base = reader.u8()? as i8 as i64; + let line_range = reader.u8()?.max(1) as i64; + let opcode_base = reader.u8()?.max(1); + let mut standard_lengths = Vec::new(); + for _ in 1..opcode_base { + standard_lengths.push(reader.u8()?); + } + + let files = if version >= 5 { + file_names_v5(reader, sections, endian, sixty_four)? + } else { + file_names_v4(reader)? + }; + + reader.seek(program_start); + let mut state = LineState::new(default_is_stmt); + let rows_before = out.len(); + while reader.position() < end { + // Every opcode costs a byte and at most one row, so a program cannot + // emit more rows than it has bytes. + if out.len() - rows_before > length as usize { + return None; + } + let opcode = reader.u8()?; + if opcode == 0 { + // Extended. + let size = reader.uleb128()? as usize; + let at = reader.position(); + let sub = reader.u8()?; + match sub { + DW_LNE_END_SEQUENCE => { + state.end = true; + emit(&mut state, &files, out); + state = LineState::new(default_is_stmt); + } + // `size` counts the sub-opcode byte, so it cannot be zero in a + // well-formed program; `reader.seek` below resynchronises + // whatever this arm does, so a corrupt one is skipped rather + // than ending the program. + DW_LNE_SET_ADDRESS if size >= 1 => { + let remaining = size - 1; + state.address = if remaining == 4 { + reader.u32()? as u64 + } else { + reader.u64()? + }; + } + _ => {} + } + reader.seek(at + size); + } else if opcode < opcode_base { + match opcode { + DW_LNS_COPY => emit(&mut state, &files, out), + DW_LNS_ADVANCE_PC => { + let n = reader.uleb128()?; + state.address = state + .address + .saturating_add(n.saturating_mul(minimum_instruction_length.max(1) as u64)); + } + DW_LNS_ADVANCE_LINE => { + let n = reader.sleb128()?; + state.line = state.line.saturating_add_signed(n as i32 as i64 as i32); + } + DW_LNS_SET_FILE => state.file = reader.uleb128()?, + DW_LNS_SET_COLUMN => state.column = reader.uleb128()? as u32, + DW_LNS_NEGATE_STMT => state.statement = !state.statement, + DW_LNS_CONST_ADD_PC => { + let adjusted = (255 - opcode_base) as i64; + state.address = state.address.saturating_add( + (adjusted / line_range) as u64 * minimum_instruction_length.max(1) as u64, + ); + } + DW_LNS_FIXED_ADVANCE_PC => { + state.address = state.address.saturating_add(reader.u16()? as u64) + } + _ => { + // An opcode this does not model still declares how many + // arguments it takes. + let n = standard_lengths + .get(opcode as usize - 1) + .copied() + .unwrap_or(0); + for _ in 0..n { + reader.uleb128()?; + } + } + } + } else { + let adjusted = (opcode - opcode_base) as i64; + state.address = state.address.saturating_add( + (adjusted / line_range) as u64 * minimum_instruction_length.max(1) as u64, + ); + state.line = state + .line + .saturating_add_signed((line_base + adjusted % line_range) as i32); + emit(&mut state, &files, out); + } + } + reader.seek(end); + Some(()) +} + +fn file_names_v4(reader: &mut Cur<'_>) -> Option> { + // Include directories, which this does not need but has to step over. + let limit = reader.0.len(); + let mut seen = 0; + loop { + seen += 1; + if seen > limit { + return None; + } + let s = reader.cstr()?; + if s.is_empty() { + break; + } + } + // DWARF 4 numbers files from one, so index zero is a placeholder. + let mut files = vec![String::new()]; + loop { + let name = reader.cstr()?; + if name.is_empty() { + break; + } + reader.uleb128()?; // directory + reader.uleb128()?; // modification time + reader.uleb128()?; // length + files.push(name); + } + Some(files) +} + +fn file_names_v5( + reader: &mut Cur<'_>, + sections: &Sections<'_>, + endian: Endian, + sixty_four: bool, +) -> Option> { + // Two tables in the same shape: directories, then files. Only the second + // is wanted, but the first has to be stepped over entry by entry because + // its size is not declared. + let directories = entry_table(reader, sections, endian, sixty_four)?; + let files = entry_table(reader, sections, endian, sixty_four)?; + let _ = directories; + Some(files) +} + +/// One DWARF 5 entry table: a description of the fields, then the rows. +fn entry_table( + reader: &mut Cur<'_>, + sections: &Sections<'_>, + endian: Endian, + sixty_four: bool, +) -> Option> { + let format_count = reader.u8()?; + let mut formats = Vec::new(); + for _ in 0..format_count { + let content = reader.uleb128()?; + let form = reader.uleb128()?; + formats.push((content, form)); + } + let count = reader.uleb128()?; + // A count from the file is attacker-controlled; a table longer than the + // section could hold is a malformed file, not a big one. + if count > sections.line.len() as u64 { + return None; + } + let mut names = Vec::with_capacity(count as usize); + for _ in 0..count { + let mut name = String::new(); + for (content, form) in &formats { + let mut unit = Unit { + address_size: 8, + sixty_four, + version: 5, + base: 0, + str_offsets_base: 8, + addr_base: 8, + rnglists_base: 12, + loclists_base: 12, + low_pc: Addr::ZERO, + endian, + sections: *sections, + }; + let value = read_form(reader, *form, 0, &mut unit)?; + if *content == DW_LNCT_PATH { + if let Value::String(s) = value { + name = s; + } + } + } + names.push(name); + } + Some(names) +} + +struct LineState { + address: u64, + file: u64, + line: u32, + column: u32, + statement: bool, + end: bool, +} + +impl LineState { + fn new(default_is_stmt: bool) -> LineState { + LineState { + address: 0, + file: 1, + line: 1, + column: 0, + statement: default_is_stmt, + end: false, + } + } +} + +fn emit(state: &mut LineState, files: &[String], out: &mut Vec) { + out.push(LineRow { + addr: Addr(state.address), + file: files.get(state.file as usize).cloned().unwrap_or_default(), + line: state.line, + column: state.column, + end: state.end, + statement: state.statement, + }); +} + +/// An address in the unit's address size. +fn read_address(reader: &mut Cur<'_>, unit: &Unit<'_>) -> Option { + Some(Addr(if unit.address_size == 4 { + reader.u32()? as u64 + } else { + reader.u64()? + })) +} + +/// The section offset an index into a unit's range or location offset table +/// resolves to. +/// +/// DWARF 5 lets a unit reach its lists by index so the attribute stays small; +/// the table sits at the unit's base and its entries are measured from that +/// same base. +fn list_offset(unit: &Unit<'_>, index: u64, locations: bool) -> Option { + let (section, base) = if locations { + (unit.sections.loclists, unit.loclists_base) + } else { + (unit.sections.rnglists, unit.rnglists_base) + }; + let size = if unit.sixty_four { 8 } else { 4 }; + let at = base.checked_add(usize::try_from(index).ok()?.checked_mul(size)?)?; + let bytes = section.get(at..at.checked_add(size)?)?; + let mut reader = Cur::new(bytes, unit.endian); + let value = if unit.sixty_four { + reader.u64()? + } else { + reader.u32()? as u64 + }; + base.checked_add(usize::try_from(value).ok()?) +} + +/// The ranges a `DW_AT_ranges` offset names, in whichever section this DWARF +/// version keeps them in. +fn range_list(unit: &Unit<'_>, at: usize) -> Vec { + if unit.version >= 5 { + rnglists(unit, at) + } else { + debug_ranges(unit, at) + } +} + +/// A DWARF 5 `.debug_rnglists` list. +fn rnglists(unit: &Unit<'_>, at: usize) -> Vec { + let Some(rest) = unit.sections.rnglists.get(at..) else { + return Vec::new(); + }; + let mut reader = Cur::new(rest, unit.endian); + let mut base = unit.low_pc; + let mut out = Vec::new(); + // Every entry costs at least its one-byte kind, so a list cannot hold more + // entries than the bytes behind it. + for _ in 0..rest.len() { + let Some(kind) = reader.u8() else { break }; + let pair = match kind { + DW_RLE_END_OF_LIST => break, + DW_RLE_BASE_ADDRESSX => { + match reader.uleb128().and_then(|i| address_from_table(unit, i)) { + Some(a) => base = a, + None => break, + } + continue; + } + DW_RLE_BASE_ADDRESS => { + match read_address(&mut reader, unit) { + Some(a) => base = a, + None => break, + } + continue; + } + DW_RLE_STARTX_ENDX => { + let (Some(s), Some(e)) = (reader.uleb128(), reader.uleb128()) else { + break; + }; + match (address_from_table(unit, s), address_from_table(unit, e)) { + (Some(s), Some(e)) => (s, e), + _ => break, + } + } + DW_RLE_STARTX_LENGTH => { + let (Some(s), Some(n)) = (reader.uleb128(), reader.uleb128()) else { + break; + }; + match address_from_table(unit, s).and_then(|s| Some((s, s.checked_add(n)?))) { + Some(p) => p, + None => break, + } + } + DW_RLE_OFFSET_PAIR => { + let (Some(s), Some(e)) = (reader.uleb128(), reader.uleb128()) else { + break; + }; + match (base.checked_add(s), base.checked_add(e)) { + (Some(s), Some(e)) => (s, e), + _ => break, + } + } + DW_RLE_START_END => { + let (Some(s), Some(e)) = ( + read_address(&mut reader, unit), + read_address(&mut reader, unit), + ) else { + break; + }; + (s, e) + } + DW_RLE_START_LENGTH => { + let Some(s) = read_address(&mut reader, unit) else { + break; + }; + match reader.uleb128().and_then(|n| s.checked_add(n)) { + Some(e) => (s, e), + None => break, + } + } + // A kind this does not know carries an operand of a length only + // the kind says, so the list stops rather than resynchronizing. + _ => break, + }; + if let Some(r) = AddrRange::new(pair.0, pair.1).filter(|r| !r.is_empty()) { + out.push(r); + } + } + out +} + +/// A DWARF 4 `.debug_ranges` list: bare address pairs. +fn debug_ranges(unit: &Unit<'_>, at: usize) -> Vec { + let Some(rest) = unit.sections.ranges.get(at..) else { + return Vec::new(); + }; + let mut reader = Cur::new(rest, unit.endian); + let mut base = unit.low_pc; + let mut out = Vec::new(); + let all_ones = if unit.address_size == 4 { + u32::MAX as u64 + } else { + u64::MAX + }; + // Each entry is two addresses, so the bytes bound the count. + for _ in 0..rest.len() / 2 { + let (Some(start), Some(end)) = ( + read_address(&mut reader, unit), + read_address(&mut reader, unit), + ) else { + break; + }; + if start == Addr::ZERO && end == Addr::ZERO { + break; + } + // The all-ones first word is how the format announces a new base + // rather than a range. + if start.get() == all_ones { + base = end; + continue; + } + match (base.checked_add(start.get()), base.checked_add(end.get())) { + (Some(s), Some(e)) => { + if let Some(r) = AddrRange::new(s, e).filter(|r| !r.is_empty()) { + out.push(r); + } + } + _ => break, + } + } + out +} + +/// The entries a location list offset names, in whichever section this DWARF +/// version keeps them in. +fn location_list(unit: &Unit<'_>, at: usize) -> Vec { + if unit.version >= 5 { + loclists(unit, at) + } else { + debug_loc(unit, at) + } +} + +/// A DWARF 5 `.debug_loclists` list. +fn loclists(unit: &Unit<'_>, at: usize) -> Vec { + let Some(rest) = unit.sections.loclists.get(at..) else { + return Vec::new(); + }; + let mut reader = Cur::new(rest, unit.endian); + let mut base = unit.low_pc; + let mut out = Vec::new(); + for _ in 0..rest.len() { + let Some(kind) = reader.u8() else { break }; + let pair = match kind { + DW_LLE_END_OF_LIST => break, + DW_LLE_BASE_ADDRESSX => { + match reader.uleb128().and_then(|i| address_from_table(unit, i)) { + Some(a) => base = a, + None => break, + } + continue; + } + DW_LLE_BASE_ADDRESS => { + match read_address(&mut reader, unit) { + Some(a) => base = a, + None => break, + } + continue; + } + DW_LLE_STARTX_ENDX => { + let (Some(s), Some(e)) = (reader.uleb128(), reader.uleb128()) else { + break; + }; + match (address_from_table(unit, s), address_from_table(unit, e)) { + (Some(s), Some(e)) => (s, e), + _ => break, + } + } + DW_LLE_STARTX_LENGTH => { + let (Some(s), Some(n)) = (reader.uleb128(), reader.uleb128()) else { + break; + }; + match address_from_table(unit, s).and_then(|s| Some((s, s.checked_add(n)?))) { + Some(p) => p, + None => break, + } + } + DW_LLE_OFFSET_PAIR => { + let (Some(s), Some(e)) = (reader.uleb128(), reader.uleb128()) else { + break; + }; + match (base.checked_add(s), base.checked_add(e)) { + (Some(s), Some(e)) => (s, e), + _ => break, + } + } + DW_LLE_START_END => { + let (Some(s), Some(e)) = ( + read_address(&mut reader, unit), + read_address(&mut reader, unit), + ) else { + break; + }; + (s, e) + } + DW_LLE_START_LENGTH => { + let Some(s) = read_address(&mut reader, unit) else { + break; + }; + match reader.uleb128().and_then(|n| s.checked_add(n)) { + Some(e) => (s, e), + None => break, + } + } + // A default location applies wherever no other entry does, which + // is not a range and has nowhere to go in the result. + DW_LLE_DEFAULT_LOCATION => { + let Some(n) = reader.uleb128() else { break }; + if reader.bytes(n as usize).is_none() { + break; + } + continue; + } + _ => break, + }; + let Some(n) = reader.uleb128() else { break }; + let Some(expression) = reader.bytes(n as usize) else { + break; + }; + let Some(range) = AddrRange::new(pair.0, pair.1).filter(|r| !r.is_empty()) else { + continue; + }; + if let Some(location) = decode_location(expression, unit) { + out.push(LocationRange { range, location }); + } + } + out +} + +/// A DWARF 4 `.debug_loc` list: address pairs, each with a counted expression. +fn debug_loc(unit: &Unit<'_>, at: usize) -> Vec { + let Some(rest) = unit.sections.loc.get(at..) else { + return Vec::new(); + }; + let mut reader = Cur::new(rest, unit.endian); + let mut base = unit.low_pc; + let mut out = Vec::new(); + let all_ones = if unit.address_size == 4 { + u32::MAX as u64 + } else { + u64::MAX + }; + for _ in 0..rest.len() / 2 { + let (Some(start), Some(end)) = ( + read_address(&mut reader, unit), + read_address(&mut reader, unit), + ) else { + break; + }; + if start == Addr::ZERO && end == Addr::ZERO { + break; + } + if start.get() == all_ones { + base = end; + continue; + } + let Some(n) = reader.u16() else { break }; + let Some(expression) = reader.bytes(n as usize) else { + break; + }; + let (Some(s), Some(e)) = (base.checked_add(start.get()), base.checked_add(end.get())) + else { + break; + }; + let Some(range) = AddrRange::new(s, e).filter(|r| !r.is_empty()) else { + continue; + }; + if let Some(location) = decode_location(expression, unit) { + out.push(LocationRange { range, location }); + } + } + out +} + +/// What a location expression says about where a value is. +/// +/// The shapes a compiler actually emits for storage are a register, a frame +/// offset, a register offset and a fixed address. Anything else is kept as its +/// bytes rather than half-evaluated, because a half-evaluated expression is a +/// wrong answer that looks like a right one. +fn decode_location(bytes: &[u8], unit: &Unit<'_>) -> Option { + if bytes.is_empty() { + return None; + } + // Anything the decoder cannot finish becomes the bytes themselves rather + // than nothing: dropping the entry would lose the range it covers, which + // is information even when the expression is not understood. + Some(decode_known(bytes, unit).unwrap_or_else(|| Location::Expression(bytes.to_vec()))) +} + +fn decode_known(bytes: &[u8], unit: &Unit<'_>) -> Option { + let mut reader = Cur::new(bytes, unit.endian); + let op = reader.u8()?; + let decoded = match op { + DW_OP_ADDR => Location::Address(read_address(&mut reader, unit)?), + DW_OP_ADDRX => Location::Address(address_from_table(unit, reader.uleb128()?)?), + DW_OP_FBREG => Location::FrameOffset(reader.sleb128()?), + DW_OP_REGX => Location::Register(u16::try_from(reader.uleb128()?).ok()?), + DW_OP_BREGX => { + let register = u16::try_from(reader.uleb128()?).ok()?; + Location::RegisterOffset(register, reader.sleb128()?) + } + DW_OP_LIT0..=DW_OP_LIT31 => Location::Constant((op - DW_OP_LIT0) as i64), + DW_OP_REG0..=DW_OP_REG31 => Location::Register((op - DW_OP_REG0) as u16), + DW_OP_BREG0..=DW_OP_BREG31 => { + Location::RegisterOffset((op - DW_OP_BREG0) as u16, reader.sleb128()?) + } + _ => return Some(Location::Expression(bytes.to_vec())), + }; + // What is left has to be nothing, or a marker that changes what the value + // means rather than where it is. + loop { + match reader.u8() { + None => return Some(decoded), + Some(DW_OP_STACK_VALUE) => {} + Some(_) => return Some(Location::Expression(bytes.to_vec())), + } + } +} + +/// The file table of the line program at `at`. +/// +/// `DW_AT_decl_file` and `DW_AT_call_file` are indices into this, and without +/// it neither can become a name. Only the header is read: the program itself +/// is walked separately, for the rows. +fn file_table(sections: &Sections<'_>, endian: Endian, at: usize) -> Option> { + if at >= sections.line.len() { + return None; + } + let mut reader = Cur::new(sections.line, endian); + if at != 0 && !reader.seek(at) { + return None; + } + let length = reader.u32()?; + let sixty_four = length == 0xffff_ffff; + if sixty_four { + reader.u64()?; + } + let version = reader.u16()?; + if !(2..=5).contains(&version) { + return None; + } + if version >= 5 { + reader.u8()?; // address size + reader.u8()?; // segment selector size + } + offset(&mut reader, sixty_four)?; // header length + reader.u8()?; // minimum instruction length + if version >= 4 { + reader.u8()?; // maximum operations per instruction + } + reader.u8()?; // default is_stmt + reader.u8()?; // line base + reader.u8()?; // line range + let opcode_base = reader.u8()?.max(1); + for _ in 1..opcode_base { + reader.u8()?; + } + if version >= 5 { + file_names_v5(&mut reader, sections, endian, sixty_four) + } else { + file_names_v4(&mut reader) + } +} + +// Tags. +const DW_TAG_ARRAY_TYPE: u64 = 0x01; +const DW_TAG_CLASS_TYPE: u64 = 0x02; +const DW_TAG_ENUMERATION_TYPE: u64 = 0x04; +const DW_TAG_FORMAL_PARAMETER: u64 = 0x05; +const DW_TAG_MEMBER: u64 = 0x0d; +const DW_TAG_POINTER_TYPE: u64 = 0x0f; +const DW_TAG_COMPILE_UNIT: u64 = 0x11; +const DW_TAG_STRUCTURE_TYPE: u64 = 0x13; +const DW_TAG_SUBROUTINE_TYPE: u64 = 0x15; +const DW_TAG_TYPEDEF: u64 = 0x16; +const DW_TAG_UNION_TYPE: u64 = 0x17; +const DW_TAG_UNSPECIFIED_PARAMETERS: u64 = 0x18; +const DW_TAG_SUBRANGE_TYPE: u64 = 0x21; +const DW_TAG_INLINED_SUBROUTINE: u64 = 0x1d; +const DW_TAG_CALL_SITE: u64 = 0x48; +const DW_TAG_CALL_SITE_PARAMETER: u64 = 0x49; +/// The GNU spelling, which is what a producer writing DWARF 4 emits. +const DW_TAG_GNU_CALL_SITE: u64 = 0x4109; +const DW_TAG_GNU_CALL_SITE_PARAMETER: u64 = 0x410a; +const DW_TAG_BASE_TYPE: u64 = 0x24; +const DW_TAG_CONST_TYPE: u64 = 0x26; +const DW_TAG_ENUMERATOR: u64 = 0x28; +const DW_TAG_SUBPROGRAM: u64 = 0x2e; +const DW_TAG_VARIABLE: u64 = 0x34; +const DW_TAG_VOLATILE_TYPE: u64 = 0x35; +const DW_TAG_RESTRICT_TYPE: u64 = 0x37; + +// Attributes. +const DW_AT_LOCATION: u64 = 0x02; +const DW_AT_NAME: u64 = 0x03; +const DW_AT_BYTE_SIZE: u64 = 0x0b; +const DW_AT_BIT_SIZE: u64 = 0x0d; +const DW_AT_LOW_PC: u64 = 0x11; +const DW_AT_HIGH_PC: u64 = 0x12; +const DW_AT_UPPER_BOUND: u64 = 0x2f; +const DW_AT_CONST_VALUE: u64 = 0x1c; +const DW_AT_INLINE: u64 = 0x20; +const DW_AT_COUNT: u64 = 0x37; +const DW_AT_DATA_MEMBER_LOCATION: u64 = 0x38; +const DW_AT_DECL_LINE: u64 = 0x3b; +const DW_AT_ENCODING: u64 = 0x3e; +const DW_AT_TYPE: u64 = 0x49; +const DW_AT_LINKAGE_NAME: u64 = 0x6e; +const DW_AT_STR_OFFSETS_BASE: u64 = 0x72; +const DW_AT_ADDR_BASE: u64 = 0x73; +const DW_AT_STMT_LIST: u64 = 0x10; +const DW_AT_DECL_FILE: u64 = 0x3a; +const DW_AT_ABSTRACT_ORIGIN: u64 = 0x31; +const DW_AT_SPECIFICATION: u64 = 0x47; +const DW_AT_ENTRY_PC: u64 = 0x52; +const DW_AT_RANGES: u64 = 0x55; +const DW_AT_CALL_COLUMN: u64 = 0x57; +const DW_AT_CALL_FILE: u64 = 0x58; +const DW_AT_CALL_LINE: u64 = 0x59; +const DW_AT_RNGLISTS_BASE: u64 = 0x74; +const DW_AT_LOCLISTS_BASE: u64 = 0x8c; +const DW_AT_CALL_RETURN_PC: u64 = 0x7d; +const DW_AT_CALL_VALUE: u64 = 0x7e; +const DW_AT_CALL_ORIGIN: u64 = 0x7f; +const DW_AT_CALL_PC: u64 = 0x81; +const DW_AT_CALL_TAIL_CALL: u64 = 0x82; +const DW_AT_CALL_TARGET: u64 = 0x83; +const DW_AT_CALL_DATA_VALUE: u64 = 0x86; +// The GNU call site attributes, which a DWARF 4 producer uses instead. +const DW_AT_GNU_CALL_SITE_VALUE: u64 = 0x2111; +const DW_AT_GNU_CALL_SITE_DATA_VALUE: u64 = 0x2112; +const DW_AT_GNU_CALL_SITE_TARGET: u64 = 0x2113; +const DW_AT_GNU_TAIL_CALL: u64 = 0x2115; + +// Base type encodings. +const DW_ATE_BOOLEAN: u64 = 0x02; +const DW_ATE_FLOAT: u64 = 0x04; +const DW_ATE_SIGNED: u64 = 0x05; +const DW_ATE_SIGNED_CHAR: u64 = 0x06; + +// Forms. +const DW_FORM_ADDR: u64 = 0x01; +const DW_FORM_BLOCK2: u64 = 0x03; +const DW_FORM_BLOCK4: u64 = 0x04; +const DW_FORM_DATA2: u64 = 0x05; +const DW_FORM_DATA4: u64 = 0x06; +const DW_FORM_DATA8: u64 = 0x07; +const DW_FORM_STRING: u64 = 0x08; +const DW_FORM_BLOCK: u64 = 0x09; +const DW_FORM_BLOCK1: u64 = 0x0a; +const DW_FORM_DATA1: u64 = 0x0b; +const DW_FORM_FLAG: u64 = 0x0c; +const DW_FORM_SDATA: u64 = 0x0d; +const DW_FORM_STRP: u64 = 0x0e; +const DW_FORM_UDATA: u64 = 0x0f; +const DW_FORM_REF_ADDR: u64 = 0x10; +const DW_FORM_REF1: u64 = 0x11; +const DW_FORM_REF2: u64 = 0x12; +const DW_FORM_REF4: u64 = 0x13; +const DW_FORM_REF8: u64 = 0x14; +const DW_FORM_REF_UDATA: u64 = 0x15; +const DW_FORM_INDIRECT: u64 = 0x16; +const DW_FORM_SEC_OFFSET: u64 = 0x17; +const DW_FORM_EXPRLOC: u64 = 0x18; +const DW_FORM_FLAG_PRESENT: u64 = 0x19; +const DW_FORM_STRX: u64 = 0x1a; +const DW_FORM_ADDRX: u64 = 0x1b; +const DW_FORM_REF_SUP4: u64 = 0x1c; +const DW_FORM_STRP_SUP: u64 = 0x1d; +const DW_FORM_DATA16: u64 = 0x1e; +const DW_FORM_LINE_STRP: u64 = 0x1f; +const DW_FORM_IMPLICIT_CONST: u64 = 0x21; +const DW_FORM_LOCLISTX: u64 = 0x22; +const DW_FORM_RNGLISTX: u64 = 0x23; +const DW_FORM_STRX1: u64 = 0x25; +const DW_FORM_STRX2: u64 = 0x26; +const DW_FORM_STRX3: u64 = 0x27; +const DW_FORM_STRX4: u64 = 0x28; +const DW_FORM_ADDRX1: u64 = 0x29; +const DW_FORM_ADDRX2: u64 = 0x2a; +const DW_FORM_ADDRX3: u64 = 0x2b; +const DW_FORM_ADDRX4: u64 = 0x2c; + +// Location expression opcodes. +const DW_OP_ADDR: u8 = 0x03; +const DW_OP_LIT0: u8 = 0x30; +const DW_OP_LIT31: u8 = 0x4f; +const DW_OP_REG0: u8 = 0x50; +const DW_OP_REG31: u8 = 0x6f; +const DW_OP_BREG0: u8 = 0x70; +const DW_OP_BREG31: u8 = 0x8f; +const DW_OP_REGX: u8 = 0x90; +const DW_OP_FBREG: u8 = 0x91; +const DW_OP_BREGX: u8 = 0x92; +const DW_OP_STACK_VALUE: u8 = 0x9f; +const DW_OP_ADDRX: u8 = 0xa1; + +// Range list entry kinds, DWARF 5. +const DW_RLE_END_OF_LIST: u8 = 0x00; +const DW_RLE_BASE_ADDRESSX: u8 = 0x01; +const DW_RLE_STARTX_ENDX: u8 = 0x02; +const DW_RLE_STARTX_LENGTH: u8 = 0x03; +const DW_RLE_OFFSET_PAIR: u8 = 0x04; +const DW_RLE_BASE_ADDRESS: u8 = 0x05; +const DW_RLE_START_END: u8 = 0x06; +const DW_RLE_START_LENGTH: u8 = 0x07; + +// Location list entry kinds, DWARF 5. +const DW_LLE_END_OF_LIST: u8 = 0x00; +const DW_LLE_BASE_ADDRESSX: u8 = 0x01; +const DW_LLE_STARTX_ENDX: u8 = 0x02; +const DW_LLE_STARTX_LENGTH: u8 = 0x03; +const DW_LLE_OFFSET_PAIR: u8 = 0x04; +const DW_LLE_DEFAULT_LOCATION: u8 = 0x05; +const DW_LLE_BASE_ADDRESS: u8 = 0x06; +const DW_LLE_START_END: u8 = 0x07; +const DW_LLE_START_LENGTH: u8 = 0x08; + +// Line number program opcodes. +const DW_LNS_COPY: u8 = 0x01; +const DW_LNS_ADVANCE_PC: u8 = 0x02; +const DW_LNS_ADVANCE_LINE: u8 = 0x03; +const DW_LNS_SET_FILE: u8 = 0x04; +const DW_LNS_SET_COLUMN: u8 = 0x05; +const DW_LNS_NEGATE_STMT: u8 = 0x06; +const DW_LNS_CONST_ADD_PC: u8 = 0x08; +const DW_LNS_FIXED_ADVANCE_PC: u8 = 0x09; +const DW_LNE_END_SEQUENCE: u8 = 0x01; +const DW_LNE_SET_ADDRESS: u8 = 0x02; +const DW_LNCT_PATH: u64 = 0x01; diff --git a/crates/e5r-format/src/ehframe.rs b/crates/e5r-format/src/ehframe.rs new file mode 100644 index 0000000..a0d582b --- /dev/null +++ b/crates/e5r-format/src/ehframe.rs @@ -0,0 +1,289 @@ +//! `.eh_frame` walking, for function starts and sizes. +//! +//! Unwind tables are emitted for exception handling, not for us, which is what +//! makes them good evidence: they are generated from the compiler's own idea of +//! where each function begins and ends, and they survive stripping. One FDE per +//! function, each naming a start address and a length. +//! +//! Only the header of each entry is read. The CFI program that follows says how +//! to restore registers and is not needed to answer "where are the functions". + +use e5r_core::{Addr, Bits, Error, Reader, Result}; + +// DW_EH_PE format, low nibble. +const PE_ABSPTR: u8 = 0x00; +const PE_ULEB128: u8 = 0x01; +const PE_UDATA2: u8 = 0x02; +const PE_UDATA4: u8 = 0x03; +const PE_UDATA8: u8 = 0x04; +const PE_SLEB128: u8 = 0x09; +const PE_SDATA2: u8 = 0x0a; +const PE_SDATA4: u8 = 0x0b; +const PE_SDATA8: u8 = 0x0c; +// DW_EH_PE application, high nibble. +const PE_PCREL: u8 = 0x10; +const PE_DATAREL: u8 = 0x30; +const PE_OMIT: u8 = 0xff; + +/// What one CIE says about the FDEs that point at it. +#[derive(Clone, Copy, Default)] +struct Cie { + /// Encoding of an FDE's `pc_begin`. + fde_encoding: u8, + /// True when the augmentation string starts with `z`, so entries carry a + /// length-prefixed augmentation block. + has_augmentation_data: bool, +} + +/// Every `(start, length)` an `.eh_frame` section declares. +/// +/// `section_addr` is where the section is mapped, needed because most encodings +/// are relative to the field's own address. +pub fn function_starts(body: &[u8], section_addr: Addr, bits: Bits) -> Result> { + let ptr_size = bits.bytes(); + let r = Reader::le(body); + let mut out = Vec::new(); + let mut cies: Vec<(u64, Cie)> = Vec::new(); + let mut pos: u64 = 0; + + while pos + 4 <= body.len() as u64 { + let entry_start = pos; + let mut e = r.slice_at("eh_frame entry", pos, (body.len() as u64) - pos)?; + let len32 = e.u32("length")?; + // Zero length terminates the section. + if len32 == 0 { + break; + } + let (len, header) = if len32 == 0xffff_ffff { + (e.u64("extended length")?, 12u64) + } else { + (len32 as u64, 4u64) + }; + let next = entry_start + .checked_add(header) + .and_then(|v| v.checked_add(len)) + .ok_or(Error::BadField { + field: "eh_frame entry length", + value: len, + reason: "carries past the end of the address space", + })?; + if next > body.len() as u64 { + // A truncated last entry is common in a damaged file; stop rather + // than fail, since everything before it is still good. + break; + } + + let id = e.u32("CIE id")?; + if id == 0 { + let cie = parse_cie(&mut e, ptr_size)?; + cies.push((entry_start, cie)); + } else { + // CIE_pointer is a backward distance from the field's own position. + let id_pos = entry_start + header; + let cie_at = id_pos.checked_sub(id as u64); + let cie = cie_at + .and_then(|a| cies.iter().rev().find(|(at, _)| *at == a).map(|(_, c)| *c)) + .unwrap_or_default(); + + let pc_begin_at = section_addr.get().wrapping_add(e.pos() + entry_start); + let Some(start) = read_encoded( + &mut e, + cie.fde_encoding, + pc_begin_at, + section_addr, + ptr_size, + )? + else { + pos = next; + continue; + }; + // pc_range uses the same format with no application applied. + let range = read_encoded(&mut e, cie.fde_encoding & 0x0f, 0, section_addr, ptr_size)? + .unwrap_or(0); + if start != 0 { + out.push((Addr(start), range)); + } + } + pos = next; + } + + out.sort_unstable(); + out.dedup(); + Ok(out) +} + +fn parse_cie(e: &mut Reader<'_>, ptr_size: u64) -> Result { + let version = e.u8("CIE version")?; + let aug_start = e.pos(); + let aug = e + .data() + .get(aug_start as usize..) + .and_then(|s| s.iter().position(|&b| b == 0).map(|n| &s[..n])) + .unwrap_or(b""); + e.skip("augmentation string", aug.len() as u64 + 1)?; + + let _code_align = e.uleb128("code alignment")?; + let _data_align = e.sleb128("data alignment")?; + if version == 1 { + e.u8("return address register")?; + } else { + e.uleb128("return address register")?; + } + + let mut cie = Cie { + fde_encoding: PE_ABSPTR, + has_augmentation_data: aug.first() == Some(&b'z'), + }; + if !cie.has_augmentation_data { + return Ok(cie); + } + + let aug_len = e.uleb128("augmentation length")?; + let aug_end = e.pos() + aug_len; + for c in aug.iter().skip(1) { + match c { + b'R' => cie.fde_encoding = e.u8("FDE pointer encoding")?, + b'L' => { + e.u8("LSDA encoding")?; + } + b'P' => { + let enc = e.u8("personality encoding")?; + read_encoded(e, enc, 0, Addr::ZERO, ptr_size)?; + } + b'S' | b'B' | b'G' => {} + _ => break, + } + } + // Trust the declared length over the augmentation string, which is what + // makes an unknown augmentation character survivable. + if aug_end <= e.len() as u64 { + e.seek("augmentation end", aug_end)?; + } + Ok(cie) +} + +/// Read one DW_EH_PE-encoded pointer. `None` when the encoding says omit. +fn read_encoded( + e: &mut Reader<'_>, + enc: u8, + field_addr: u64, + section_addr: Addr, + ptr_size: u64, +) -> Result> { + if enc == PE_OMIT { + return Ok(None); + } + let raw: i64 = match enc & 0x0f { + PE_ABSPTR => { + if ptr_size == 8 { + e.u64("pointer")? as i64 + } else { + e.u32("pointer")? as i64 + } + } + PE_ULEB128 => e.uleb128("pointer")? as i64, + PE_UDATA2 => e.u16("pointer")? as i64, + PE_UDATA4 => e.u32("pointer")? as i64, + PE_UDATA8 => e.u64("pointer")? as i64, + PE_SLEB128 => e.sleb128("pointer")?, + PE_SDATA2 => e.i16("pointer")? as i64, + PE_SDATA4 => e.i32("pointer")? as i64, + PE_SDATA8 => e.i64("pointer")?, + other => { + return Err(Error::BadField { + field: "DW_EH_PE format", + value: other as u64, + reason: "is not a pointer format DWARF defines", + }); + } + }; + + let base = match enc & 0x70 { + PE_PCREL => field_addr, + PE_DATAREL => section_addr.get(), + _ => 0, + }; + Ok(Some(base.wrapping_add(raw as u64))) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A CIE with `zR`/sdata4-pcrel and one FDE, built by hand so the test does + /// not depend on a compiler. + fn one_fde(pc_rel: i32, range: u32) -> Vec { + let mut cie = vec![1u8]; // version + cie.extend_from_slice(b"zR\0"); + cie.push(1); // code alignment + cie.push(0x7c); // data alignment, sleb -4 + cie.push(16); // return address register + cie.push(1); // augmentation length + cie.push(PE_SDATA4 | PE_PCREL); + while (cie.len() + 8) % 4 != 0 { + cie.push(0); + } + let mut out = Vec::new(); + out.extend_from_slice(&((cie.len() as u32 + 4).to_le_bytes())); + out.extend_from_slice(&0u32.to_le_bytes()); // CIE id + out.extend_from_slice(&cie); + + let cie_end = out.len(); + let mut fde = Vec::new(); + fde.extend_from_slice(&pc_rel.to_le_bytes()); + fde.extend_from_slice(&range.to_le_bytes()); + fde.push(0); // augmentation length + while (fde.len() + 8) % 4 != 0 { + fde.push(0); + } + out.extend_from_slice(&((fde.len() as u32 + 4).to_le_bytes())); + // CIE_pointer: distance back from this field to the CIE start. + out.extend_from_slice(&(cie_end as u32 + 4).to_le_bytes()); + out.extend_from_slice(&fde); + out + } + + #[test] + fn one_fde_yields_one_function() { + let body = one_fde(0x100, 0x40); + let f = function_starts(&body, Addr(0x2000), Bits::Bits64).unwrap(); + assert_eq!(f.len(), 1); + assert_eq!(f[0].1, 0x40); + // pcrel means the start is relative to the pc_begin field's address, + // which sits at the section address plus its offset in the section. + assert!(f[0].0.get() > 0x2000); + } + + #[test] + fn a_truncated_tail_keeps_what_came_before() { + let mut body = one_fde(0x100, 0x40); + body.extend_from_slice(&0x7fff_ffffu32.to_le_bytes()); + body.extend_from_slice(&[0, 0]); + let f = function_starts(&body, Addr(0x2000), Bits::Bits64).unwrap(); + assert_eq!(f.len(), 1); + } + + #[test] + fn zero_length_terminates() { + let mut body = one_fde(0x100, 0x40); + body.extend_from_slice(&0u32.to_le_bytes()); + body.extend_from_slice(&[0xff; 32]); + assert_eq!( + function_starts(&body, Addr(0x2000), Bits::Bits64) + .unwrap() + .len(), + 1 + ); + } + + #[test] + fn garbage_does_not_panic() { + for seed in 0u32..256 { + let body: Vec = (0..128u32) + .map(|i| (i.wrapping_mul(2654435761).wrapping_add(seed) >> 13) as u8) + .collect(); + let _ = function_starts(&body, Addr(0x1000), Bits::Bits64); + let _ = function_starts(&body, Addr(0x1000), Bits::Bits32); + } + } +} diff --git a/crates/e5r-format/src/elf.rs b/crates/e5r-format/src/elf.rs new file mode 100644 index 0000000..2f478d8 --- /dev/null +++ b/crates/e5r-format/src/elf.rs @@ -0,0 +1,2407 @@ +//! ELF, 32- and 64-bit, both byte orders. +//! +//! Structure layout differs between the two classes in ways that are easy to +//! get wrong: ELF32 program headers put `p_flags` last, ELF64 puts it second, +//! and the two `Elf_Sym` layouts interleave their fields differently. Each is +//! read field by field rather than cast over, so the difference is visible. +//! +//! A relocatable object has no program headers and every section sits at +//! address zero. Those get synthetic addresses, assigned in section order with +//! alignment respected, so an `.o` file is analyzable like anything else. + +use std::collections::BTreeMap; + +use e5r_core::{ + Addr, AddrRange, Arch, Bits, Endian, Error, Evidence, MemoryMap, Perms, Provenance, Reader, + Result, Segment, +}; + +use crate::{ + Binding, Export, Format, FunctionHint, Import, LoadOptions, Object, Section, Symbol, SymbolKind, +}; + +const MAGIC: [u8; 4] = [0x7f, b'E', b'L', b'F']; + +// e_type +const ET_REL: u16 = 1; +const ET_EXEC: u16 = 2; +const ET_DYN: u16 = 3; +const ET_CORE: u16 = 4; + +// p_type +const PT_LOAD: u32 = 1; +const PT_DYNAMIC: u32 = 2; +const PT_INTERP: u32 = 3; +const PT_NOTE: u32 = 4; +const PT_GNU_EH_FRAME: u32 = 0x6474_e550; +const PT_GNU_STACK: u32 = 0x6474_e551; +const PT_GNU_RELRO: u32 = 0x6474_e552; + +// sh_type +const SHT_SYMTAB: u32 = 2; +const SHT_STRTAB: u32 = 3; +const SHT_RELA: u32 = 4; +const SHT_NOBITS: u32 = 8; +const SHT_REL: u32 = 9; +const SHT_DYNSYM: u32 = 11; +const SHT_INIT_ARRAY: u32 = 14; +const SHT_FINI_ARRAY: u32 = 15; + +// sh_flags +const SHF_WRITE: u64 = 0x1; +const SHF_ALLOC: u64 = 0x2; +const SHF_EXECINSTR: u64 = 0x4; + +// d_tag +const DT_NULL: u64 = 0; +const DT_NEEDED: u64 = 1; +const DT_PLTGOT: u64 = 3; +const DT_SONAME: u64 = 14; +const DT_INIT: u64 = 12; +const DT_FINI: u64 = 13; +const DT_RPATH: u64 = 15; +const DT_RUNPATH: u64 = 29; +const DT_FLAGS_1: u64 = 0x6fff_fffb; +const DF_1_PIE: u64 = 0x0800_0000; + +// ST_TYPE +const STT_OBJECT: u8 = 1; +const STT_FUNC: u8 = 2; +const STT_SECTION: u8 = 3; +const STT_FILE: u8 = 4; +const STT_TLS: u8 = 6; +const STT_GNU_IFUNC: u8 = 10; + +const SHN_UNDEF: u16 = 0; + +// i386 relocation types, from the psABI. Spelled out because the numbering is +// dense and unmemorable, and because half of them differ from the x86-64 type +// with the same number. +const R_386_NONE: u64 = 0; +const R_386_32: u64 = 1; +const R_386_PC32: u64 = 2; +const R_386_GOT32: u64 = 3; +const R_386_PLT32: u64 = 4; +const R_386_COPY: u64 = 5; +const R_386_GLOB_DAT: u64 = 6; +const R_386_JMP_SLOT: u64 = 7; +const R_386_RELATIVE: u64 = 8; +const R_386_GOTOFF: u64 = 9; +const R_386_GOTPC: u64 = 10; +const R_386_32PLT: u64 = 11; +const R_386_16: u64 = 20; +const R_386_PC16: u64 = 21; +const R_386_8: u64 = 22; +const R_386_PC8: u64 = 23; +const R_386_SIZE32: u64 = 38; +const R_386_IRELATIVE: u64 = 42; +const R_386_GOT32X: u64 = 43; + +/// Raw section header, before names are resolved. +struct SecHdr { + name_off: u32, + kind: u32, + flags: u64, + addr: u64, + offset: u64, + size: u64, + link: u32, + /// The section these entries apply to. Read for the record rather than + /// used: identifying the PLT relocation table through it does not work, + /// because it names `.got.plt` rather than `.plt`. + #[allow(dead_code)] + info: u32, + addralign: u64, + entsize: u64, +} + +/// Raw program header. +struct ProgHdr { + kind: u32, + flags: u32, + offset: u64, + vaddr: u64, + filesz: u64, + memsz: u64, +} + +/// True when the buffer starts with the ELF magic. +pub fn is_elf(data: &[u8]) -> bool { + data.len() >= 4 && data[..4] == MAGIC +} + +/// Parse an ELF file. +pub fn load(data: &[u8], opts: &LoadOptions) -> Result { + if !is_elf(data) { + return Err(Error::NotRecognized { expected: "ELF" }); + } + let caps = &opts.caps; + let mut warnings = Vec::new(); + + // Read through the reader rather than indexing: a five-byte file that + // starts with the magic is a real fuzz case, and indexing panicked on it. + let ident = Reader::le(data); + let class = ident.bytes_at("e_ident[EI_CLASS]", 4, 1)?[0]; + let data_encoding = ident.bytes_at("e_ident[EI_DATA]", 5, 1)?[0]; + let wide = match class { + 1 => false, + 2 => true, + other => { + return Err(Error::BadField { + field: "e_ident[EI_CLASS]", + value: other as u64, + reason: "is neither ELFCLASS32 (1) nor ELFCLASS64 (2)", + }); + } + }; + let endian = match data_encoding { + 1 => Endian::Little, + 2 => Endian::Big, + other => { + return Err(Error::BadField { + field: "e_ident[EI_DATA]", + value: other as u64, + reason: "is neither ELFDATA2LSB (1) nor ELFDATA2MSB (2)", + }); + } + }; + let bits = if wide { Bits::Bits64 } else { Bits::Bits32 }; + + let r = Reader::new(data, endian); + let mut h = r; + h.seek("e_type", 16)?; + let e_type = h.u16("e_type")?; + let e_machine = h.u16("e_machine")?; + let _e_version = h.u32("e_version")?; + let e_entry = h.uword("e_entry", wide)?; + let e_phoff = h.uword("e_phoff", wide)?; + let e_shoff = h.uword("e_shoff", wide)?; + let _e_flags = h.u32("e_flags")?; + let _e_ehsize = h.u16("e_ehsize")?; + let e_phentsize = h.u16("e_phentsize")?; + let e_phnum = h.u16("e_phnum")?; + let e_shentsize = h.u16("e_shentsize")?; + let e_shnum = h.u16("e_shnum")?; + let e_shstrndx = h.u16("e_shstrndx")?; + + let arch = machine_to_arch(e_machine, bits); + + let phdrs = read_program_headers(&r, wide, e_phoff, e_phentsize, e_phnum, caps, &mut warnings)?; + let (shdrs, shstrndx) = read_section_headers( + &r, + wide, + e_shoff, + e_shentsize, + e_shnum, + e_shstrndx, + caps, + &mut warnings, + )?; + + // Section names come from the section named by e_shstrndx. A file may lie + // about which section that is; a bad index costs the names, not the load. + let shstr = shdrs + .get(shstrndx as usize) + .and_then(|s| r.slice_at("shstrtab", s.offset, s.size).ok()); + if shstr.is_none() && !shdrs.is_empty() { + warnings.push(format!( + "e_shstrndx is {shstrndx}, which does not name a readable string table; \ + section names are unavailable" + )); + } + + let mut sections = Vec::with_capacity(shdrs.len()); + for sh in &shdrs { + let name = shstr + .and_then(|t| { + t.cstr_at("section name", sh.name_off as u64, caps.string_len) + .ok() + }) + .map(|b| String::from_utf8_lossy(b).into_owned()) + .unwrap_or_default(); + let file_size = if sh.kind == SHT_NOBITS { 0 } else { sh.size }; + sections.push(Section { + name, + range: AddrRange::sized(Addr(sh.addr), sh.size) + .unwrap_or(AddrRange::empty_at(Addr(sh.addr))), + file_offset: sh.offset, + file_size, + exec: sh.flags & SHF_EXECINSTR != 0, + write: sh.flags & SHF_WRITE != 0, + kind: sh.kind, + }); + } + + let relocatable = e_type == ET_REL; + let memory = if relocatable { + map_relocatable(&r, &shdrs, &mut sections, opts, &mut warnings)? + } else { + map_loadable(&r, &phdrs, &shdrs, §ions, opts, &mut warnings)? + }; + + let image_base = phdrs + .iter() + .filter(|p| p.kind == PT_LOAD) + .map(|p| p.vaddr) + .min() + .map(Addr) + .unwrap_or(Addr::ZERO); + + let mut obj = Object { + debug: None, + format: Format::Elf, + arch, + endian, + bits, + entry: (e_entry != 0).then_some(Addr(e_entry)), + image_base, + pic: e_type == ET_DYN, + memory, + sections, + symbols: Vec::new(), + imports: Vec::new(), + exports: Vec::new(), + function_hints: Vec::new(), + metadata: BTreeMap::new(), + warnings, + }; + + obj.metadata + .insert("elf.type".into(), type_name(e_type).into()); + obj.metadata + .insert("elf.machine".into(), format!("{e_machine}")); + obj.metadata.insert("elf.class".into(), bits.to_string()); + + let dynsym_names = read_symbols(&r, &shdrs, &mut obj, caps)?; + read_dynamic(&r, &phdrs, &shdrs, &mut obj, caps); + read_notes(&r, &phdrs, &shdrs, &mut obj); + read_init_arrays(&r, &shdrs, wide, &mut obj, caps); + read_plt_relocations(&r, &shdrs, wide, &dynsym_names, &mut obj, caps); + + if opts.eh_frame { + read_eh_frame(&r, &phdrs, &mut obj); + } + + if relocatable { + // A relocatable object writes zero where an address goes: without the + // relocations, every call inside it points at itself and the call + // graph is fiction. + apply_code_relocations(&r, &shdrs, &mut obj, caps); + } + + if opts.debug_info { + // A relocatable object's debug addresses are section-relative and need + // the relocations applied; its symbol table already names every + // function, so the types and lines are read and the hints are not. + read_debug_info(&r, &shdrs, &mut obj, !relocatable, caps); + } + + if let Some(entry) = obj.entry { + obj.function_hints.push(FunctionHint { + addr: entry, + size: None, + name: Some("_start".into()), + provenance: Provenance::new(Evidence::EntryPoint), + }); + } + + resolve_arm_mode(&mut obj, arm_profile(&r, &shdrs, shstr.as_ref())); + read_arm_vectors(&mut obj); + + // What a language's own runtime needs to find at run time: Go's function + // table names every function in a stripped binary, which no other evidence + // in the file does. + crate::metadata::apply(&mut obj); + + obj.normalize_symbols(); + obj.normalize_hints(); + Ok(obj) +} + +fn type_name(t: u16) -> &'static str { + match t { + ET_REL => "relocatable", + ET_EXEC => "executable", + ET_DYN => "shared object or PIE", + ET_CORE => "core dump", + _ => "unknown", + } +} + +/// Function entries read out of a Cortex-M vector table. +/// +/// An M-profile image begins with a table of exception handlers: the initial +/// stack pointer, then the reset handler, then one word per exception. Every +/// handler is a Thumb address and so has its low bit set, and in a stripped +/// firmware image nothing else says where any of them are -- `e_entry` often +/// points into a library routine rather than at the reset handler, and there +/// are no symbols. These are the roots the whole program hangs off. +/// +/// A word is taken only when its low bit is set and it points inside +/// executable memory, which the initial stack pointer and an unused slot both +/// fail. The table is read from the lowest mapped address, which is where the +/// architecture puts it out of reset. +fn read_arm_vectors(obj: &mut Object) { + /// Cortex-M has sixteen system exceptions and up to 240 external ones. + const MAX_ENTRIES: u64 = 256; + if obj.arch != Arch::Thumb { + return; + } + let Some(bounds) = obj.memory.bounds() else { + return; + }; + let executable: Vec<_> = obj + .memory + .segments() + .iter() + .filter(|s| s.perms.exec) + .map(|s| s.range) + .collect(); + if executable.is_empty() { + return; + } + let base = bounds.start(); + let mut found = Vec::new(); + for n in 1..MAX_ENTRIES { + let Some(at) = base.get().checked_add(n * 4).map(Addr) else { + break; + }; + let Ok(word) = obj + .memory + .read_ptr(at, 4, obj.endian == e5r_core::Endian::Little) + else { + break; + }; + if word & 1 == 0 { + continue; + } + let target = Addr(word & !1); + if executable.iter().any(|r| r.contains(target)) { + found.push(target); + } + } + found.sort(); + found.dedup(); + for addr in found { + obj.function_hints.push(FunctionHint { + addr, + size: None, + name: None, + provenance: Provenance::new(Evidence::EntryPoint), + }); + } +} + +/// The CPU architecture profile an `.ARM.attributes` section declares. +/// +/// `Tag_CPU_arch_profile` is `'M'` for the microcontroller profile, whose +/// processors execute T32 and have no A32 at all. That is a stronger statement +/// than any address's low bit, and it is what says so in a Cortex-M image whose +/// entry point was written without its Thumb bit -- which is most of them. +/// +/// The section is a version byte, then subsections of a four-byte length, a +/// vendor name and a body. The `aeabi` vendor's body is a sequence of tagged +/// blocks, of which tag 1 holds the file attributes: pairs of ULEB128 tag and +/// value. Everything here is bounds-checked and anything unexpected gives +/// `None` rather than a guess. +fn arm_profile(r: &Reader<'_>, shdrs: &[SecHdr], names: Option<&Reader<'_>>) -> Option { + const SHT_ARM_ATTRIBUTES: u32 = 0x7000_0003; + let sh = shdrs.iter().find(|s| { + s.kind == SHT_ARM_ATTRIBUTES + || names.is_some_and(|n| { + n.cstr_at("section name", s.name_off as u64, 32) + .is_ok_and(|name| name == b".ARM.attributes") + }) + })?; + let body = r.bytes_at("arm attributes", sh.offset, sh.size).ok()?; + let (first, rest) = body.split_first()?; + if *first != b'A' { + return None; + } + // One subsection. Its length counts the four bytes it is written in, and + // is measured from them rather than from the version byte above. + let len = u32::from_le_bytes(rest.get(..4)?.try_into().ok()?) as usize; + let sub = rest.get(..len)?.get(4..)?; + let vendor_end = sub.iter().position(|b| *b == 0)?; + if &sub[..vendor_end] != b"aeabi" { + return None; + } + let mut at = vendor_end + 1; + // The tagged blocks. Tag 1 is the file attributes, which is the only one + // that carries the profile. + while at + 5 <= sub.len() { + let tag = sub[at]; + let size = u32::from_le_bytes(sub.get(at + 1..at + 5)?.try_into().ok()?) as usize; + if size < 5 || at + size > sub.len() { + return None; + } + if tag == 1 { + return file_attribute(&sub[at + 5..at + size], 7); + } + at += size; + } + None +} + +/// The value of one file attribute, read out of a run of ULEB128 pairs. +/// +/// Every tag before the one asked for has to be stepped over, and stepping +/// over one means knowing whether its value is a number or a string, which the +/// tag decides. The two string-valued tags below 32 are named; above 32 the +/// low bit of the tag says which, as the ABI specifies. +fn file_attribute(mut body: &[u8], want: u64) -> Option { + fn uleb(b: &mut &[u8]) -> Option { + let (mut out, mut shift) = (0u64, 0u32); + loop { + let (byte, rest) = b.split_first()?; + *b = rest; + out |= u64::from(byte & 0x7f).checked_shl(shift)?; + if byte & 0x80 == 0 { + return Some(out); + } + shift += 7; + if shift >= 64 { + return None; + } + } + } + while !body.is_empty() { + let tag = uleb(&mut body)?; + let string = tag == 4 || tag == 5 || tag == 67 || (tag > 32 && tag % 2 == 1); + if string { + let end = body.iter().position(|b| *b == 0)?; + body = body.get(end + 1..)?; + continue; + } + let value = uleb(&mut body)?; + if tag == want { + return u8::try_from(value).ok(); + } + } + None +} + +/// Which instruction set an ARM image holds, and the bit that said so. +/// +/// On ARM the low bit of a code address is not part of the address: it is what +/// `bx` reads to decide which instruction set to switch to. A function symbol +/// or an entry point with that bit set is Thumb, and the address itself is the +/// value with the bit cleared -- leaving it on puts every function one byte +/// past where it starts and decodes the file from the wrong offset. +/// +/// The bytes never say which set an address holds, so something has to. What +/// says it here is those bits: an image whose code addresses are mostly odd is +/// Thumb, which is every Cortex-M image and every Thumb-compiled object. An +/// image that genuinely mixes the two in one file gets whichever its majority +/// is, and the minority decodes wrongly; separating those needs the `$a` and +/// `$t` mapping symbols, which a stripped image does not have. +fn resolve_arm_mode(obj: &mut Object, profile: Option) { + if obj.arch != Arch::Arm { + return; + } + let mut thumb = 0usize; + let mut total = 0usize; + let mut count = |addr: Addr| { + total += 1; + if addr.get() & 1 == 1 { + thumb += 1; + } + }; + if let Some(entry) = obj.entry { + count(entry); + } + for s in &obj.symbols { + if s.kind == SymbolKind::Function && s.addr != Addr::ZERO { + count(s.addr); + } + } + if total == 0 { + return; + } + let strip = |a: Addr| Addr(a.get() & !1); + if let Some(entry) = obj.entry { + obj.entry = Some(strip(entry)); + } + for s in &mut obj.symbols { + if s.kind == SymbolKind::Function { + s.addr = strip(s.addr); + } + } + for h in &mut obj.function_hints { + h.addr = strip(h.addr); + } + // The profile outranks the bits: an M-profile processor has no A32 to + // switch to, so an even entry point in one of its images is a linker that + // wrote the address without the bit rather than an A32 function. + let m_profile = profile == Some(b'M'); + if m_profile || thumb * 2 > total { + obj.arch = Arch::Thumb; + obj.metadata.insert("arm.mode".into(), "thumb".into()); + } else { + obj.metadata.insert("arm.mode".into(), "a32".into()); + } + if let Some(p) = profile { + obj.metadata + .insert("arm.profile".into(), (p as char).to_string()); + } +} + +/// `e_machine` to an architecture. Unrecognized machines load as a container +/// without a decoder rather than failing. +fn machine_to_arch(m: u16, bits: Bits) -> Arch { + match m { + 3 => Arch::X86, + 62 => Arch::X86_64, + 40 => Arch::Arm, + 183 => Arch::AArch64, + // Reachable through a SLEIGH language; the id is what the spec calls it. + 8 => Arch::Sleigh(format!("MIPS:BE:{bits}:default")), + 20 => Arch::Sleigh("PowerPC:BE:32:default".into()), + 21 => Arch::Sleigh("PowerPC:BE:64:default".into()), + 2 => Arch::Sleigh("sparc:BE:32:default".into()), + 22 => Arch::Sleigh("s390:BE:64:default".into()), + 243 => Arch::Sleigh(format!("RISCV:LE:{bits}:default")), + 258 => Arch::Sleigh(format!("Loongarch:LE:{bits}:default")), + other => Arch::Unknown(other as u32), + } +} + +fn read_program_headers( + r: &Reader<'_>, + wide: bool, + off: u64, + entsize: u16, + num: u16, + caps: &e5r_core::Caps, + warnings: &mut Vec, +) -> Result> { + if num == 0 || off == 0 { + return Ok(Vec::new()); + } + caps.check("program headers", num as u64, caps.sections)?; + let want = if wide { 56 } else { 32 }; + if (entsize as u64) < want { + warnings.push(format!( + "e_phentsize is {entsize}, smaller than the {want} bytes this class needs; \ + program headers skipped" + )); + return Ok(Vec::new()); + } + let mut out = Vec::with_capacity(num as usize); + for i in 0..num as u64 { + let at = off + i * entsize as u64; + let mut p = match r.slice_at("program header", at, want) { + Ok(p) => p, + Err(e) => { + warnings.push(format!("program header {i}: {e}")); + break; + } + }; + // The one field whose position differs between the classes. + let (kind, flags, offset, vaddr, filesz, memsz) = if wide { + let kind = p.u32("p_type")?; + let flags = p.u32("p_flags")?; + let offset = p.u64("p_offset")?; + let vaddr = p.u64("p_vaddr")?; + let _paddr = p.u64("p_paddr")?; + ( + kind, + flags, + offset, + vaddr, + p.u64("p_filesz")?, + p.u64("p_memsz")?, + ) + } else { + let kind = p.u32("p_type")?; + let offset = p.u32("p_offset")? as u64; + let vaddr = p.u32("p_vaddr")? as u64; + let _paddr = p.u32("p_paddr")?; + let filesz = p.u32("p_filesz")? as u64; + let memsz = p.u32("p_memsz")? as u64; + (kind, p.u32("p_flags")?, offset, vaddr, filesz, memsz) + }; + out.push(ProgHdr { + kind, + flags, + offset, + vaddr, + filesz, + memsz, + }); + } + Ok(out) +} + +#[allow(clippy::too_many_arguments)] +fn read_section_headers( + r: &Reader<'_>, + wide: bool, + off: u64, + entsize: u16, + num: u16, + shstrndx: u16, + caps: &e5r_core::Caps, + warnings: &mut Vec, +) -> Result<(Vec, u32)> { + if off == 0 { + return Ok((Vec::new(), 0)); + } + let want = if wide { 64 } else { 40 }; + if (entsize as u64) < want { + warnings.push(format!( + "e_shentsize is {entsize}, smaller than the {want} bytes this class needs; \ + sections skipped" + )); + return Ok((Vec::new(), 0)); + } + + // e_shnum of zero means the real count is in section 0's sh_size, the + // escape hatch for files with more than 0xffff sections. + let mut count = num as u64; + let mut index = shstrndx as u32; + if count == 0 { + let mut z = r.slice_at("section header 0", off, want)?; + z.skip("sh_name..sh_addr", if wide { 24 } else { 12 })?; + count = z.uword("sh_size", wide)?; + // SHN_XINDEX: section 0's sh_link holds the real shstrndx. + if shstrndx == 0xffff { + let mut z2 = r.slice_at("section header 0", off, want)?; + z2.skip("to sh_link", if wide { 40 } else { 24 })?; + index = z2.u32("sh_link")?; + } + } + caps.check("sections", count, caps.sections)?; + + let mut out = Vec::with_capacity(count as usize); + for i in 0..count { + let at = off + i * entsize as u64; + let mut s = match r.slice_at("section header", at, want) { + Ok(s) => s, + Err(e) => { + warnings.push(format!("section header {i}: {e}")); + break; + } + }; + out.push(SecHdr { + name_off: s.u32("sh_name")?, + kind: s.u32("sh_type")?, + flags: s.uword("sh_flags", wide)?, + addr: s.uword("sh_addr", wide)?, + offset: s.uword("sh_offset", wide)?, + size: s.uword("sh_size", wide)?, + link: s.u32("sh_link")?, + info: s.u32("sh_info")?, + addralign: s.uword("sh_addralign", wide)?, + entsize: s.uword("sh_entsize", wide)?, + }); + } + Ok((out, index)) +} + +fn perms_of(flags: u32) -> Perms { + Perms::new(flags & 4 != 0, flags & 2 != 0, flags & 1 != 0) +} + +/// Map PT_LOAD segments, naming each after the sections that fall inside it. +fn map_loadable( + r: &Reader<'_>, + phdrs: &[ProgHdr], + shdrs: &[SecHdr], + sections: &[Section], + opts: &LoadOptions, + warnings: &mut Vec, +) -> Result { + let mut mem = MemoryMap::new(); + let slide = opts.base.map(|b| b.get()).unwrap_or(0); + + for (i, p) in phdrs.iter().enumerate().filter(|(_, p)| p.kind == PT_LOAD) { + if p.memsz == 0 { + continue; + } + let vaddr = p.vaddr.wrapping_add(slide); + let Some(range) = AddrRange::sized(Addr(vaddr), p.memsz) else { + warnings.push(format!( + "PT_LOAD {i} at {vaddr:#x} for {:#x} bytes wraps the address space; skipped", + p.memsz + )); + continue; + }; + // filesz above memsz is a lie; take the smaller and say so. + let filesz = if p.filesz > p.memsz { + warnings.push(format!( + "PT_LOAD {i}: p_filesz {:#x} exceeds p_memsz {:#x}; truncated", + p.filesz, p.memsz + )); + p.memsz + } else { + p.filesz + }; + let data = match r.bytes_at("segment body", p.offset, filesz) { + Ok(b) => b.to_vec(), + Err(e) => { + warnings.push(format!("PT_LOAD {i}: {e}; mapped as zeros")); + Vec::new() + } + }; + let name = sections + .iter() + .filter(|s| { + !s.range.is_empty() && range.contains_range(s.range) && s.name.starts_with('.') + }) + .map(|s| s.name.as_str()) + .collect::>() + .join(","); + mem.add(Segment::new( + range, + perms_of(p.flags), + name, + p.offset, + data, + )?); + } + + // A file with sections but no loadable segments still deserves a map: a + // core dump, or a stripped-down object that kept SHF_ALLOC sections. + if mem.is_empty() && !shdrs.is_empty() { + warnings.push("no loadable segments; mapping allocated sections instead".into()); + for (sh, sec) in shdrs.iter().zip(sections) { + if sh.flags & SHF_ALLOC == 0 || sh.size == 0 { + continue; + } + let Some(range) = AddrRange::sized(Addr(sh.addr.wrapping_add(slide)), sh.size) else { + continue; + }; + let perms = Perms::new( + true, + sh.flags & SHF_WRITE != 0, + sh.flags & SHF_EXECINSTR != 0, + ); + let data = if sh.kind == SHT_NOBITS { + Vec::new() + } else { + r.bytes_at("section body", sh.offset, sh.size) + .map(|b| b.to_vec()) + .unwrap_or_default() + }; + mem.add(Segment::new( + range, + perms, + sec.name.clone(), + sh.offset, + data, + )?); + } + } + Ok(mem) +} + +/// Give a relocatable object's sections synthetic addresses so it can be +/// analyzed. Section order is preserved and alignment respected, which keeps +/// the layout deterministic and close to what a link would produce. +fn map_relocatable( + r: &Reader<'_>, + shdrs: &[SecHdr], + sections: &mut [Section], + opts: &LoadOptions, + warnings: &mut Vec, +) -> Result { + let mut mem = MemoryMap::new(); + let mut next = opts.base.unwrap_or(Addr(0x10_0000)); + + for (sh, sec) in shdrs.iter().zip(sections.iter_mut()) { + if sh.flags & SHF_ALLOC == 0 || sh.size == 0 { + continue; + } + // Checked: `addralign` is whatever the file says, and + // `next_power_of_two` panics rather than saturates above 2^63. + let Some(align) = sh.addralign.max(1).checked_next_power_of_two() else { + warnings.push(format!("section {}: implausible alignment", sec.name)); + continue; + }; + let Some(at) = next.align_up(align) else { + warnings.push(format!( + "section {} does not fit the address space", + sec.name + )); + continue; + }; + let Some(range) = AddrRange::sized(at, sh.size) else { + continue; + }; + let data = if sh.kind == SHT_NOBITS { + Vec::new() + } else { + match r.bytes_at("section body", sh.offset, sh.size) { + Ok(b) => b.to_vec(), + Err(e) => { + warnings.push(format!("section {}: {e}", sec.name)); + continue; + } + } + }; + let perms = Perms::new( + true, + sh.flags & SHF_WRITE != 0, + sh.flags & SHF_EXECINSTR != 0, + ); + mem.add(Segment::new( + range, + perms, + sec.name.clone(), + sh.offset, + data, + )?); + sec.range = range; + next = range.end(); + } + Ok(mem) +} + +/// Walk `.symtab` and `.dynsym`. +fn read_symbols( + r: &Reader<'_>, + shdrs: &[SecHdr], + obj: &mut Object, + caps: &e5r_core::Caps, +) -> Result> { + // Dynamic symbol names by their index in the table, including the entries + // that are skipped as symbols. A relocation names a symbol by that index, + // so a vector that drops entries would name the wrong one. + let mut dynsym_names: Vec = Vec::new(); + let wide = obj.bits == Bits::Bits64; + let entsize = if wide { 24 } else { 16 }; + + // Section index to the address it was mapped at, so a relocatable object's + // symbols land where its sections did. + let relocatable = obj.metadata.get("elf.type").map(String::as_str) == Some("relocatable"); + + for sh in shdrs + .iter() + .filter(|s| s.kind == SHT_SYMTAB || s.kind == SHT_DYNSYM) + { + let dynamic = sh.kind == SHT_DYNSYM; + let Some(strtab) = shdrs + .get(sh.link as usize) + .and_then(|s| r.slice_at("symbol strtab", s.offset, s.size).ok()) + else { + obj.warnings.push(format!( + "symbol table sh_link is {}, which is not a readable string table", + sh.link + )); + continue; + }; + let step = if sh.entsize >= entsize { + sh.entsize + } else { + entsize + }; + let count = sh.size / step; + caps.check("symbols", count, caps.symbols)?; + + if dynamic { + dynsym_names.resize(count as usize, String::new()); + } + for i in 0..count { + let Ok(mut s) = r.slice_at("symbol", sh.offset + i * step, entsize) else { + break; + }; + // ELF32 and ELF64 interleave these differently. + let (name_off, info, shndx, value, size) = if wide { + let n = s.u32("st_name")?; + let info = s.u8("st_info")?; + let _other = s.u8("st_other")?; + let shndx = s.u16("st_shndx")?; + (n, info, shndx, s.u64("st_value")?, s.u64("st_size")?) + } else { + let n = s.u32("st_name")?; + let value = s.u32("st_value")? as u64; + let size = s.u32("st_size")? as u64; + let info = s.u8("st_info")?; + let _other = s.u8("st_other")?; + (n, info, s.u16("st_shndx")?, value, size) + }; + if name_off == 0 && value == 0 && info == 0 { + continue; + } + let name = strtab + .cstr_at("symbol name", name_off as u64, caps.string_len) + .map(|b| String::from_utf8_lossy(b).into_owned()) + .unwrap_or_default(); + if dynamic { + if let Some(slot) = dynsym_names.get_mut(i as usize) { + slot.clone_from(&name); + } + } + if name.is_empty() && info >> 4 == 0 { + continue; + } + + let kind = match info & 0xf { + STT_FUNC | STT_GNU_IFUNC => SymbolKind::Function, + STT_OBJECT => SymbolKind::Object, + STT_SECTION => SymbolKind::Section, + STT_FILE => SymbolKind::File, + STT_TLS => SymbolKind::Tls, + _ if shndx == SHN_UNDEF => SymbolKind::Undefined, + _ => SymbolKind::Other, + }; + let binding = match info >> 4 { + 0 => Binding::Local, + 2 => Binding::Weak, + _ => Binding::Global, + }; + + // In a relocatable object st_value is an offset into its section. + let addr = if relocatable && shndx != SHN_UNDEF && (shndx as usize) < obj.sections.len() + { + Addr( + obj.sections[shndx as usize] + .range + .start() + .get() + .wrapping_add(value), + ) + } else { + Addr(value) + }; + let undefined = shndx == SHN_UNDEF; + + if undefined && !name.is_empty() { + obj.imports.push(Import { + name: name.clone(), + library: None, + thunk: None, + }); + } else if dynamic && binding != Binding::Local && !name.is_empty() && addr != Addr::ZERO + { + obj.exports.push(Export { + name: name.clone(), + addr, + ordinal: None, + }); + } + + if kind == SymbolKind::Function && !undefined && addr != Addr::ZERO { + obj.function_hints.push(FunctionHint { + addr, + size: (size != 0).then_some(size), + name: Some(name.clone()), + provenance: Provenance::new(if dynamic { + Evidence::DynamicSymbol + } else { + Evidence::SymbolTable + }), + }); + } else if kind == SymbolKind::Other + && !undefined + && addr != Addr::ZERO + && is_code_name(&name) + && in_executable_section(obj, addr) + { + // A symbol with no declared type sitting in an executable + // section names code: assemblers write them for hand-written + // entry points and linkers for wrappers. Claiming a function + // here is an inference, not a fact, and says so. + obj.function_hints.push(FunctionHint { + addr, + size: (size != 0).then_some(size), + name: Some(name.clone()), + provenance: Provenance::new(Evidence::CodeSymbol), + }); + } + + obj.symbols.push(Symbol { + name, + addr: if undefined { Addr::ZERO } else { addr }, + size, + kind: if undefined { + SymbolKind::Undefined + } else { + kind + }, + binding, + dynamic, + }); + } + } + Ok(dynsym_names) +} + +/// Read `.dynamic` for needed libraries, soname, and init/fini. +fn read_dynamic( + r: &Reader<'_>, + phdrs: &[ProgHdr], + shdrs: &[SecHdr], + obj: &mut Object, + caps: &e5r_core::Caps, +) { + let wide = obj.bits == Bits::Bits64; + let step = if wide { 16 } else { 8 }; + + let Some((off, size)) = phdrs + .iter() + .find(|p| p.kind == PT_DYNAMIC) + .map(|p| (p.offset, p.filesz)) + .or_else(|| { + shdrs + .iter() + .find(|s| s.kind == 6) + .map(|s| (s.offset, s.size)) + }) + else { + return; + }; + + // The string table .dynamic refers to by address; find the section that + // holds it rather than trusting DT_STRTAB's address to be mapped. + let dynstr = shdrs + .iter() + .find(|s| s.kind == SHT_STRTAB && s.flags & SHF_ALLOC != 0) + .and_then(|s| r.slice_at("dynstr", s.offset, s.size).ok()); + + let mut needed = Vec::new(); + for i in 0..(size / step) { + let Ok(mut d) = r.slice_at("dynamic entry", off + i * step, step) else { + break; + }; + let (Ok(tag), Ok(val)) = (d.uword("d_tag", wide), d.uword("d_val", wide)) else { + break; + }; + let name_of = |v: u64| { + dynstr + .and_then(|t| t.cstr_at("dynamic string", v, caps.string_len).ok()) + .map(|b| String::from_utf8_lossy(b).into_owned()) + }; + match tag { + DT_NULL => break, + DT_NEEDED => { + if let Some(n) = name_of(val) { + needed.push(n); + } + } + DT_SONAME => { + if let Some(n) = name_of(val) { + obj.metadata.insert("elf.soname".into(), n); + } + } + DT_RPATH | DT_RUNPATH => { + if let Some(n) = name_of(val) { + obj.metadata.insert("elf.runpath".into(), n); + } + } + DT_INIT | DT_FINI => { + if val != 0 { + obj.function_hints.push(FunctionHint { + addr: Addr(val), + size: None, + name: Some(if tag == DT_INIT { + "_init".into() + } else { + "_fini".into() + }), + provenance: Provenance::new(Evidence::InitArray), + }); + } + } + DT_PLTGOT => { + obj.metadata + .insert("elf.pltgot".into(), format!("{val:#x}")); + } + DT_FLAGS_1 if val & DF_1_PIE != 0 => { + obj.metadata.insert("elf.pie".into(), "true".into()); + } + _ => {} + } + } + if !needed.is_empty() { + for imp in &mut obj.imports { + if imp.library.is_none() && needed.len() == 1 { + imp.library = Some(needed[0].clone()); + } + } + obj.metadata.insert("elf.needed".into(), needed.join(", ")); + } +} + +/// `.init_array` and `.fini_array` hold pointers to functions that run before +/// and after main. Nothing else names them, so they are the only evidence for a +/// static constructor. +fn read_init_arrays( + r: &Reader<'_>, + shdrs: &[SecHdr], + wide: bool, + obj: &mut Object, + caps: &e5r_core::Caps, +) { + let step = if wide { 8 } else { 4 }; + for sh in shdrs + .iter() + .filter(|s| s.kind == SHT_INIT_ARRAY || s.kind == SHT_FINI_ARRAY) + { + let count = sh.size / step; + if caps + .check("init array entries", count, caps.symbols) + .is_err() + { + continue; + } + for i in 0..count { + let Ok(mut e) = r.slice_at("init array entry", sh.offset + i * step, step) else { + break; + }; + let Ok(v) = e.uword("pointer", wide) else { + break; + }; + // 0 and -1 are the "empty slot" conventions. + if v == 0 || v == u64::MAX || v == u32::MAX as u64 { + continue; + } + obj.function_hints.push(FunctionHint { + addr: Addr(v), + size: None, + name: None, + provenance: Provenance::new(Evidence::InitArray), + }); + } + } +} + +/// Map PLT entries to the names they call, so an indirect call prints a name. +/// +/// `.rela.plt` entries are in PLT slot order, and the PLT's first entry is the +/// resolver stub, so slot `i` of `.plt` corresponds to relocation `i - 1`. +fn read_plt_relocations( + r: &Reader<'_>, + shdrs: &[SecHdr], + wide: bool, + dynsym_names: &[String], + obj: &mut Object, + caps: &e5r_core::Caps, +) { + // `.plt.sec` where there is one. A binary built for indirect-branch + // tracking puts the address a call actually goes to there -- one 16-byte + // entry per relocation with no resolver header -- and leaves `.plt` as the + // lazy-resolution stubs behind it. objdump labels the `.plt.sec` entry + // ``, so naming `.plt` on such a file names the wrong addresses, + // and every indirect call through the PLT resolves to nothing. Ubuntu + // builds x86-64 this way by default; aarch64 has no such section, which is + // why the whole form went unnoticed here. + let sec = obj.section(".plt.sec").cloned(); + let Some(plt) = sec.clone().or_else(|| obj.section(".plt").cloned()) else { + return; + }; + if dynsym_names.is_empty() || plt.range.is_empty() { + return; + } + + // The PLT relocation table is the one named `.rela.plt` or `.rel.plt`. + // Finding it through `sh_info` does not work: that field names the section + // the relocations apply to, which is `.got.plt`, not the PLT. + let Some((sh, _)) = shdrs.iter().zip(&obj.sections).find(|(sh, sec)| { + (sh.kind == SHT_RELA || sh.kind == SHT_REL) + && (sec.name == ".rela.plt" || sec.name == ".rel.plt") + }) else { + return; + }; + + let is_rela = sh.kind == SHT_RELA; + let step = reloc_step(wide, is_rela); + let table_offset = sh.offset; + let declared = sh.size / step; + // Bounded by what fits in the file before anything is walked: a `sh_size` + // of 2^60 is a number, not a table. + let count = relocation_count(r, sh, step, caps); + if count == 0 { + return; + } + if declared > count { + obj.warnings.push(format!( + "the PLT relocation table declares {declared} entries but only {count} fit" + )); + } + + // i386 decodes its entries rather than counting them. Both forms begin + // with an indirect jump through a GOT slot, and that slot is exactly what + // a `R_386_JMP_SLOT` relocation names, so the pairing survives an IFUNC + // entry in the middle of `.plt`, which shifts every later slot under the + // index-order rule below. + if obj.arch == Arch::X86 + && name_i386_plt( + r, + table_offset, + step, + is_rela, + count, + dynsym_names, + &plt, + obj, + ) + { + return; + } + + // The PLT's layout is fixed per architecture: a resolver stub of one size + // followed by entries of another. Dividing the section's length by the + // relocation count instead gives the wrong answer whenever the section + // also holds IFUNC entries, which it usually does. + // `.plt.sec` is entries alone: the resolver stub stays in `.plt`. + let (header, entry_size) = match &sec { + Some(_) => (0, 16), + None => plt_layout(&obj.arch), + }; + // x86-64 `.plt.sec` is read the way i386 reads `.plt`: by decoding each + // entry to find the GOT slot it jumps through, and naming it from the + // relocation that slot belongs to. Index order does not survive here -- + // an IFUNC takes a slot in `.plt` and none in `.plt.sec`, so every + // IRELATIVE before a symbol shifts the rest by one -- and counting which + // relocations have a slot means walking the table twice, which on a + // corrupt file with a large declared count is the difference between a + // sweep and a hang. + if sec.is_some() + && obj.arch == Arch::X86_64 + && name_plt_sec(r, sh.offset, step, is_rela, count, dynsym_names, &plt, obj) + { + return; + } + if plt.range.len() < header + count * entry_size { + obj.warnings.push(format!( + ".plt is {:#x} bytes, too small for {count} entries of {entry_size:#x} \ + after a {header:#x}-byte header; thunks not named", + plt.range.len() + )); + return; + } + + for i in 0..count { + let Ok(mut e) = r.slice_at("relocation", sh.offset + i * step, step) else { + break; + }; + if e.uword("r_offset", wide).is_err() { + break; + } + let sym_index = if wide { + let Ok(info) = e.u64("r_info") else { break }; + (info >> 32) as usize + } else { + let Ok(info) = e.u32("r_info") else { break }; + (info >> 8) as usize + }; + let Some(name) = dynsym_names.get(sym_index).filter(|n| !n.is_empty()) else { + continue; + }; + let thunk = plt + .range + .start() + .checked_add(header + i * entry_size) + .filter(|a| plt.range.contains(*a)); + if let Some(imp) = obj.imports.iter_mut().find(|im| im.name == *name) { + imp.thunk = thunk; + } + if let Some(t) = thunk { + obj.function_hints.push(FunctionHint { + addr: t, + size: Some(entry_size), + name: Some(format!("{name}@plt")), + provenance: Provenance::new(Evidence::ImportThunk), + }); + } + } +} + +/// How many entries of `size` bytes a section can really hold. +/// +/// `sh_size` is a number from the file, and a corrupt one makes a PLT that +/// claims four gigabytes inside a four-kilobyte image. Dividing that by an +/// entry width gives a loop of a quarter of a billion iterations, each one a +/// bounds check that fails -- eighteen seconds on a dev build, which is how +/// this was found. The image is what the file actually provided, so it is the +/// bound. +fn entries_within(obj: &Object, sec: &Section, size: u64) -> u64 { + let Some(image) = obj.memory.bounds() else { + return 0; + }; + if sec.range.start() >= image.end() { + return 0; + } + let reach = image.end().get() - sec.range.start().get(); + sec.range.len().min(reach) / size +} + +/// Name the entries of an x86-64 `.plt.sec` by what each one jumps through. +/// +/// One entry is sixteen bytes: `endbr64`, then a `jmp *disp(%rip)` that may +/// carry the `bnd` prefix, then padding. The displacement is relative to the +/// end of the jump, and what it reaches is the GOT slot a `JUMP_SLOT` +/// relocation names -- so the pairing is by address and survives anything the +/// linker put in between. +/// +/// Returns false when nothing was named, leaving the caller to try `.plt`. +#[allow(clippy::too_many_arguments)] +fn name_plt_sec( + r: &Reader<'_>, + table_offset: u64, + step: u64, + is_rela: bool, + count: u64, + dynsym_names: &[String], + plt: &Section, + obj: &mut Object, +) -> bool { + const R_X86_64_JUMP_SLOT: u64 = 7; + const ENTRY: u64 = 16; + + let mut by_slot: Vec<(u64, String)> = Vec::new(); + for i in 0..count { + let Some(rel) = read_reloc(r, table_offset + i * step, true, is_rela) else { + break; + }; + if rel.kind != R_X86_64_JUMP_SLOT { + continue; + } + let Some(name) = dynsym_names + .get(rel.symbol as usize) + .filter(|n| !n.is_empty()) + else { + continue; + }; + by_slot.push((rel.offset, name.clone())); + } + if by_slot.is_empty() { + return false; + } + by_slot.sort_by_key(|(slot, _)| *slot); + + let mut found: Vec<(u64, String)> = Vec::new(); + for i in 0..entries_within(obj, plt, ENTRY) { + let at = plt.range.start().get().wrapping_add(i * ENTRY); + let Some(code) = obj.memory.slice(Addr(at), ENTRY) else { + continue; + }; + // `endbr64` is what puts these entries in their own section, but the + // form without it is still a `.plt.sec` the linker may emit. + let mut off = if code.starts_with(&[0xf3, 0x0f, 0x1e, 0xfa]) { + 4 + } else { + 0 + }; + if code.get(off) == Some(&0xf2) { + off += 1; // `bnd` + } + if code.get(off) != Some(&0xff) || code.get(off + 1) != Some(&0x25) { + continue; + } + let Some(rel32) = code.get(off + 2..off + 6) else { + continue; + }; + let disp = i32::from_le_bytes([rel32[0], rel32[1], rel32[2], rel32[3]]) as i64; + // Relative to the end of the jump, which is where the program counter + // stands when it is executed. + let after = at.wrapping_add(off as u64 + 6); + let slot = after.wrapping_add(disp as u64); + if let Ok(k) = by_slot.binary_search_by_key(&slot, |(s, _)| *s) { + found.push((at, by_slot[k].1.clone())); + } + } + if found.is_empty() { + return false; + } + for (at, name) in found { + if let Some(imp) = obj.imports.iter_mut().find(|im| im.name == name) { + imp.thunk = Some(Addr(at)); + } + obj.function_hints.push(FunctionHint { + addr: Addr(at), + size: Some(ENTRY), + name: Some(format!("{name}@plt")), + provenance: Provenance::new(Evidence::ImportThunk), + }); + } + true +} + +/// Where `_GLOBAL_OFFSET_TABLE_` is, for the `%ebx`-relative form of a PLT +/// entry. +/// +/// `DT_PLTGOT` is the authority and the linker always emits it; the symbol and +/// the section are there for an image whose dynamic section was stripped. +fn i386_got_base(obj: &Object) -> Option { + if let Some(v) = obj + .metadata + .get("elf.pltgot") + .and_then(|v| u64::from_str_radix(v.trim_start_matches("0x"), 16).ok()) + { + return Some(v); + } + if let Some(s) = obj + .symbols + .iter() + .find(|s| s.name == "_GLOBAL_OFFSET_TABLE_") + { + return Some(s.addr.get()); + } + obj.section(".got.plt") + .or_else(|| obj.section(".got")) + .map(|s| s.range.start().get()) +} + +/// Name i386 PLT entries by decoding the slot each one jumps through. +/// +/// Returns false when nothing could be named, so the caller falls back to the +/// index-order rule. The two entry shapes are `jmp *abs32` (`ff 25`) in a +/// position-dependent image and `jmp *disp32(%ebx)` (`ff a3`) in a position +/// independent one, either optionally behind an `endbr32`. +#[allow(clippy::too_many_arguments)] +fn name_i386_plt( + r: &Reader<'_>, + table_offset: u64, + step: u64, + is_rela: bool, + count: u64, + dynsym_names: &[String], + plt: &Section, + obj: &mut Object, +) -> bool { + // Slot address to the name the relocation gives it. A linear scan: a PLT + // with thousands of entries is a few thousand comparisons once. + let mut by_slot: Vec<(u64, String)> = Vec::with_capacity(count as usize); + for i in 0..count { + let Some(rel) = read_reloc(r, table_offset + i * step, false, is_rela) else { + break; + }; + if rel.kind != R_386_JMP_SLOT { + continue; + } + let Some(name) = dynsym_names + .get(rel.symbol as usize) + .filter(|n| !n.is_empty()) + else { + continue; + }; + by_slot.push((rel.offset, name.clone())); + } + if by_slot.is_empty() { + return false; + } + // Sorted so the lookup per entry is a search rather than a scan: a PLT of + // a few thousand entries would otherwise be quadratic. + by_slot.sort_by_key(|(slot, _)| *slot); + let got = i386_got_base(obj); + + const ENTRY: u64 = 16; + let entries = entries_within(obj, plt, ENTRY); + let mut found = Vec::new(); + for i in 0..entries { + let at = plt.range.start().get().wrapping_add(i * ENTRY); + let Some(code) = obj.memory.slice(Addr(at), 10) else { + continue; + }; + // An IBT-enabled PLT puts `endbr32` in front of the jump. + let body = if code.starts_with(&[0xf3, 0x0f, 0x1e, 0xfb]) { + &code[4..] + } else { + code + }; + if body.len() < 6 || body[0] != 0xff { + continue; + } + let imm = u32::from_le_bytes([body[2], body[3], body[4], body[5]]) as u64; + let slot = match body[1] { + // jmp *abs32, the position-dependent form. + 0x25 => imm, + // jmp *disp32(%ebx), where %ebx holds the GOT base. + 0xa3 => match got { + Some(g) => g.wrapping_add(imm) & 0xffff_ffff, + None => continue, + }, + _ => continue, + }; + let Ok(k) = by_slot.binary_search_by_key(&slot, |(s, _)| *s) else { + continue; + }; + found.push((Addr(at), by_slot[k].1.clone())); + } + if found.is_empty() { + return false; + } + obj.metadata.insert("elf.plt.form".into(), "decoded".into()); + for (at, name) in found { + if let Some(imp) = obj.imports.iter_mut().find(|im| im.name == name) { + imp.thunk = Some(at); + } + obj.function_hints.push(FunctionHint { + addr: at, + size: Some(ENTRY), + name: Some(format!("{name}@plt")), + provenance: Provenance::new(Evidence::ImportThunk), + }); + } + true +} + +/// The size of a PLT's resolver stub and of each entry after it. +fn plt_layout(arch: &Arch) -> (u64, u64) { + match arch { + Arch::AArch64 => (32, 16), + Arch::X86_64 | Arch::X86 => (16, 16), + Arch::Arm | Arch::Thumb => (20, 12), + _ => (16, 16), + } +} + +/// Build id and any other note worth reporting. +fn read_notes(r: &Reader<'_>, phdrs: &[ProgHdr], shdrs: &[SecHdr], obj: &mut Object) { + if let Some(p) = phdrs.iter().find(|p| p.kind == PT_INTERP) { + if let Ok(t) = r.slice_at("interp", p.offset, p.filesz) { + if let Ok(s) = t.cstr_at("interp", 0, p.filesz) { + obj.metadata + .insert("elf.interp".into(), String::from_utf8_lossy(s).into_owned()); + } + } + } + if phdrs + .iter() + .any(|p| p.kind == PT_GNU_STACK && p.flags & 1 != 0) + { + obj.metadata.insert("elf.exec_stack".into(), "true".into()); + } + if phdrs.iter().any(|p| p.kind == PT_GNU_RELRO) { + obj.metadata.insert("elf.relro".into(), "true".into()); + } + + let notes = phdrs + .iter() + .filter(|p| p.kind == PT_NOTE) + .map(|p| (p.offset, p.filesz)) + .chain( + shdrs + .iter() + .filter(|s| s.kind == 7) + .map(|s| (s.offset, s.size)), + ); + for (off, size) in notes { + let Ok(mut n) = r.slice_at("note", off, size) else { + continue; + }; + while n.remaining() >= 12 { + let (Ok(namesz), Ok(descsz), Ok(kind)) = + (n.u32("n_namesz"), n.u32("n_descsz"), n.u32("n_type")) + else { + break; + }; + let Ok(name) = n.bytes("note name", (namesz as u64).next_multiple_of(4)) else { + break; + }; + let Ok(desc) = n.bytes("note desc", (descsz as u64).next_multiple_of(4)) else { + break; + }; + // NT_GNU_BUILD_ID + if kind == 3 && name.starts_with(b"GNU\0") { + let hex: String = desc[..descsz.min(64) as usize] + .iter() + .map(|b| format!("{b:02x}")) + .collect(); + obj.metadata.insert("elf.build_id".into(), hex); + } + } + } +} + +/// Function starts from `.eh_frame`, the best boundary evidence an ELF carries. +/// Read what the compiler recorded, when it recorded anything. +/// +/// A function the debug information names is a fact, not an inference, so the +/// hint it produces outranks everything the analysis would work out for +/// itself. +fn read_debug_info( + r: &Reader<'_>, + shdrs: &[SecHdr], + obj: &mut Object, + hints: bool, + caps: &e5r_core::Caps, +) { + let section = |name: &str| -> &[u8] { + match obj.section(name) { + Some(s) if s.file_size > 0 => r + .bytes_at("debug section", s.file_offset, s.file_size) + .unwrap_or(&[]), + _ => &[], + } + }; + // A relocatable object writes zero where an address goes and leaves a + // relocation to fill it in, so every function would appear to start at the + // same place until they are applied. + let info_bytes = relocated(r, shdrs, obj, caps, ".debug_info", section(".debug_info")); + let line_bytes = relocated(r, shdrs, obj, caps, ".debug_line", section(".debug_line")); + // DWARF 5 puts the addresses in a table of their own and refers to them by + // index, so that table needs its relocations as much as the rest: without + // them every function in the unit resolves to zero. + let addr_bytes = relocated(r, shdrs, obj, caps, ".debug_addr", section(".debug_addr")); + // The string offsets are relocated too: an index resolves through this + // table, and unrelocated it points every name at the first string in the + // section, which is the producer. + let str_offset_bytes = relocated( + r, + shdrs, + obj, + caps, + ".debug_str_offsets", + section(".debug_str_offsets"), + ); + let rnglist_bytes = relocated( + r, + shdrs, + obj, + caps, + ".debug_rnglists", + section(".debug_rnglists"), + ); + // The range and location lists carry addresses too, so a relocatable + // object needs them applied here for the same reason `.debug_addr` does. + let range_bytes = relocated( + r, + shdrs, + obj, + caps, + ".debug_ranges", + section(".debug_ranges"), + ); + let loclist_bytes = relocated( + r, + shdrs, + obj, + caps, + ".debug_loclists", + section(".debug_loclists"), + ); + let loc_bytes = relocated(r, shdrs, obj, caps, ".debug_loc", section(".debug_loc")); + let sections = crate::dwarf::Sections { + info: &info_bytes, + abbrev: section(".debug_abbrev"), + str: section(".debug_str"), + line_str: section(".debug_line_str"), + str_offsets: &str_offset_bytes, + + addr: &addr_bytes, + rnglists: &rnglist_bytes, + ranges: &range_bytes, + loclists: &loclist_bytes, + loc: &loc_bytes, + line: &line_bytes, + }; + if sections.is_empty() { + return; + } + let info = crate::dwarf::parse(§ions, obj.endian); + for (addr, f) in info.functions.iter().filter(|_| hints) { + if f.name.is_empty() { + continue; + } + obj.function_hints.push(FunctionHint { + addr: *addr, + size: f.size.filter(|s| *s > 0), + name: Some(f.name.clone()), + provenance: Provenance::new(Evidence::DebugInfo), + }); + } + obj.debug = Some(info); +} + +/// How many relocations a section really holds. +/// +/// The declared size is a number from the file and can say anything; what +/// bounds the work is how many entries actually fit in the file, and then the +/// cap. A `sh_size` of 7x10^17 divided by an entry size is not a table, it is +/// 87 seconds of a test run. +fn relocation_count(r: &Reader<'_>, sh: &SecHdr, step: u64, caps: &e5r_core::Caps) -> u64 { + let step = step.max(1); + let declared = sh.size / step; + let available = (r.len() as u64).saturating_sub(sh.offset) / step; + declared.min(available).min(caps.relocations) +} + +/// Bytes per entry. `REL` has no addend field, `RELA` does, and both double +/// between the classes. +fn reloc_step(wide: bool, rela: bool) -> u64 { + match (wide, rela) { + (true, true) => 24, + (true, false) => 16, + (false, true) => 12, + (false, false) => 8, + } +} + +/// One relocation, after the class and table kind have been read away. +struct Reloc { + offset: u64, + kind: u64, + symbol: u64, + /// Present only for `RELA`. A `REL` entry's addend is in the bytes being + /// patched and cannot be read until the place is known. + explicit_addend: Option, +} + +/// Read one entry of a `REL` or `RELA` table. +fn read_reloc(r: &Reader<'_>, at: u64, wide: bool, rela: bool) -> Option { + let mut e = r.slice_at("relocation", at, reloc_step(wide, rela)).ok()?; + let (offset, info, addend) = if wide { + let o = e.u64("r_offset").ok()?; + let i = e.u64("r_info").ok()?; + let a = rela.then(|| e.i64("r_addend")).transpose().ok()?; + (o, i, a) + } else { + let o = e.u32("r_offset").ok()? as u64; + let i = e.u32("r_info").ok()? as u64; + let a = rela.then(|| e.i32("r_addend")).transpose().ok()?; + (o, i, a.map(|v| v as i64)) + }; + Some(Reloc { + offset, + kind: if wide { + info & 0xffff_ffff + } else { + info & 0xff + }, + symbol: if wide { info >> 32 } else { info >> 8 }, + explicit_addend: addend, + }) +} + +/// The outcome of computing one relocation. +enum Fixup { + /// Bytes to write at the place. + Write(Vec), + /// Understood, and writes nothing: `R_386_NONE`, and `R_386_COPY`, which + /// the dynamic loader performs by copying at run time. + Nothing, + /// Not implemented. Counted by type number rather than dropped, so the + /// gap is visible instead of looking like a clean load. + Unhandled, +} + +/// What an i386 fixup needs beyond the relocation itself. +/// +/// The psABI writes its formulas in terms of `S`, `A`, `P`, `B`, `G` and +/// `GOT`; this carries the three that are not properties of the relocation. +#[derive(Default)] +struct RelocContext { + /// Where `_GLOBAL_OFFSET_TABLE_` sits. A relocatable object has no GOT, + /// so one is invented below and this is where it was put. + got: u64, + /// Offset from the GOT base of this symbol's slot, when it has one. + got_slot: Option, + /// `st_size`, which one relocation writes rather than reads. + size: u64, + /// `B`, where the image was loaded. + base: u64, +} + +/// Apply the relocations that name code and data addresses. +/// +/// Only where the symbol resolves to something this file defines: an external +/// symbol has no address here, and writing a guess would produce a call graph +/// that points somewhere wrong rather than nowhere. +fn apply_code_relocations( + r: &Reader<'_>, + shdrs: &[SecHdr], + obj: &mut Object, + caps: &e5r_core::Caps, +) { + let wide = obj.bits == Bits::Bits64; + let arch = obj.arch.clone(); + let base = obj.image_base.get(); + + // i386 code reaches its own data through the GOT, and a relocatable object + // has no GOT: the link makes one. Make one here too, so that the `%ebx` + // the `R_386_GOTPC` establishes and the `@GOTOFF` displacements measured + // from it agree, and a `@GOT` slot holds the address it would hold. + let got_layout = (arch == Arch::X86) + .then(|| plan_got(r, shdrs, obj, caps)) + .flatten(); + let got = got_layout.as_ref().map(|g| g.base).unwrap_or(0); + + let mut applied: u64 = 0; + let mut unresolved: u64 = 0; + let mut unhandled: BTreeMap = BTreeMap::new(); + let mut patches: Vec<(Addr, Vec)> = Vec::new(); + + for sh in shdrs + .iter() + .filter(|s| s.kind == SHT_RELA || s.kind == SHT_REL) + { + let rela = sh.kind == SHT_RELA; + let step = reloc_step(wide, rela); + let Some(target) = obj.sections.get(sh.info as usize).cloned() else { + continue; + }; + // Any section that is mapped: a jump table lives in `.rodata` and is + // as much in need of its relocations as the code that reads it. + if target.range.is_empty() || target.name.starts_with(".debug") { + continue; + } + let count = relocation_count(r, sh, step, caps); + let declared = sh.size / step.max(1); + if declared > count { + // Warn rather than error: the rest of the table is still readable + // and the file is still worth analyzing. + obj.warnings.push(format!( + "{} declares {declared} relocations but only {count} fit; the rest are ignored", + target.name + )); + } + for i in 0..count { + let Some(rel) = read_reloc(r, sh.offset + i * step, wide, rela) else { + break; + }; + // Where the fixup goes, which is an offset into the section the + // relocation names. + let place = target.range.start().get().wrapping_add(rel.offset); + // However much is there: the last entry of a table sits at the + // end of its section, and asking for eight bytes there fails. + let mut word = [0u8; 8]; + let available = (1..=8).rev().find_map(|n| obj.memory.slice(Addr(place), n)); + let Some(existing) = available else { continue }; + word[..existing.len()].copy_from_slice(existing); + + // The order of operations that `REL` forces. A `RELA` entry hands + // over its addend before anything is read from the image; a `REL` + // entry's addend is whatever the assembler already encoded at the + // place, so the width has to be known and the bytes read first. + // Reading it after an earlier patch landed would add the addend + // twice, which is why every patch is held back to the end. + let width = reloc_width(&arch, rel.kind); + let addend = match rel.explicit_addend { + Some(a) => a, + None => match implicit_addend(existing, width) { + Some(a) => a, + None => continue, + }, + }; + + let value = symbol_value(r, shdrs, obj, rel.symbol); + if value.is_none() && needs_symbol(&arch, rel.kind) { + unresolved += 1; + continue; + } + let cx = RelocContext { + got, + got_slot: got_layout.as_ref().and_then(|g| g.slot(rel.symbol)), + // Read only for the one type that writes a size: every other + // relocation would pay a second symbol table read for nothing. + size: match (&arch, rel.kind) { + (Arch::X86, R_386_SIZE32) => { + symbol_size(r, shdrs, obj, rel.symbol).unwrap_or(0) + } + _ => 0, + }, + base, + }; + match fixup( + &arch, + rel.kind, + value.unwrap_or(0), + addend, + place, + &word, + &cx, + ) { + Fixup::Write(bytes) => { + applied += 1; + patches.push((Addr(place), bytes)); + } + Fixup::Nothing => applied += 1, + Fixup::Unhandled => *unhandled.entry(rel.kind).or_default() += 1, + } + } + } + + if let Some(g) = got_layout { + g.map_into(obj); + } + for (at, bytes) in patches { + obj.memory.patch(at, &bytes); + } + + obj.metadata + .insert("elf.relocations.applied".into(), applied.to_string()); + if unresolved > 0 { + obj.metadata + .insert("elf.relocations.unresolved".into(), unresolved.to_string()); + } + if !unhandled.is_empty() { + let listed = unhandled + .iter() + .map(|(kind, n)| format!("type {kind}: {n}")) + .collect::>() + .join(", "); + obj.metadata + .insert("elf.relocations.unhandled".into(), listed.clone()); + obj.warnings + .push(format!("relocations not applied ({listed})")); + } +} + +/// The bytes a relocation writes, which for a `REL` table is also the width of +/// the addend already sitting there. +fn reloc_width(arch: &Arch, kind: u64) -> u64 { + match arch { + Arch::X86 => match kind { + R_386_8 | R_386_PC8 => 1, + R_386_16 | R_386_PC16 => 2, + _ => 4, + }, + _ => 4, + } +} + +/// The addend a `REL` entry does not carry, sign extended from the bytes being +/// patched. i386 is little endian in every configuration that exists. +fn implicit_addend(existing: &[u8], width: u64) -> Option { + match width { + 1 => Some(*existing.first()? as i8 as i64), + 2 => Some(i16::from_le_bytes([*existing.first()?, *existing.get(1)?]) as i64), + _ => { + let b = existing.get(..4)?; + Some(i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as i64) + } + } +} + +/// True when the formula for this type reads `S`. +/// +/// Three of them do not. `R_386_GOTPC` names `_GLOBAL_OFFSET_TABLE_`, which is +/// undefined in every relocatable object that uses it, so requiring a value +/// would drop exactly the relocation that establishes the GOT base. The two +/// `GOT32` forms write a slot offset, which is known whether or not the slot's +/// eventual contents are: `outside_data@GOT(%ebx)` is a real address in this +/// image even when `outside_data` is defined in some other file. +fn needs_symbol(arch: &Arch, kind: u64) -> bool { + match arch { + Arch::X86 => !matches!( + kind, + R_386_NONE + | R_386_COPY + | R_386_GOTPC + | R_386_GOT32 + | R_386_GOT32X + | R_386_RELATIVE + | R_386_IRELATIVE + ), + _ => true, + } +} + +/// Which symbols need a GOT slot, and where the GOT goes. +struct GotPlan { + base: u64, + /// Symbol index to slot offset. Offsets are handed out in first-use + /// order, so the layout is a function of the file; the map is ordered, so + /// the lookup does not turn a large table quadratic. + slots: BTreeMap, + /// Slot contents, already laid out. + bytes: Vec, +} + +impl GotPlan { + fn slot(&self, symbol: u64) -> Option { + self.slots.get(&symbol).copied() + } + + /// Map the invented GOT, so a load through a slot reads the address the + /// link would have put there rather than failing as unmapped. + fn map_into(self, obj: &mut Object) { + // The base is recorded whether or not there are slots: `@GOTOFF` and + // `@GOTPC` are measured from it and appear in code that never loads a + // slot, and a displacement whose origin is unstated is one nobody can + // check. + obj.metadata + .insert("elf.got_base".into(), format!("{:#x}", self.base)); + if self.bytes.is_empty() { + return; + } + let Some(range) = AddrRange::sized(Addr(self.base), self.bytes.len() as u64) else { + return; + }; + // No file offset: nothing in the file backs it. Named so that anything + // reporting an address inside it says where it came from. + if let Ok(seg) = Segment::new( + range, + Perms::new(true, true, false), + ".got.synthetic", + 0, + self.bytes, + ) { + obj.memory.add(seg); + } + } +} + +/// Lay out a GOT for a relocatable i386 object. +/// +/// It goes past everything the sections were mapped at, so it collides with +/// nothing, and the slots are assigned in the order the relocations mention +/// them, which makes the layout deterministic. +fn plan_got( + r: &Reader<'_>, + shdrs: &[SecHdr], + obj: &Object, + caps: &e5r_core::Caps, +) -> Option { + // Past everything the sections were mapped at, so it collides with + // nothing. `checked_`, because the end of the map is where a caller's + // chosen load address put it and that can be anywhere. + let base = obj + .memory + .bounds() + .map(|b| b.end().get()) + .unwrap_or(0x10_0000) + .checked_next_multiple_of(16)?; + let mut slots: BTreeMap = BTreeMap::new(); + // True once anything in the file measures from the GOT base. + let mut wanted = false; + for sh in shdrs + .iter() + .filter(|s| s.kind == SHT_RELA || s.kind == SHT_REL) + { + let rela = sh.kind == SHT_RELA; + let step = reloc_step(false, rela); + for i in 0..relocation_count(r, sh, step, caps) { + let Some(rel) = read_reloc(r, sh.offset + i * step, false, rela) else { + break; + }; + if matches!(rel.kind, R_386_GOTOFF | R_386_GOTPC) { + wanted = true; + } + if !matches!(rel.kind, R_386_GOT32 | R_386_GOT32X) { + continue; + } + wanted = true; + let off = slots.len() as u64 * 4; + slots.entry(rel.symbol).or_insert(off); + } + } + // An object that never mentions the GOT gets no GOT, and no invented + // address range to go with it. + if !wanted { + return None; + } + let mut bytes = vec![0u8; slots.len() * 4]; + for (symbol, off) in &slots { + // An undefined symbol has no address here, so its slot stays zero: + // the slot's own address is still correct, which is what the code + // computing `x@GOT(%ebx)` needs. + let value = symbol_value(r, shdrs, obj, *symbol).unwrap_or(0) as u32; + let at = *off as usize; + if let Some(slot) = bytes.get_mut(at..at + 4) { + slot.copy_from_slice(&value.to_le_bytes()); + } + } + Some(GotPlan { base, slots, bytes }) +} + +/// The bytes one relocation writes, or `Unhandled` when its kind is not one +/// this understands. +fn fixup( + arch: &Arch, + kind: u64, + symbol: u64, + addend: i64, + place: u64, + existing: &[u8; 8], + cx: &RelocContext, +) -> Fixup { + let value = symbol.wrapping_add(addend as u64); + let relative = value.wrapping_sub(place); + let word = u32::from_le_bytes([existing[0], existing[1], existing[2], existing[3]]); + + match arch { + Arch::X86 => fixup_i386(kind, symbol, addend, place, cx), + Arch::X86_64 => Fixup::Write(match kind { + // R_X86_64_64. + 1 => value.to_le_bytes().to_vec(), + // PC32 and PLT32, which differ only in whether a stub may be used. + 2 | 4 => (relative as u32).to_le_bytes().to_vec(), + // The 32-bit absolute forms. + 10 | 11 => (value as u32).to_le_bytes().to_vec(), + 0 => return Fixup::Nothing, + _ => return Fixup::Unhandled, + }), + Arch::AArch64 => Fixup::Write(match kind { + // ABS64. + 257 => value.to_le_bytes().to_vec(), + // ABS32. + 258 => (value as u32).to_le_bytes().to_vec(), + // PREL64 and PREL32. + 260 => relative.to_le_bytes().to_vec(), + 261 => (relative as u32).to_le_bytes().to_vec(), + // ADR_PREL_PG_HI21: the page difference, in the split immediate an + // `adrp` carries. + 275 | 276 => { + let pages = (value & !0xfff).wrapping_sub(place & !0xfff) as i64 >> 12; + let immlo = (pages as u32 & 3) << 29; + let immhi = ((pages as u32 >> 2) & 0x7ffff) << 5; + ((word & !0x60ff_ffe0) | immlo | immhi) + .to_le_bytes() + .to_vec() + } + // The twelve-bit offsets: an add, or a load or store scaled by its + // access size. + 277 => { + let imm = (value & 0xfff) as u32; + ((word & !0x003f_fc00) | (imm << 10)).to_le_bytes().to_vec() + } + 278 | 284 | 285 | 286 | 299 => { + let scale = match kind { + 278 => 0, + 284 => 1, + 285 => 2, + 286 => 3, + _ => 4, + }; + let imm = ((value & 0xfff) >> scale) as u32; + ((word & !0x003f_fc00) | (imm << 10)).to_le_bytes().to_vec() + } + // JUMP26 and CALL26: the branch displacement in instructions. + 282 | 283 => { + let imm = ((relative as i64 >> 2) as u32) & 0x03ff_ffff; + ((word & !0x03ff_ffff) | imm).to_le_bytes().to_vec() + } + // CONDBR19 and the test-and-branch form. + 280 => { + let imm = ((relative as i64 >> 2) as u32 & 0x7ffff) << 5; + ((word & !0x00ff_ffe0) | imm).to_le_bytes().to_vec() + } + 0 => return Fixup::Nothing, + _ => return Fixup::Unhandled, + }), + _ => Fixup::Unhandled, + } +} + +/// The bytes one i386 relocation writes. +/// +/// The psABI's formulas, with `S` the symbol, `A` the addend, `P` the place, +/// `B` the load address, `G` the symbol's offset in the GOT and `GOT` the GOT +/// base. The one liberty is `L`, the PLT entry: a relocatable object has no +/// PLT, so a call that asked for a stub resolves to the symbol itself, which +/// is what a link needing no stub leaves behind. +fn fixup_i386(kind: u64, s: u64, a: i64, p: u64, cx: &RelocContext) -> Fixup { + let a = a as u64; + let (width, value): (usize, u64) = match kind { + R_386_NONE | R_386_COPY => return Fixup::Nothing, + // S + A. + R_386_32 | R_386_32PLT => (4, s.wrapping_add(a)), + // S + A - P. + R_386_PC32 | R_386_PLT32 => (4, s.wrapping_add(a).wrapping_sub(p)), + // G + A, the slot's offset from the GOT base. + R_386_GOT32 | R_386_GOT32X => match cx.got_slot { + Some(g) => (4, g.wrapping_add(a)), + None => return Fixup::Unhandled, + }, + // S, written into a GOT slot by the dynamic loader. + R_386_GLOB_DAT | R_386_JMP_SLOT => (4, s), + // B + A. + R_386_RELATIVE | R_386_IRELATIVE => (4, cx.base.wrapping_add(a)), + // S + A - GOT. + R_386_GOTOFF => (4, s.wrapping_add(a).wrapping_sub(cx.got)), + // GOT + A - P. + R_386_GOTPC => (4, cx.got.wrapping_add(a).wrapping_sub(p)), + // The symbol's size rather than its address. + R_386_SIZE32 => (4, cx.size.wrapping_add(a)), + R_386_16 => (2, s.wrapping_add(a)), + R_386_PC16 => (2, s.wrapping_add(a).wrapping_sub(p)), + R_386_8 => (1, s.wrapping_add(a)), + R_386_PC8 => (1, s.wrapping_add(a).wrapping_sub(p)), + // Every thread-local form. A relocatable object has no thread block + // and no module id, so there is no address to write; the count says + // so rather than the bytes claiming a wrong one. + _ => return Fixup::Unhandled, + }; + Fixup::Write(value.to_le_bytes()[..width].to_vec()) +} + +/// A copy of a section with its relocations applied. +/// +/// Only the absolute kinds, which is all debug information uses: the value +/// written is the symbol's address plus the addend. Anything else is left +/// alone, because writing a guess into a debug section produces confident +/// nonsense rather than a gap. +fn relocated( + r: &Reader<'_>, + shdrs: &[SecHdr], + obj: &Object, + caps: &e5r_core::Caps, + name: &str, + data: &[u8], +) -> Vec { + let mut out = data.to_vec(); + if data.is_empty() { + return out; + } + let Some(target) = obj.sections.iter().position(|s| s.name == name) else { + return out; + }; + let wide = obj.bits == Bits::Bits64; + + for sh in shdrs.iter() { + if (sh.kind != SHT_RELA && sh.kind != SHT_REL) || sh.info as usize != target { + continue; + } + let rela = sh.kind == SHT_RELA; + let step = reloc_step(wide, rela); + for i in 0..relocation_count(r, sh, step, caps) { + let Some(rel) = read_reloc(r, sh.offset + i * step, wide, rela) else { + continue; + }; + // The absolute relocations, which are the only ones a debug + // section uses: 1 is 64-bit on both 64-bit architectures, and the + // 32-bit ones differ by number. On i386 the number 1 is the + // 32-bit absolute form instead. + let size = match (&obj.arch, rel.kind) { + (Arch::X86_64, 1) | (Arch::AArch64, 257) => 8, + (Arch::X86_64, 10) | (Arch::X86_64, 11) | (Arch::AArch64, 258) => 4, + (Arch::X86, R_386_32) => 4, + _ => continue, + }; + // The offset comes from the file, so it can be anything. + let Ok(start) = usize::try_from(rel.offset) else { + continue; + }; + let Some(end) = start.checked_add(size) else { + continue; + }; + // `REL` again: the addend is the value the assembler already wrote + // at the place. It is read out of the untouched `data` rather than + // out of `out`, so an earlier fixup at the same offset cannot be + // counted into this one's addend. + let addend = match rel.explicit_addend { + Some(a) => a, + None => match data + .get(start..end) + .and_then(|b| implicit_addend(b, size as u64)) + { + Some(a) => a, + None => continue, + }, + }; + let Some(value) = symbol_value(r, shdrs, obj, rel.symbol) else { + continue; + }; + let result = value.wrapping_add(addend as u64); + let Some(slot) = out.get_mut(start..end) else { + continue; + }; + for (k, byte) in slot.iter_mut().enumerate() { + *byte = (result >> (k * 8)) as u8; + } + } + } + out +} + +/// One symbol table entry, read for what a relocation needs from it. +struct SymEntry { + value: u64, + size: u64, + shndx: u16, +} + +fn symbol_entry(r: &Reader<'_>, shdrs: &[SecHdr], wide: bool, index: u64) -> Option { + let entsize: u64 = if wide { 24 } else { 16 }; + let sh = shdrs.iter().find(|s| s.kind == SHT_SYMTAB)?; + // A symbol index from the file is not bounded by the table; asking for an + // entry past the end has to fail rather than read the next section. + if index >= sh.size / entsize { + return None; + } + let at = sh.offset.checked_add(index.checked_mul(entsize)?)?; + let mut e = r.slice_at("symbol", at, entsize).ok()?; + // ELF32 and ELF64 interleave these differently. + if wide { + let _name = e.u32("st_name").ok()?; + let _info = e.u8("st_info").ok()?; + let _other = e.u8("st_other").ok()?; + let shndx = e.u16("st_shndx").ok()?; + let value = e.u64("st_value").ok()?; + let size = e.u64("st_size").ok()?; + Some(SymEntry { value, size, shndx }) + } else { + let _name = e.u32("st_name").ok()?; + let value = e.u32("st_value").ok()? as u64; + let size = e.u32("st_size").ok()? as u64; + let _info = e.u8("st_info").ok()?; + let _other = e.u8("st_other").ok()?; + let shndx = e.u16("st_shndx").ok()?; + Some(SymEntry { value, size, shndx }) + } +} + +/// The address a relocation's symbol resolves to, which for a section symbol +/// is where the loader put that section. +/// +/// An undefined symbol resolves to nothing. Section index zero is a real +/// section header with address zero, so without this the answer would be a +/// confident zero and every call to an external function would be relocated +/// to point at address zero rather than left as the compiler wrote it. +fn symbol_value(r: &Reader<'_>, shdrs: &[SecHdr], obj: &Object, index: u64) -> Option { + let e = symbol_entry(r, shdrs, obj.bits == Bits::Bits64, index)?; + if e.shndx == SHN_UNDEF { + return None; + } + Some(section_base(obj, e.shndx)?.wrapping_add(e.value)) +} + +/// `st_size`, which `R_386_SIZE32` writes instead of an address. +fn symbol_size(r: &Reader<'_>, shdrs: &[SecHdr], obj: &Object, index: u64) -> Option { + symbol_entry(r, shdrs, obj.bits == Bits::Bits64, index).map(|e| e.size) +} + +fn section_base(obj: &Object, index: u16) -> Option { + obj.sections + .get(index as usize) + .map(|s| s.range.start().get()) +} + +fn read_eh_frame(r: &Reader<'_>, phdrs: &[ProgHdr], obj: &mut Object) { + let Some(sec) = obj.section(".eh_frame").cloned() else { + // A stripped binary can still have PT_GNU_EH_FRAME pointing at the + // lookup table even when section headers are gone. + let _ = phdrs.iter().find(|p| p.kind == PT_GNU_EH_FRAME); + return; + }; + if sec.file_size == 0 { + return; + } + let Ok(body) = r.bytes_at(".eh_frame", sec.file_offset, sec.file_size) else { + return; + }; + match crate::ehframe::function_starts(body, sec.range.start(), obj.bits) { + Ok(fns) => { + for (addr, size) in fns { + obj.function_hints.push(FunctionHint { + addr, + size: (size != 0).then_some(size), + name: None, + provenance: Provenance::new(Evidence::EhFrame), + }); + } + } + Err(e) => obj.warnings.push(format!(".eh_frame: {e}")), + } +} + +/// True when a name is one a function could have. +/// +/// ARM and AArch64 write mapping symbols — `$x` for code, `$d` for data — at +/// every transition, and they are not functions. Nor are the assembler's local +/// labels. +fn is_code_name(name: &str) -> bool { + !name.is_empty() && !name.starts_with('$') && !name.starts_with(".L") +} + +/// True when an address falls inside a section the loader marked executable. +fn in_executable_section(obj: &Object, addr: Addr) -> bool { + obj.sections + .iter() + .any(|s| s.exec && s.range.contains(addr)) +} diff --git a/crates/e5r-format/src/lib.rs b/crates/e5r-format/src/lib.rs new file mode 100644 index 0000000..4072465 --- /dev/null +++ b/crates/e5r-format/src/lib.rs @@ -0,0 +1,334 @@ +//! Container loaders. +//! +//! Every loader produces the same [`Object`]: a memory map, symbols, and +//! function hints tagged with where they came from. Nothing above this crate +//! learns whether the bytes arrived as an ELF segment, a PE section, or a raw +//! blob with a base address. +//! +//! Loaders are written here rather than delegated to a parsing crate because +//! provenance tagging and behaviour on hostile input are the product. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod archive; +pub mod dwarf; +pub mod ehframe; +pub mod elf; +pub mod macho; +pub mod metadata; +pub mod overlay; +pub mod pdata; +pub mod pdb; +pub mod pe; +pub mod raw; +pub mod swift; +pub mod windirs; + +use std::collections::BTreeMap; + +use e5r_core::{Addr, AddrRange, Arch, Bits, Caps, Endian, Error, MemoryMap, Provenance, Result}; +use serde::{Deserialize, Serialize}; + +/// Which container a file turned out to be. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Format { + /// ELF, any class or byte order. + Elf, + /// PE or COFF. + Pe, + /// Mach-O, thin or one slice of a fat file. + MachO, + /// No container: bytes at a base address the caller chose. + Raw, +} + +impl Format { + /// The name used in output and JSON. + pub fn as_str(self) -> &'static str { + match self { + Format::Elf => "elf", + Format::Pe => "pe", + Format::MachO => "macho", + Format::Raw => "raw", + } + } +} + +impl std::fmt::Display for Format { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// What a symbol names. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SymbolKind { + /// Code. + Function, + /// Data. + Object, + /// A section. + Section, + /// A source file name. + File, + /// A thread-local object. + Tls, + /// Declared but not defined here. + Undefined, + /// Something else the container names. + Other, +} + +/// How visible a symbol is outside its object. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Binding { + /// Visible only within the object. + Local, + /// Visible everywhere. + Global, + /// Global, but a strong definition overrides it. + Weak, +} + +/// A name the container attaches to an address. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Symbol { + /// The name as the file spells it, still mangled. + pub name: String, + /// Where it lives. Undefined symbols sit at zero. + pub addr: Addr, + /// Declared size, zero when the container does not say. + pub size: u64, + /// What it names. + pub kind: SymbolKind, + /// How widely it is visible. + pub binding: Binding, + /// True when the name came from a dynamic table rather than a static one. + pub dynamic: bool, +} + +impl Symbol { + /// True when the symbol names code at a real address. + pub fn is_defined_function(&self) -> bool { + self.kind == SymbolKind::Function && self.addr != Addr::ZERO + } +} + +/// A named region the container declares. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Section { + /// Name as the container spells it. + pub name: String, + /// Where it is mapped, empty when it is not mapped at all. + pub range: AddrRange, + /// Where its bytes are in the file. + pub file_offset: u64, + /// How many bytes are in the file, which is zero for `.bss`. + pub file_size: u64, + /// True when the container marks it executable. + pub exec: bool, + /// True when the container marks it writable. + pub write: bool, + /// Container-specific type number, for reporting. + pub kind: u32, +} + +/// A symbol this object needs from somewhere else. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Import { + /// The symbol name. + pub name: String, + /// The library it should come from, when the container says. + pub library: Option, + /// The address of the thunk or slot that reaches it, when known. This is + /// what makes an indirect call print a name. + pub thunk: Option, +} + +/// A symbol this object offers to others. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Export { + /// The symbol name. + pub name: String, + /// Where it points. + pub addr: Addr, + /// Ordinal, for containers that have them. + pub ordinal: Option, +} + +/// An address analysis should treat as a function entry, and why. +/// +/// Loaders emit these instead of a bare address list so the strongest evidence +/// can win and disagreements stay visible. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FunctionHint { + /// Where the function starts. + pub addr: Addr, + /// Its size, when the source of the hint knew one. + pub size: Option, + /// Name from the same source, when there was one. + pub name: Option, + /// What said so. + pub provenance: Provenance, +} + +/// A loaded container. +#[derive(Debug, Clone)] +pub struct Object { + /// Which container it was. + pub format: Format, + /// Machine it targets. + pub arch: Arch, + /// Byte order. + pub endian: Endian, + /// Pointer width. + pub bits: Bits, + /// Declared entry point, absent for objects and libraries that have none. + pub entry: Option, + /// Address the container prefers to be loaded at. + pub image_base: Addr, + /// True when the image is position independent, so addresses are offsets. + pub pic: bool, + /// The bytes, by address. + pub memory: MemoryMap, + /// Declared sections, in container order. + pub sections: Vec
, + /// Every symbol, sorted by address then name. + pub symbols: Vec, + /// Symbols needed from elsewhere. + pub imports: Vec, + /// Symbols offered to others. + pub exports: Vec, + /// Function entries with their evidence, strongest first. + pub function_hints: Vec, + /// Things worth reporting that do not fit elsewhere: build id, interpreter, + /// needed libraries, compiler notes. + pub metadata: BTreeMap, + /// What DWARF said, when the file carried it. + pub debug: Option, + /// Problems found while loading that did not stop the load. A hostile file + /// is still worth analyzing, and the complaints belong in the report. + pub warnings: Vec, +} + +impl Object { + /// Symbol covering `addr`, preferring the closest start at or below it. + pub fn symbol_at(&self, addr: Addr) -> Option<&Symbol> { + let i = self.symbols.partition_point(|s| s.addr <= addr); + self.symbols[..i] + .iter() + .rev() + // The distance rather than the sum: the size comes from the file, + // and one large enough to wrap makes the comparison say a symbol + // covers an address nowhere near it. + .find(|s| { + s.addr <= addr && (s.size == 0 || addr.get().wrapping_sub(s.addr.get()) < s.size) + }) + } + + /// Section containing `addr`. + pub fn section_at(&self, addr: Addr) -> Option<&Section> { + self.sections + .iter() + .find(|s| !s.range.is_empty() && s.range.contains(addr)) + } + + /// Section by name. + pub fn section(&self, name: &str) -> Option<&Section> { + self.sections.iter().find(|s| s.name == name) + } + + /// Merge hints for the same address and sort strongest first, then by + /// address. Deterministic regardless of the order loaders emitted them. + pub fn normalize_hints(&mut self) { + self.function_hints.sort_by_key(|h| h.addr); + let mut out: Vec = Vec::with_capacity(self.function_hints.len()); + for h in self.function_hints.drain(..) { + match out.last_mut() { + Some(prev) if prev.addr == h.addr => { + prev.provenance.merge(&h.provenance); + if prev.size.is_none() { + prev.size = h.size; + } + if prev.name.is_none() { + prev.name = h.name; + } + } + _ => out.push(h), + } + } + out.sort_by(|a, b| a.provenance.rank(&b.provenance).then(a.addr.cmp(&b.addr))); + self.function_hints = out; + } + + /// Sort symbols by address, then name, so output does not depend on the + /// order the tables were walked. + pub fn normalize_symbols(&mut self) { + self.symbols + .sort_by(|a, b| a.addr.cmp(&b.addr).then_with(|| a.name.cmp(&b.name))); + self.symbols.dedup_by(|a, b| { + a.addr == b.addr && a.name == b.name && a.kind == b.kind && a.dynamic == b.dynamic + }); + } +} + +/// Options a caller can set before loading. +#[derive(Debug, Clone)] +pub struct LoadOptions { + /// Resource caps. + pub caps: Caps, + /// Base address for a raw image, or a rebase for a relocatable one. + pub base: Option, + /// Architecture for a raw image, which has no header to say. + pub arch: Option, + /// Read `.eh_frame` for function starts. On by default; it is the best + /// boundary evidence an ELF offers and costs one pass. + pub eh_frame: bool, + /// Read DWARF debug information when it is present. On by default: what + /// the compiler knew about names and types cannot be recovered any other + /// way, so it is worth the pass over the sections. + pub debug_info: bool, +} + +impl Default for LoadOptions { + fn default() -> Self { + LoadOptions { + caps: Caps::default(), + base: None, + arch: None, + eh_frame: true, + debug_info: true, + } + } +} + +/// Identify and load a file. +/// +/// Probing order is by magic, not by extension. A loader that says "not my +/// format" lets the next one try; any other error stops, because a file that +/// announces itself as ELF and then fails is a broken ELF, not a PE. +pub fn load(data: &[u8], opts: &LoadOptions) -> Result { + archive::refuse_in_load(data)?; + match elf::load(data, opts) { + Err(e) if e.is_not_recognized() => {} + other => return other, + } + match macho::load(data, opts) { + Err(e) if e.is_not_recognized() => {} + other => return other, + } + match pe::load(data, opts) { + Err(e) if e.is_not_recognized() => {} + other => return other, + } + if opts.arch.is_some() { + return raw::load(data, opts); + } + Err(Error::NotRecognized { + expected: "recognized container (ELF, Mach-O or PE); pass an architecture to load it raw", + }) +} diff --git a/crates/e5r-format/src/macho.rs b/crates/e5r-format/src/macho.rs new file mode 100644 index 0000000..b96df3d --- /dev/null +++ b/crates/e5r-format/src/macho.rs @@ -0,0 +1,651 @@ +//! Mach-O, thin and fat. +//! +//! A fat file is a big-endian index of thin ones; the requested architecture is +//! selected, or the first slice if none was asked for. +//! +//! `LC_FUNCTION_STARTS` is why this format is worth reading carefully: it lists +//! every function's start as a chain of ULEB128 deltas, it survives stripping, +//! and nothing else in a stripped Mach-O says where the functions are. + +use std::collections::BTreeMap; + +use e5r_core::{ + Addr, AddrRange, Arch, Bits, Caps, Endian, Error, Evidence, MemoryMap, Perms, Provenance, + Reader, Result, Segment, +}; + +use crate::{ + Binding, Export, Format, FunctionHint, Import, LoadOptions, Object, Section, Symbol, SymbolKind, +}; + +const MH_MAGIC_64: u32 = 0xfeed_facf; +const MH_CIGAM_64: u32 = 0xcffa_edfe; +const MH_MAGIC_32: u32 = 0xfeed_face; +const MH_CIGAM_32: u32 = 0xcefa_edfe; +const FAT_MAGIC: u32 = 0xcafe_babe; +const FAT_MAGIC_64: u32 = 0xcafe_babf; + +// Load commands. +const LC_SEGMENT: u32 = 0x01; +const LC_SYMTAB: u32 = 0x02; +const LC_SEGMENT_64: u32 = 0x19; +const LC_LOAD_DYLIB: u32 = 0x0c; +const LC_ID_DYLIB: u32 = 0x0d; +const LC_UUID: u32 = 0x1b; +const LC_FUNCTION_STARTS: u32 = 0x26; +const LC_MAIN: u32 = 0x8000_0028; + +// File types. +const MH_OBJECT: u32 = 1; +const MH_EXECUTE: u32 = 2; +const MH_DYLIB: u32 = 6; +const MH_BUNDLE: u32 = 8; +const MH_DSYM: u32 = 10; + +// Header flags. +const MH_PIE: u32 = 0x0020_0000; + +// nlist n_type. +const N_STAB: u8 = 0xe0; +const N_TYPE: u8 = 0x0e; +const N_EXT: u8 = 0x01; +const N_UNDF: u8 = 0x0; +const N_SECT: u8 = 0xe; + +// Section flags: the low byte is the section type. +const S_ATTR_PURE_INSTRUCTIONS: u32 = 0x8000_0000; +const S_ATTR_SOME_INSTRUCTIONS: u32 = 0x0000_0400; +const S_ZEROFILL: u32 = 0x1; + +/// True when the buffer starts with any Mach-O magic, thin or fat. +pub fn is_macho(data: &[u8]) -> bool { + if data.len() < 4 { + return false; + } + let le = u32::from_le_bytes([data[0], data[1], data[2], data[3]]); + let be = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); + matches!(le, MH_MAGIC_64 | MH_CIGAM_64 | MH_MAGIC_32 | MH_CIGAM_32) + || matches!(be, FAT_MAGIC | FAT_MAGIC_64) +} + +/// Parse a Mach-O file. +pub fn load(data: &[u8], opts: &LoadOptions) -> Result { + if !is_macho(data) { + return Err(Error::NotRecognized { expected: "Mach-O" }); + } + let head = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); + if matches!(head, FAT_MAGIC | FAT_MAGIC_64) { + return load_fat(data, opts, head == FAT_MAGIC_64); + } + load_thin(data, opts, 0) +} + +/// Pick a slice out of a fat file and load it. +/// +/// The index is big-endian whatever the slices are, which is the one place a +/// reader of this format has to change byte order mid-file. +fn load_fat(data: &[u8], opts: &LoadOptions, wide: bool) -> Result { + let r = Reader::new(data, Endian::Big); + let mut h = r; + h.skip("fat magic", 4)?; + let count = h.u32("nfat_arch")?; + opts.caps.check("fat slices", count as u64, 64)?; + + let step = if wide { 32 } else { 20 }; + let mut first: Option = None; + for i in 0..count as u64 { + let mut a = r.slice_at("fat_arch", 8 + i * step, step)?; + let cputype = a.u32("cputype")? as i32; + let _sub = a.u32("cpusubtype")?; + let offset = a.uword("offset", wide)?; + let _size = a.uword("size", wide)?; + let arch = cpu_to_arch(cputype); + if first.is_none() { + first = Some(offset); + } + if opts.arch.as_ref().is_some_and(|want| *want == arch) { + return load_thin(data, opts, offset); + } + } + let at = first.ok_or(Error::BadField { + field: "nfat_arch", + value: 0, + reason: "means the file contains no slices at all", + })?; + let mut obj = load_thin(data, opts, at)?; + if count > 1 { + obj.metadata.insert( + "macho.fat".into(), + format!("{count} slices; loaded the first, pass --arch to choose"), + ); + } + Ok(obj) +} + +fn cpu_to_arch(cputype: i32) -> Arch { + match cputype { + 0x0100_0007 => Arch::X86_64, + 7 => Arch::X86, + 0x0100_000c => Arch::AArch64, + 12 => Arch::Arm, + other => Arch::Unknown(other as u32), + } +} + +fn file_type_name(t: u32) -> &'static str { + match t { + MH_OBJECT => "object", + MH_EXECUTE => "executable", + MH_DYLIB => "dylib", + MH_BUNDLE => "bundle", + MH_DSYM => "debug symbols", + _ => "unknown", + } +} + +/// Parse one thin Mach-O, starting at `base` in the file. +fn load_thin(data: &[u8], opts: &LoadOptions, base: u64) -> Result { + let caps = &opts.caps; + let mut warnings = Vec::new(); + + let magic = Reader::le(data).slice_at("magic", base, 4)?.u32("magic")?; + let (wide, endian) = match magic { + MH_MAGIC_64 => (true, Endian::Little), + MH_CIGAM_64 => (true, Endian::Big), + MH_MAGIC_32 => (false, Endian::Little), + MH_CIGAM_32 => (false, Endian::Big), + other => { + return Err(Error::BadField { + field: "mach_header magic", + value: other as u64, + reason: "is not a Mach-O magic number", + }); + } + }; + + let r = Reader::new(data, endian); + let mut h = r; + h.seek("mach_header", base + 4)?; + let cputype = h.u32("cputype")? as i32; + let _cpusubtype = h.u32("cpusubtype")?; + let filetype = h.u32("filetype")?; + let ncmds = h.u32("ncmds")?; + let sizeofcmds = h.u32("sizeofcmds")?; + let flags = h.u32("flags")?; + if wide { + h.u32("reserved")?; + } + let cmds_at = h.pos(); + caps.check("load commands", ncmds as u64, caps.sections)?; + + let arch = cpu_to_arch(cputype); + let bits = if wide { Bits::Bits64 } else { Bits::Bits32 }; + + let mut memory = MemoryMap::new(); + let mut sections: Vec
= Vec::new(); + let mut metadata: BTreeMap = BTreeMap::new(); + let mut symbols: Vec = Vec::new(); + let mut imports: Vec = Vec::new(); + let mut exports: Vec = Vec::new(); + let mut hints: Vec = Vec::new(); + let mut dylibs: Vec = Vec::new(); + let mut entry: Option = None; + let mut text_start: Option = None; + let mut function_starts: Option<(u64, u64)> = None; + let mut symtab: Option<(u32, u32, u32, u32)> = None; + // An object file lays every section at address zero, as ELF and COFF do. + let relocatable = filetype == MH_OBJECT; + let mut next_free = opts.base.map(|b| b.get()).unwrap_or(0x1_0000_0000); + + let mut at = cmds_at; + let end = cmds_at + sizeofcmds as u64; + for _ in 0..ncmds { + if at >= end { + break; + } + let Ok(mut c) = r.slice_at("load command", at, 8) else { + warnings.push("load commands run past the end of the file".into()); + break; + }; + let cmd = c.u32("cmd")?; + let size = c.u32("cmdsize")? as u64; + if size < 8 { + warnings.push(format!("load command {cmd:#x} claims {size} bytes")); + break; + } + let Ok(body) = r.slice_at("load command body", at, size) else { + break; + }; + at += size; + + match cmd { + LC_SEGMENT | LC_SEGMENT_64 => { + let seg_wide = cmd == LC_SEGMENT_64; + let mut s = body; + s.skip("cmd and cmdsize", 8)?; + let name = fixed_name(s.bytes("segname", 16)?); + let vmaddr = s.uword("vmaddr", seg_wide)?; + let vmsize = s.uword("vmsize", seg_wide)?; + let fileoff = s.uword("fileoff", seg_wide)?; + let filesize = s.uword("filesize", seg_wide)?; + let maxprot = s.u32("maxprot")?; + let initprot = s.u32("initprot")?; + let nsects = s.u32("nsects")?; + let _segflags = s.u32("flags")?; + let _ = (vmsize, maxprot); + + // A segment with no sections still maps, which is how + // __PAGEZERO and __LINKEDIT appear. + if nsects == 0 && filesize > 0 && !relocatable { + if let Some(range) = AddrRange::sized(Addr(vmaddr), filesize) { + let body = r + .bytes_at("segment body", base + fileoff, filesize) + .map(|b| b.to_vec()) + .unwrap_or_default(); + memory.add(Segment::new( + range, + prot(initprot), + name.clone(), + base + fileoff, + body, + )?); + } + } + + caps.check("sections", nsects as u64, caps.sections)?; + for _ in 0..nsects { + let sect_size = if seg_wide { 80 } else { 68 }; + let Ok(mut sec) = s.slice_at("section", s.pos(), sect_size) else { + break; + }; + s.skip("section", sect_size)?; + let sectname = fixed_name(sec.bytes("sectname", 16)?); + let segname = fixed_name(sec.bytes("segname", 16)?); + let addr = sec.uword("addr", seg_wide)?; + let size = sec.uword("size", seg_wide)?; + let offset = sec.u32("offset")? as u64; + let align = sec.u32("align")?; + let _reloff = sec.u32("reloff")?; + let _nreloc = sec.u32("nreloc")?; + let sflags = sec.u32("flags")?; + + let exec = sflags & (S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS) != 0; + let zerofill = sflags & 0xff == S_ZEROFILL; + let full = format!("{segname},{sectname}"); + + let va = if relocatable { + // A relocatable object gives every section address + // zero, so they are laid out here. The size is the + // file's to choose, and one large enough to wrap the + // address space would place the next section back + // inside this one: a later section would then appear + // to contain the earlier one's code. + let alignment = 1u64 << align.min(16); + let Some(a) = next_free.checked_next_multiple_of(alignment.max(1)) else { + warnings.push(format!("section {full} does not fit the address space")); + continue; + }; + let Some(end) = a.checked_add(size) else { + warnings.push(format!("section {full} does not fit the address space")); + continue; + }; + next_free = end; + a + } else { + addr + }; + let Some(range) = AddrRange::sized(Addr(va), size) else { + warnings.push(format!("section {full} does not fit the address space")); + continue; + }; + let file_size = if zerofill { 0 } else { size }; + let body = if zerofill { + Vec::new() + } else { + // A section whose bytes are not in the file is worth + // saying: without this the object reports a section of + // the declared size holding nothing, and a caller that + // trusts `file_size` gets a wrong answer with no + // signal. The ELF loader already warns at this point. + match r.bytes_at("section body", base + offset, file_size) { + Ok(b) => b.to_vec(), + Err(e) => { + warnings.push(format!("section {full}: {e}")); + Vec::new() + } + } + }; + if !range.is_empty() { + let perms = if relocatable { + Perms::new(true, !exec, exec) + } else { + prot(initprot) + }; + memory.add(Segment::new( + range, + perms, + full.clone(), + base + offset, + body, + )?); + } + if full == "__TEXT,__text" { + text_start = Some(Addr(va)); + } + sections.push(Section { + name: full, + range, + file_offset: base + offset, + file_size, + exec, + write: !relocatable && prot(initprot).write, + kind: sflags, + }); + } + if name == "__TEXT" && text_start.is_none() { + text_start = Some(Addr(vmaddr)); + } + } + LC_SYMTAB => { + let mut s = body; + s.skip("cmd and cmdsize", 8)?; + symtab = Some(( + s.u32("symoff")?, + s.u32("nsyms")?, + s.u32("stroff")?, + s.u32("strsize")?, + )); + } + LC_FUNCTION_STARTS => { + let mut s = body; + s.skip("cmd and cmdsize", 8)?; + function_starts = Some((s.u32("dataoff")? as u64, s.u32("datasize")? as u64)); + } + LC_MAIN => { + let mut s = body; + s.skip("cmd and cmdsize", 8)?; + let entryoff = s.u64("entryoff")?; + // LC_MAIN's offset is from the start of the file, which for a + // loaded image is the start of __TEXT. + entry = text_start + .or(Some(Addr::ZERO)) + .and_then(|t| t.checked_add(entryoff)); + } + LC_LOAD_DYLIB | LC_ID_DYLIB => { + let mut s = body; + s.skip("cmd and cmdsize", 8)?; + let name_off = s.u32("name offset")? as u64; + if let Ok(n) = body.cstr_at("dylib name", name_off, caps.string_len) { + let n = String::from_utf8_lossy(n).into_owned(); + if cmd == LC_ID_DYLIB { + metadata.insert("macho.id".into(), n); + } else { + dylibs.push(n); + } + } + } + LC_UUID => { + if let Ok(u) = body.bytes_at("uuid", 8, 16) { + metadata.insert( + "macho.uuid".into(), + u.iter().map(|b| format!("{b:02x}")).collect(), + ); + } + } + _ => {} + } + } + + metadata.insert("macho.type".into(), file_type_name(filetype).into()); + if flags & MH_PIE != 0 { + metadata.insert("macho.pie".into(), "true".into()); + } + if !dylibs.is_empty() { + metadata.insert("macho.dylibs".into(), dylibs.join(", ")); + } + + let mut obj = Object { + debug: None, + format: Format::MachO, + arch, + endian, + bits, + entry, + image_base: text_start.unwrap_or(Addr::ZERO), + pic: flags & MH_PIE != 0 || filetype == MH_DYLIB, + memory, + sections, + symbols: Vec::new(), + imports: Vec::new(), + exports: Vec::new(), + function_hints: Vec::new(), + metadata, + warnings, + }; + + if let Some((symoff, nsyms, stroff, strsize)) = symtab { + read_symbols( + &r, + base, + wide, + symoff, + nsyms, + stroff, + strsize, + relocatable, + &mut obj, + &mut symbols, + &mut imports, + &mut exports, + &mut hints, + caps, + ); + } + if let Some((off, size)) = function_starts { + read_function_starts(&r, base, off, size, text_start, &mut hints, &mut obj, caps); + } + + obj.symbols = symbols; + obj.imports = imports; + obj.exports = exports; + obj.function_hints = hints; + if let Some(e) = obj.entry { + obj.function_hints.push(FunctionHint { + addr: e, + size: None, + name: Some("main".into()), + provenance: Provenance::new(Evidence::EntryPoint), + }); + } + obj.normalize_symbols(); + obj.normalize_hints(); + Ok(obj) +} + +fn prot(initprot: u32) -> Perms { + Perms::new(initprot & 1 != 0, initprot & 2 != 0, initprot & 4 != 0) +} + +/// A fixed-width name field, which is NUL-padded rather than NUL-terminated. +fn fixed_name(b: &[u8]) -> String { + let end = b.iter().position(|c| *c == 0).unwrap_or(b.len()); + String::from_utf8_lossy(&b[..end]).into_owned() +} + +#[allow(clippy::too_many_arguments)] +fn read_symbols( + r: &Reader<'_>, + base: u64, + wide: bool, + symoff: u32, + nsyms: u32, + stroff: u32, + strsize: u32, + relocatable: bool, + obj: &mut Object, + symbols: &mut Vec, + imports: &mut Vec, + exports: &mut Vec, + hints: &mut Vec, + caps: &Caps, +) { + if caps.check("symbols", nsyms as u64, caps.symbols).is_err() { + obj.warnings + .push(format!("{nsyms} symbols exceeds the cap; skipped")); + return; + } + let Ok(strings) = r.slice_at("string table", base + stroff as u64, strsize as u64) else { + obj.warnings.push("string table is not readable".into()); + return; + }; + let step = if wide { 16 } else { 12 }; + + for i in 0..nsyms as u64 { + let Ok(mut s) = r.slice_at("nlist", base + symoff as u64 + i * step, step) else { + break; + }; + let (Ok(strx), Ok(ntype), Ok(nsect), Ok(_desc)) = ( + s.u32("n_strx"), + s.u8("n_type"), + s.u8("n_sect"), + s.u16("n_desc"), + ) else { + break; + }; + let Ok(value) = s.uword("n_value", wide) else { + break; + }; + // Debug symbols describe source, not code. + if ntype & N_STAB != 0 { + continue; + } + let name = strings + .cstr_at("symbol name", strx as u64, caps.string_len) + .map(|b| String::from_utf8_lossy(b).into_owned()) + .unwrap_or_default(); + if name.is_empty() { + continue; + } + // Mach-O prefixes C symbols with an underscore. + let clean = name.strip_prefix('_').unwrap_or(&name).to_string(); + + let kind_bits = ntype & N_TYPE; + let external = ntype & N_EXT != 0; + let undefined = kind_bits == N_UNDF; + + let addr = if undefined { + Addr::ZERO + } else if relocatable && nsect > 0 { + // In an object the value is an offset into its section. + match obj.sections.get(nsect as usize - 1) { + Some(sec) => Addr(sec.range.start().get().wrapping_add(value)), + None => Addr(value), + } + } else { + Addr(value) + }; + + let in_code = addr != Addr::ZERO && obj.memory.is_executable(addr); + let kind = if undefined { + SymbolKind::Undefined + } else if in_code { + SymbolKind::Function + } else { + SymbolKind::Object + }; + + if undefined { + imports.push(Import { + name: clean.clone(), + library: None, + thunk: None, + }); + } else if external && kind_bits == N_SECT { + exports.push(Export { + name: clean.clone(), + addr, + ordinal: None, + }); + } + if in_code { + // An assembler temporary marks a place, not a function, and letting + // one name a function hides the real name at the same address. + let temporary = name.starts_with("ltmp") + || name.starts_with("l_") + || name.starts_with("L") + || clean.starts_with("ltmp"); + hints.push(FunctionHint { + addr, + size: None, + name: (!temporary).then(|| clean.clone()), + provenance: Provenance::new(Evidence::SymbolTable), + }); + } + + symbols.push(Symbol { + name: clean, + addr, + size: 0, + kind, + binding: if external { + Binding::Global + } else { + Binding::Local + }, + dynamic: false, + }); + } +} + +/// `LC_FUNCTION_STARTS`: a chain of ULEB128 deltas from the text segment. +/// +/// The best boundary evidence a Mach-O carries, and the only one a stripped +/// binary keeps. +#[allow(clippy::too_many_arguments)] +fn read_function_starts( + r: &Reader<'_>, + base: u64, + off: u64, + size: u64, + text_start: Option, + hints: &mut Vec, + obj: &mut Object, + caps: &Caps, +) { + let Some(start) = text_start else { + obj.warnings + .push("LC_FUNCTION_STARTS with no __TEXT to measure from".into()); + return; + }; + let Ok(mut d) = r.slice_at("function starts", base + off, size) else { + return; + }; + let mut at = start.get(); + let mut count = 0u64; + while d.remaining() > 0 { + let Ok(delta) = d.uleb128("function start delta") else { + break; + }; + // A zero delta terminates the chain. + if delta == 0 { + break; + } + let Some(next) = at.checked_add(delta) else { + break; + }; + at = next; + count += 1; + if caps.check("function starts", count, caps.symbols).is_err() { + break; + } + let addr = Addr(at); + if obj.memory.is_executable(addr) { + hints.push(FunctionHint { + addr, + size: None, + name: None, + provenance: Provenance::new(Evidence::MachFunctionStarts), + }); + } + } +} diff --git a/crates/e5r-format/src/metadata.rs b/crates/e5r-format/src/metadata.rs new file mode 100644 index 0000000..7037dc1 --- /dev/null +++ b/crates/e5r-format/src/metadata.rs @@ -0,0 +1,1000 @@ +//! Runtime metadata: what a language's own runtime has to find at run time. +//! +//! A stripped Go binary still carries `pclntab`, because every traceback and +//! every `runtime.Caller` reads it; an Objective-C binary still carries its +//! class and method lists, because `objc_msgSend` dispatches through them. +//! Metadata the program itself needs cannot be stripped, which is what makes +//! it the best evidence a stripped binary offers. +//! +//! Rust is the poor relation: it records nothing for reflection. What it does +//! leave behind is a `core::panic::Location` for every bounds check and every +//! `unwrap`, naming the source file, line and column, and those are worth +//! recovering even though they name no function. +//! +//! Nothing here trusts a count. Every table is clamped to the bytes it sits in +//! before one entry is read, the rule `dwarf.rs` follows, because these tables +//! are found by scanning and a scan lands on hostile bytes by construction. + +use std::collections::BTreeMap; + +use e5r_core::{Addr, Bits, Endian, Error, Evidence, Provenance, Reader, Result}; + +use crate::{FunctionHint, Object}; + +/// Longest name any of these tables may spell. A table whose string has no +/// terminator would otherwise walk the rest of the image. +pub(crate) const MAX_NAME: u64 = 1 << 12; + +/// Tables to attempt before giving up. A file can be full of bytes that look +/// like a `pclntab` magic, and each one costs a header validation. +const MAX_CANDIDATES: u32 = 64; + +/// What the three readers found, and what they could not read. +#[derive(Debug, Clone, Default)] +pub struct Metadata { + /// Function entries recovered, with the evidence of the reader that found + /// them. + pub hints: Vec, + /// Go's `pclntab`, when the image had one. + pub go: Option, + /// Rust panic locations, sorted by the address of the record. + pub panics: Vec, + /// Objective-C classes, in class-list order. + pub classes: Vec, + /// Swift reflection metadata, when the image carried any. + pub swift: Option, + /// Facts for the object's metadata map. + pub notes: BTreeMap, + /// What could not be read. Reported rather than guessed at. + pub warnings: Vec, +} + +/// Run all three readers over a loaded image. +pub fn read(obj: &Object) -> Metadata { + let mut out = Metadata::default(); + + if let Some(go) = go_pclntab(obj) { + out.notes + .insert("go.pclntab".into(), go.version.as_str().into()); + out.notes + .insert("go.functions".into(), go.functions.len().to_string()); + if let Some(v) = &go.build_version { + out.notes.insert("go.version".into(), v.clone()); + } + if let Some(m) = &go.module_info { + out.notes.insert("go.modinfo".into(), m.clone()); + } + out.warnings.extend(go.warnings.iter().cloned()); + out.hints.extend(go.hints()); + out.go = Some(go); + } + + out.panics = rust_panic_sites(obj); + if !out.panics.is_empty() { + out.notes + .insert("rust.panic_sites".into(), out.panics.len().to_string()); + let mut files: Vec<&str> = out.panics.iter().map(|p| p.file.as_str()).collect(); + files.sort_unstable(); + files.dedup(); + out.notes + .insert("rust.source_files".into(), files.len().to_string()); + } + + let swift = crate::swift::read(obj); + if !swift.is_empty() { + out.notes + .insert("swift.types".into(), swift.types.len().to_string()); + let fields: usize = swift.field_descriptors.iter().map(|d| d.fields.len()).sum(); + out.notes.insert("swift.fields".into(), fields.to_string()); + out.notes.insert( + "swift.conformances".into(), + swift.conformances.len().to_string(), + ); + out.hints.extend(swift.hints()); + out.warnings.extend(swift.warnings.iter().cloned()); + out.swift = Some(swift); + } else { + out.warnings.extend(swift.warnings); + } + + out.classes = objc_classes(obj); + if !out.classes.is_empty() { + let methods: usize = out.classes.iter().map(|c| c.methods.len()).sum(); + out.notes + .insert("objc.classes".into(), out.classes.len().to_string()); + out.notes.insert("objc.methods".into(), methods.to_string()); + for c in &out.classes { + out.hints.extend(c.hints()); + } + } + + out +} + +/// Run the readers and fold what they found into the object. +/// +/// One call so a loader gains all three at once; the readers are public on +/// their own for a caller that wants just one. +pub fn apply(obj: &mut Object) { + let found = read(obj); + obj.function_hints.extend(found.hints); + obj.metadata.extend(found.notes); + obj.warnings.extend(found.warnings); +} + +/// Which `pclntab` layout a binary carries. The header changed shape at Go +/// 1.16, again at 1.18, and the magic changed again at 1.20 without the shape +/// following, so the version is what the magic says and nothing more. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GoVersion { + /// Go 1.2 through 1.15: a bare count followed by the function table. + V12, + /// Go 1.16 and 1.17: the sub-tables moved behind offsets in the header. + V116, + /// Go 1.18 and 1.19: entries became 32-bit offsets from `textStart`. + V118, + /// Go 1.20 and later: the 1.18 layout under a new magic. + V120, +} + +impl GoVersion { + /// The version a magic word announces. + fn from_magic(m: u32) -> Option { + match m { + 0xffff_fffb => Some(GoVersion::V12), + 0xffff_fffa => Some(GoVersion::V116), + 0xffff_fff0 => Some(GoVersion::V118), + 0xffff_fff1 => Some(GoVersion::V120), + _ => None, + } + } + + /// The releases this layout covers, for reporting. + pub fn as_str(self) -> &'static str { + match self { + GoVersion::V12 => "go1.2-1.15", + GoVersion::V116 => "go1.16-1.17", + GoVersion::V118 => "go1.18-1.19", + GoVersion::V120 => "go1.20+", + } + } + + /// True once entries are 32-bit offsets from `textStart` rather than + /// whole pointers. + fn offset_entries(self) -> bool { + matches!(self, GoVersion::V118 | GoVersion::V120) + } +} + +/// One function the table names. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GoFunc { + /// Entry address. + pub addr: Addr, + /// Where the next entry begins, which the table gives for every function + /// including the last. + pub end: Addr, + /// Name as the table spells it, still in Go's `pkg.Func` form. + pub name: String, +} + +/// A parsed `pclntab`. +#[derive(Debug, Clone)] +pub struct GoPclntab { + /// Layout the magic announced. + pub version: GoVersion, + /// Pointer width the header declares, which need not match the container. + pub ptr_size: u64, + /// Base that 1.18+ entry offsets are measured from. + pub text_start: Addr, + /// Every function, in table order, which is address order. + pub functions: Vec, + /// `runtime.buildVersion`, when the build info blob was readable. + pub build_version: Option, + /// The module line of the build info, trimmed of its sentinels. + pub module_info: Option, + /// What could not be read. + pub warnings: Vec, +} + +impl GoPclntab { + /// One hint per function. The table is generated by the linker from its + /// own symbol table, so this is as strong as a symbol table. + pub fn hints(&self) -> Vec { + self.functions + .iter() + .map(|f| FunctionHint { + addr: f.addr, + size: f.addr.distance_to(f.end).filter(|n| *n > 0), + name: Some(f.name.clone()), + provenance: Provenance::new(Evidence::GoPclntab), + }) + .collect() + } +} + +/// Find and read the `pclntab` of a loaded image. +/// +/// The section Go names is tried first; a stripped or PIE build that has lost +/// it is found by scanning read-only data for the magic, which is what every +/// Go-aware tool does. +pub fn go_pclntab(obj: &Object) -> Option { + let text = obj + .sections + .iter() + .find(|s| s.exec && !s.range.is_empty()) + .map(|s| s.range.start()); + + let mut attempts = 0u32; + for (_, bytes) in scan_regions(obj, "gopclntab") { + let mut off = 0usize; + while let Some(window) = bytes.get(off..off + 8) { + if looks_like_pclntab(window) { + attempts += 1; + let body = bytes.get(off..).unwrap_or_default(); + if let Ok(mut t) = parse_pclntab(body, text) { + read_build_info(obj, &mut t); + return Some(t); + } + if attempts >= MAX_CANDIDATES { + return None; + } + } + // Go aligns the table to a pointer pair wherever it puts it. + off += 8; + } + } + None +} + +/// A cheap gate before the full parse: magic, the two reserved zero bytes, and +/// the two sizes, in either byte order. +fn looks_like_pclntab(head: &[u8]) -> bool { + let Some(m) = head.get(..4) else { + return false; + }; + let le = u32::from_le_bytes([m[0], m[1], m[2], m[3]]); + let be = u32::from_be_bytes([m[0], m[1], m[2], m[3]]); + if GoVersion::from_magic(le).is_none() && GoVersion::from_magic(be).is_none() { + return false; + } + head.get(4) == Some(&0) + && head.get(5) == Some(&0) + && matches!(head.get(6), Some(1 | 2 | 4)) + && matches!(head.get(7), Some(4 | 8)) +} + +/// Parse a table from its magic onwards. +/// +/// `text_start` matters only from 1.18, where entries became offsets from it, +/// and only when the header declares none of its own. Earlier layouts store +/// whole addresses and ignore it. +pub fn parse_pclntab(body: &[u8], text_start: Option) -> Result { + let head = Reader::le(body).array::<8>("pclntab header")?; + let le = u32::from_le_bytes([head[0], head[1], head[2], head[3]]); + let be = u32::from_be_bytes([head[0], head[1], head[2], head[3]]); + let (version, endian) = match (GoVersion::from_magic(le), GoVersion::from_magic(be)) { + (Some(v), _) => (v, Endian::Little), + (_, Some(v)) => (v, Endian::Big), + _ => { + return Err(Error::NotRecognized { + expected: "Go pclntab", + }); + } + }; + if head[4] != 0 || head[5] != 0 { + return Err(Error::BadField { + field: "pclntab reserved bytes", + value: u64::from(head[4]) << 8 | u64::from(head[5]), + reason: "is not zero, so this magic is a coincidence", + }); + } + if !matches!(head[6], 1 | 2 | 4) { + return Err(Error::BadField { + field: "pclntab quantum", + value: head[6] as u64, + reason: "is not an instruction size any Go target uses", + }); + } + let ptr_size = match head[7] { + 4 => 4u64, + 8 => 8u64, + other => { + return Err(Error::BadField { + field: "pclntab pointer size", + value: other as u64, + reason: "is neither 4 nor 8", + }); + } + }; + + let r = Reader::new(body, endian); + let header_field = |i: u64| -> Result { + let mut c = r; + c.seek("pclntab header field", 8 + i * ptr_size)?; + c.uword("pclntab header field", ptr_size == 8) + }; + + let mut warnings = Vec::new(); + let declared = header_field(0)?; + // The sub-table offsets sit in a fixed order that shifted by one slot when + // 1.18 added `textStart` ahead of them. + let (name_at, functab_at, text) = match version { + GoVersion::V12 => (0u64, 8 + ptr_size, text_start.unwrap_or(Addr::ZERO)), + GoVersion::V116 => ( + header_field(2)?, + header_field(6)?, + text_start.unwrap_or(Addr::ZERO), + ), + GoVersion::V118 | GoVersion::V120 => { + let declared_text = header_field(2)?; + let text = match (declared_text, text_start) { + (0, Some(t)) => t, + (v, _) => Addr(v), + }; + (header_field(3)?, header_field(7)?, text) + } + }; + + // A table cannot declare more entries than it has bytes: the function + // table is `nfunc` pairs plus one sentinel giving the end of the last + // function, so the room left after its offset decides the count. + let field = if version.offset_entries() { + 4 + } else { + ptr_size + }; + let slots = (body.len() as u64).saturating_sub(functab_at) / field; + let room = slots.saturating_sub(1) / 2; + if declared > room { + warnings.push(format!( + "pclntab declares {declared} functions but has room for {room}" + )); + } + let nfunc = declared.min(room); + + let entry = |i: u64| -> Result { + let mut c = r; + c.seek("functab entry", functab_at + 2 * i * field)?; + let v = c.uword("functab entry", field == 8)?; + Ok(if version.offset_entries() { + Addr(text.get().wrapping_add(v)) + } else { + Addr(v) + }) + }; + + // The `_func` record opens with the entry PC, pointer-sized until 1.18 and + // a 32-bit offset after, and the name offset follows it. + let name_field = if version.offset_entries() { + 4 + } else { + ptr_size + }; + let mut functions = Vec::new(); + let mut skipped = 0u64; + for i in 0..nfunc { + let addr = entry(i)?; + let end = entry(i + 1)?; + let mut c = r; + c.seek("functab offset", functab_at + (2 * i + 1) * field)?; + // The `_func` offset is measured from the whole table before 1.16 and + // from the function table itself after it. + let func_base = if version == GoVersion::V12 { + 0 + } else { + functab_at + }; + let func_at = func_base.wrapping_add(c.uword("functab offset", field == 8)?); + let mut f = r; + if f.seek("_func", func_at.wrapping_add(name_field)).is_err() { + skipped += 1; + continue; + } + let name_off = match f.u32("_func nameoff") { + Ok(v) => v as u64, + Err(_) => { + skipped += 1; + continue; + } + }; + match r + .cstr_at("funcname", name_at.wrapping_add(name_off), MAX_NAME) + .ok() + .filter(|b| plausible_name(b)) + .and_then(|b| std::str::from_utf8(b).ok()) + { + Some(name) => functions.push(GoFunc { + addr, + end, + name: name.to_string(), + }), + None => skipped += 1, + } + } + if skipped != 0 { + warnings.push(format!("pclntab: {skipped} entries have no readable name")); + } + if functions.is_empty() && nfunc != 0 { + return Err(Error::BadField { + field: "pclntab function table", + value: nfunc, + reason: "names no function that can be read, so this is not a table", + }); + } + + Ok(GoPclntab { + version, + ptr_size, + text_start: text, + functions, + build_version: None, + module_info: None, + warnings, + }) +} + +/// True for a name a linker would emit: printable, and real UTF-8 because Go +/// package paths and generic instantiations are not all ASCII. +fn plausible_name(b: &[u8]) -> bool { + !b.is_empty() && b.iter().all(|&c| c >= 0x20 && c != 0x7f) && std::str::from_utf8(b).is_ok() +} + +/// The build info blob, which is a fixed magic rather than a symbol, so it +/// survives stripping the way the table does. +const BUILD_INFO_MAGIC: &[u8] = b"\xff Go buildinf:"; + +/// Fill in the Go version and module info from the build info blob. +fn read_build_info(obj: &Object, t: &mut GoPclntab) { + let img = Image::new(obj); + for (start, bytes) in scan_regions(obj, "go.buildinfo") { + let mut off = 0usize; + while let Some(window) = bytes.get(off..off + 32) { + if window.starts_with(BUILD_INFO_MAGIC) { + let at = match start.checked_add(off as u64) { + Some(a) => a, + None => break, + }; + if let Some((v, m)) = parse_build_info(&img, at, window) { + t.build_version = Some(v); + t.module_info = m; + return; + } + } + // The linker aligns the blob, and nothing else claims the magic. + off += 16; + } + } +} + +/// Read the version and module strings out of one build info blob. +/// +/// Go 1.18 moved both inline behind a flag bit; before that the blob held two +/// pointers to string headers that only a mapped image can follow. +fn parse_build_info(img: &Image<'_>, at: Addr, head: &[u8]) -> Option<(String, Option)> { + let ptr_size = *head.get(14)? as u64; + let flags = *head.get(15)?; + if !matches!(ptr_size, 4 | 8) { + return None; + } + if flags & 0x2 != 0 { + let seg = img.obj.memory.segment_at(at)?; + let body = seg.slice_to_end(at.checked_add(32)?)?; + let (version, rest) = varint_bytes(body)?; + let module = varint_bytes(rest).map(|(m, _)| m).unwrap_or_default(); + return Some(( + std::str::from_utf8(version).ok()?.to_string(), + trim_module(module), + )); + } + // Big-endian is flagged in the same byte the inline format reuses. + let endian = if flags & 0x1 != 0 { + Endian::Big + } else { + Endian::Little + }; + // Both fields point at a string header, which is a pointer and a length. + let read_string = |slot: Addr| -> Option<&[u8]> { + let header = img.word_with(slot, ptr_size, endian)?; + let data = Addr(img.word_with(Addr(header), ptr_size, endian)?); + let len = img.word_with(Addr(header).checked_add(ptr_size)?, ptr_size, endian)?; + img.obj.memory.slice(data, len.min(MAX_NAME)) + }; + let version = std::str::from_utf8(read_string(at.checked_add(16)?)?).ok()?; + let module = read_string(at.checked_add(16 + ptr_size)?).unwrap_or_default(); + Some((version.to_string(), trim_module(module))) +} + +/// A uvarint length followed by that many bytes, as the inline blob stores it. +fn varint_bytes(body: &[u8]) -> Option<(&[u8], &[u8])> { + let mut len = 0u64; + let mut shift = 0u32; + for (i, &b) in body.iter().take(10).enumerate() { + len |= u64::from(b & 0x7f) << shift; + shift += 7; + if b & 0x80 == 0 { + let rest = body.get(i + 1..)?; + let text = rest.get(..usize::try_from(len.min(MAX_NAME)).ok()?)?; + return Some((text, rest.get(text.len()..)?)); + } + } + None +} + +/// Strip the 16-byte binary sentinels Go wraps the module info in. They are +/// not text, so this has to happen before anything reads the blob as UTF-8. +fn trim_module(m: &[u8]) -> Option { + let inner = m.get(16..m.len().checked_sub(16)?)?; + std::str::from_utf8(inner) + .ok() + .filter(|s| !s.is_empty()) + .map(str::to_string) +} + +/// One `core::panic::Location` record. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PanicSite { + /// Where the record sits. This is the address a panicking call passes to + /// the runtime, so a reference to it from code is what ties the file and + /// line to an instruction. + pub addr: Addr, + /// Source file, as the compiler spelled it. + pub file: String, + /// Line, counted from one. + pub line: u32, + /// Column, counted from one. + pub column: u32, +} + +/// Every `core::panic::Location` the image carries, by address. +/// +/// The confidence rule: a record counts only when its pointer lands in mapped +/// non-executable memory, the string there is exactly the declared length, is +/// printable throughout, and ends in `.rs`, and the line and column are inside +/// the ranges a real source file has. The `.rs` requirement is what makes a +/// false positive essentially impossible, and a wrong file name is worse than +/// no file name. +pub fn rust_panic_sites(obj: &Object) -> Vec { + let img = Image::new(obj); + let ptr_size = img.ptr_size(); + let record = ptr_size * 2 + 8; + let Some(bounds) = obj.memory.bounds() else { + return Vec::new(); + }; + let mut out: Vec = Vec::new(); + + for (start, bytes) in scan_regions(obj, ".rodata") { + let mut off = 0u64; + // One pass at pointer alignment: the record is a static, so the linker + // never places it anywhere else. + while off + record <= bytes.len() as u64 { + let at = match start.checked_add(off) { + Some(a) => a, + None => break, + }; + off += ptr_size; + let Some(mut c) = img.at(at, record) else { + continue; + }; + let Ok(ptr) = c.uword("Location::file", ptr_size == 8) else { + continue; + }; + if ptr == 0 || !bounds.contains(Addr(ptr)) { + continue; + } + let Ok(len) = c.uword("Location::file length", ptr_size == 8) else { + continue; + }; + if !(4..=MAX_NAME).contains(&len) { + continue; + } + let (Ok(line), Ok(column)) = (c.u32("Location::line"), c.u32("Location::col")) else { + continue; + }; + if !(1..=1_000_000).contains(&line) || !(1..=10_000).contains(&column) { + continue; + } + // Section flags, not segment flags: a linker is free to put + // `.rodata` in the same executable `LOAD` as `.text`, and on this + // machine's aarch64 toolchain it does. + if obj.section_at(Addr(ptr)).is_some_and(|s| s.exec) { + continue; + } + let Some(file) = img.text(Addr(ptr), len) else { + continue; + }; + if !file.ends_with(".rs") || !file.bytes().all(|b| (0x20..0x7f).contains(&b)) { + continue; + } + out.push(PanicSite { + addr: at, + file: file.to_string(), + line, + column, + }); + } + } + out.sort_by_key(|p| p.addr); + out.dedup(); + out +} + +/// One method a class list names. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ObjcMethod { + /// Selector. + pub selector: String, + /// Type encoding, empty when it could not be read. + pub types: String, + /// The implementation. + pub imp: Addr, + /// True for a class method, which the convention spells with a `+`. + pub class_method: bool, +} + +/// One class the image declares. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ObjcClass { + /// Name, as the runtime would see it. + pub name: String, + /// Where the class object is. + pub addr: Addr, + /// Instance and class methods together; each says which it is. + pub methods: Vec, +} + +impl ObjcClass { + /// One hint per implementation, named the way a backtrace spells it. + pub fn hints(&self) -> Vec { + self.methods + .iter() + .map(|m| FunctionHint { + addr: m.imp, + size: None, + name: Some(format!( + "{}[{} {}]", + if m.class_method { '+' } else { '-' }, + self.name, + m.selector + )), + provenance: Provenance::new(Evidence::ObjcMetadata), + }) + .collect() + } +} + +// class_ro_t flags, and the two shapes a method list comes in. +const RO_META: u32 = 1; +const METHOD_LIST_SMALL: u32 = 0x8000_0000; +const METHOD_ENTSIZE: u32 = 0x0000_fffc; +/// The class's `data` field carries flag bits in its low three. +const CLASS_DATA_MASK: u64 = !0x7u64; + +/// Every Objective-C class the image declares, with its methods. +/// +/// Only the modern 64-bit ABI is read. A 32-bit image uses the original ABI, +/// whose structures share neither field order nor names with this one, and +/// guessing between them would be worse than reporting nothing. +pub fn objc_classes(obj: &Object) -> Vec { + if obj.bits != Bits::Bits64 { + return Vec::new(); + } + let img = Image::new(obj); + let mut out = Vec::new(); + for sec in obj + .sections + .iter() + .filter(|s| s.name.ends_with("__objc_classlist")) + { + let Some(bytes) = obj.memory.slice(sec.range.start(), sec.file_size) else { + continue; + }; + // The list is an array of pointers, so its own length bounds it. + let count = bytes.len() as u64 / 8; + for i in 0..count { + let Some(slot) = sec.range.start().checked_add(i * 8) else { + break; + }; + let Some(class) = img.ptr(slot).filter(|p| *p != Addr::ZERO) else { + continue; + }; + if let Some(c) = objc_class_at(&img, class) { + out.push(c); + } + } + } + out +} + +/// Read one class object and the metaclass its `isa` points at. +fn objc_class_at(img: &Image<'_>, at: Addr) -> Option { + let (name, mut methods) = objc_class_half(img, at, false)?; + // The metaclass holds the `+` methods, and pointing at itself is how a + // hostile file would ask for an endless walk; one level is all there is. + if let Some(meta) = img.ptr(at).filter(|m| *m != at && *m != Addr::ZERO) { + if let Some((_, more)) = objc_class_half(img, meta, true) { + methods.extend(more); + } + } + methods.retain(|m| m.imp != Addr::ZERO); + Some(ObjcClass { + name, + addr: at, + methods, + }) +} + +/// The name and method list of one class object, class or metaclass. +fn objc_class_half( + img: &Image<'_>, + at: Addr, + class_method: bool, +) -> Option<(String, Vec)> { + // objc_class: isa, superclass, cache, vtable, data. + let data = Addr(img.ptr(at.checked_add(32)?)?.get() & CLASS_DATA_MASK); + if data == Addr::ZERO { + return None; + } + // class_ro_t: flags, instanceStart, instanceSize, reserved, ivarLayout, + // name, baseMethods. + let flags = img.u32(data)?; + if (flags & RO_META != 0) != class_method { + return None; + } + let name = img + .cstr(img.ptr(data.checked_add(24)?)?, MAX_NAME)? + .to_string(); + let methods = match img.ptr(data.checked_add(32)?) { + Some(list) if list != Addr::ZERO => objc_methods(img, list, class_method), + _ => Vec::new(), + }; + Some((name, methods)) +} + +/// Read one `method_list_t`. +fn objc_methods(img: &Image<'_>, at: Addr, class_method: bool) -> Vec { + let Some((entsize, declared)) = img.u32(at).zip(img.u32(at.checked_add(4).unwrap_or(at))) + else { + return Vec::new(); + }; + let small = entsize & METHOD_LIST_SMALL != 0; + let step = u64::from(entsize & METHOD_ENTSIZE); + if step < 12 { + return Vec::new(); + } + // A list cannot declare more methods than the bytes after it hold. + let room = img.remaining(at).saturating_sub(8) / step; + let count = u64::from(declared).min(room); + let mut out = Vec::new(); + for i in 0..count { + let Some(entry) = at.checked_add(8 + i * step) else { + break; + }; + let m = if small { + objc_small_method(img, entry, class_method) + } else { + objc_big_method(img, entry, class_method) + }; + if let Some(m) = m { + out.push(m); + } + } + out +} + +/// A method whose three fields are whole pointers. +fn objc_big_method(img: &Image<'_>, at: Addr, class_method: bool) -> Option { + let selector = img.cstr(img.ptr(at)?, MAX_NAME)?.to_string(); + let types = img + .ptr(at.checked_add(8)?) + .and_then(|p| img.cstr(p, MAX_NAME)) + .unwrap_or_default() + .to_string(); + Some(ObjcMethod { + selector, + types, + imp: img.ptr(at.checked_add(16)?)?, + class_method, + }) +} + +/// A method whose fields are 32-bit displacements from their own address, as +/// arm64e and recent toolchains emit. The selector field reaches a selector +/// reference rather than the string, so it needs one more hop. +fn objc_small_method(img: &Image<'_>, at: Addr, class_method: bool) -> Option { + let rel = |slot: u64| -> Option { + let d = img.u32(at.checked_add(slot)?)? as i32; + Some(at.checked_add(slot)?.wrapping_offset(d as i64)) + }; + let name_at = rel(0)?; + let selector = img + .ptr(name_at) + .and_then(|p| img.cstr(p, MAX_NAME)) + .or_else(|| img.cstr(name_at, MAX_NAME))? + .to_string(); + let types = rel(4) + .and_then(|p| img.cstr(p, MAX_NAME)) + .unwrap_or_default() + .to_string(); + Some(ObjcMethod { + selector, + types, + imp: rel(8)?, + class_method, + }) +} + +/// Where a scan may look, with the section most likely to hold what is wanted +/// first so a large image is not walked to find a table that announces itself. +/// +/// A file whose section headers were removed has no sections at all, and that +/// is exactly the file a scan exists for, so the segments stand in. +fn scan_regions<'a>(obj: &'a Object, prefer: &str) -> Vec<(Addr, &'a [u8])> { + let mut regions: Vec<(bool, Addr, &[u8])> = data_sections(obj) + .filter_map(|s| { + Some(( + !s.name.ends_with(prefer), + s.range.start(), + obj.memory.slice(s.range.start(), s.file_size)?, + )) + }) + .collect(); + if regions.is_empty() { + return obj + .memory + .segments() + .iter() + .filter_map(|s| Some((s.range.start(), s.slice(s.range.start(), s.file_len())?))) + .collect(); + } + regions.sort_by_key(|(unnamed, _, _)| *unnamed); + regions.into_iter().map(|(_, a, b)| (a, b)).collect() +} + +/// Sections worth scanning: mapped, not code, and carrying real bytes. Debug +/// sections are excluded because they are large and hold no runtime metadata. +fn data_sections(obj: &Object) -> impl Iterator { + obj.sections.iter().filter(|s| { + !s.exec + && s.file_size >= 32 + && !s.range.is_empty() + && !s.name.starts_with(".debug") + && !s.name.starts_with(".zdebug") + }) +} + +/// Address-keyed reads over a loaded image. +/// +/// All three readers chase pointers, and all three must treat an unmapped one +/// as absence rather than an error, so the whole helper returns `Option`. +pub(crate) struct Image<'a> { + obj: &'a Object, + wide: bool, +} + +impl<'a> Image<'a> { + pub(crate) fn new(obj: &'a Object) -> Image<'a> { + Image { + obj, + wide: obj.bits == Bits::Bits64, + } + } + + /// The image these reads are against. + pub(crate) fn obj(&self) -> &'a Object { + self.obj + } + + pub(crate) fn ptr_size(&self) -> u64 { + if self.wide { 8 } else { 4 } + } + + fn at(&self, addr: Addr, len: u64) -> Option> { + Some(Reader::new( + self.obj.memory.slice(addr, len)?, + self.obj.endian, + )) + } + + pub(crate) fn u16(&self, addr: Addr) -> Option { + self.at(addr, 2)?.u16("field").ok() + } + + pub(crate) fn u32(&self, addr: Addr) -> Option { + self.at(addr, 4)?.u32("field").ok() + } + + /// Bytes from `addr` to the end of its segment, for a record whose length + /// is only known once it has been walked. + pub(crate) fn to_end(&self, addr: Addr) -> Option<&'a [u8]> { + self.obj.memory.segment_at(addr)?.slice_to_end(addr) + } + + pub(crate) fn ptr(&self, addr: Addr) -> Option { + let v = self.at(addr, self.ptr_size())?; + let mut c = v; + Some(Addr(c.uword("pointer", self.wide).ok()?)) + } + + /// A word of a width and order the caller chooses, which the build info + /// blob needs because it declares both itself. + fn word_with(&self, addr: Addr, size: u64, endian: Endian) -> Option { + let bytes = self.obj.memory.slice(addr, size)?; + let mut c = Reader::new(bytes, endian); + c.uword("build info word", size == 8).ok() + } + + /// Bytes from `addr` to the end of its segment, for a table whose length + /// is what bounds the entries in it. + fn remaining(&self, addr: Addr) -> u64 { + self.obj + .memory + .segment_at(addr) + .and_then(|s| s.slice_to_end(addr)) + .map_or(0, |b| b.len() as u64) + } + + /// A NUL-terminated string, bounded so an unterminated run cannot walk the + /// image. + pub(crate) fn cstr(&self, addr: Addr, max: u64) -> Option<&'a str> { + let seg = self.obj.memory.segment_at(addr)?; + let bytes = seg.slice_to_end(addr)?; + let n = bytes + .iter() + .take(usize::try_from(max).ok()?) + .position(|&b| b == 0)?; + let s = std::str::from_utf8(bytes.get(..n)?).ok()?; + (!s.is_empty() && s.bytes().all(|b| b >= 0x20 && b != 0x7f)).then_some(s) + } + + /// Exactly `len` bytes as text, for a Rust `str` which carries its length + /// instead of a terminator. + fn text(&self, addr: Addr, len: u64) -> Option<&'a str> { + std::str::from_utf8(self.obj.memory.slice(addr, len)?).ok() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_table_that_is_all_magic_and_nothing_else_is_refused() { + let body = vec![0xf1, 0xff, 0xff, 0xff, 0, 0, 4, 8]; + assert!(parse_pclntab(&body, None).is_err()); + } + + #[test] + fn a_declared_count_cannot_exceed_the_bytes() { + // nfunc of u64::MAX with 64 bytes behind it must not allocate, loop or + // read past the end. + let mut body = vec![0xf1, 0xff, 0xff, 0xff, 0, 0, 1, 8]; + body.extend_from_slice(&u64::MAX.to_le_bytes()); + body.extend_from_slice(&[0u8; 56]); + assert!(parse_pclntab(&body, None).is_err()); + } + + #[test] + fn garbage_never_panics() { + for seed in 0u32..512 { + let body: Vec = (0..256u32) + .map(|i| (i.wrapping_mul(2654435761).wrapping_add(seed) >> 11) as u8) + .collect(); + let _ = parse_pclntab(&body, None); + let mut tagged = body.clone(); + tagged[..8].copy_from_slice(&[0xfb, 0xff, 0xff, 0xff, 0, 0, 4, 8]); + let _ = parse_pclntab(&tagged, Some(Addr(0x1000))); + tagged[..4].copy_from_slice(&[0xf0, 0xff, 0xff, 0xff]); + let _ = parse_pclntab(&tagged, Some(Addr(0x1000))); + } + } + + #[test] + fn a_module_line_without_its_sentinels_is_dropped() { + assert_eq!(trim_module(&[0xffu8; 32]), None); + assert_eq!( + trim_module(b"0123456789abcdefpath0123456789abcdef"), + Some("path".to_string()) + ); + } +} diff --git a/crates/e5r-format/src/overlay.rs b/crates/e5r-format/src/overlay.rs new file mode 100644 index 0000000..a7c6e27 --- /dev/null +++ b/crates/e5r-format/src/overlay.rs @@ -0,0 +1,1055 @@ +//! Overlays, entropy and packer evidence. +//! +//! Three questions about a file that the container itself will not answer. +//! What is in the file that no header describes, because an installer, a +//! self-extracting archive and a dropper all append their payload there and +//! the loader never looks. How random is each region, because compressed and +//! encrypted bytes are near maximum entropy and compiled code is not. And what +//! about the shape of the image is unusual enough to be worth an analyst's +//! attention. +//! +//! The last of those is where tools normally lie. "Packed with UPX 3.96" is a +//! conclusion, and a wrong one whenever a legitimate binary happens to compress +//! a resource. So [`evidence`] reports what it saw and how strong the +//! observation is, and names a packer only when a section name is the packer's +//! own signature. The verdict stays the analyst's. +//! +//! Nothing here trusts a declared size. Every region is clipped to the bytes +//! that exist before it is read, and the window count has its own budget, +//! because a file is free to declare sixty-five thousand sections that each +//! claim to cover the whole image. + +use e5r_core::{Addr, Endian, Reader, Strength}; + +use crate::{Format, Object}; + +/// Window size for the per-section entropy map. A packed region inside an +/// otherwise ordinary section is the thing this exists to show, and four +/// kilobytes is small enough to isolate one and large enough that a byte +/// histogram of it means something. +pub const WINDOW: u64 = 4096; + +/// Windows measured across the whole image. Sections are capped at 65,536 by +/// the loaders and each may claim the whole file, so the product needs its own +/// ceiling or the map becomes the analysis. +const MAX_WINDOWS: usize = 1 << 16; + +/// Entropy at or above this is what compressed or encrypted bytes score. +/// English text sits near 4.5 and x86 code near 6. +const HIGH_ENTROPY: f64 = 7.2; + +/// Fewer imports than this is worth mentioning, but only alongside enough code +/// to need them. +const FEW_IMPORTS: usize = 8; + +/// Executable bytes a program has to carry before a short import table says +/// anything. A hello world genuinely imports three symbols. +const CODE_NEEDING_IMPORTS: u64 = 64 * 1024; + +/// Section names a normal toolchain emits. Anything else is worth a mention +/// and nothing more: a name is metadata, and metadata is free to lie. +const USUAL_NAMES: &[&str] = &[ + ".text", + ".data", + ".rdata", + ".rodata", + ".bss", + ".idata", + ".edata", + ".pdata", + ".xdata", + ".reloc", + ".rsrc", + ".tls", + ".debug", + ".comment", + ".eh_frame", + ".eh_frame_hdr", + ".init", + ".fini", + ".init_array", + ".fini_array", + ".got", + ".got.plt", + ".plt", + ".plt.got", + ".plt.sec", + ".dynamic", + ".dynsym", + ".dynstr", + ".symtab", + ".strtab", + ".shstrtab", + ".gnu.hash", + ".gnu.version", + ".gnu.version_r", + ".gnu.version_d", + ".rela.dyn", + ".rela.plt", + ".rel.dyn", + ".rel.plt", + ".note.ABI-tag", + ".note.gnu.build-id", + ".note.gnu.property", + ".interp", + ".data.rel.ro", + ".tbss", + ".tdata", + ".didat", + ".CRT", + ".gfids", + ".00cfg", + ".textbss", +]; + +/// Section names that are a named packer's own signature. Matching one of +/// these is the only case where naming a product is a statement of fact rather +/// than a guess, and even then it says the name was found, not that the file +/// was packed by it. +const PACKER_NAMES: &[(&str, &str)] = &[ + ("UPX0", "UPX"), + ("UPX1", "UPX"), + ("UPX2", "UPX"), + ("UPX!", "UPX"), + (".UPX0", "UPX"), + (".UPX1", "UPX"), + (".aspack", "ASPack"), + (".adata", "ASPack"), + (".ASPack", "ASPack"), + (".petite", "Petite"), + ("FSG!", "FSG"), + ("MPRESS1", "MPRESS"), + ("MPRESS2", "MPRESS"), + (".MPRESS1", "MPRESS"), + (".nsp0", "NsPack"), + (".nsp1", "NsPack"), + ("PEBundle", "PEBundle"), + ("PEC2", "PECompact"), + ("pec1", "PECompact"), + (".pklstb", "PKLite32"), + (".Themida", "Themida"), + (".vmp0", "VMProtect"), + (".vmp1", "VMProtect"), + (".enigma1", "Enigma"), + (".boom", "The Boomerang"), + (".packed", "RLPack"), + (".RLPack", "RLPack"), + (".WWP32", "WWPack32"), + (".winapi", "AHPack"), + (".yP", "Y0da Protector"), + (".taz", "PESpin"), +]; + +/// What the first bytes of an overlay look like. A guess from magic, so it is +/// a label on the bytes and not a claim about what they are for. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Content { + /// Another ELF. + Elf, + /// Another PE. + Pe, + /// Another Mach-O. + MachO, + /// An `ar` archive. + Archive, + /// A zip, which is also a jar, an apk and a self-extracting installer. + Zip, + /// Gzip. + Gzip, + /// Bzip2. + Bzip2, + /// XZ. + Xz, + /// Zstandard. + Zstd, + /// 7-Zip. + SevenZip, + /// A Microsoft cabinet, which is what most self-extractors append. + Cabinet, + /// RAR. + Rar, + /// An Authenticode signature, which the PE certificate directory + /// describes but which lives past the last section. + Certificate, + /// Mostly printable bytes. + Text, + /// No magic matched and the bytes are near maximum entropy: compressed or + /// encrypted, with nothing to say which. + HighEntropy, + /// No magic matched and the entropy is unremarkable. + Unknown, +} + +impl Content { + /// The name used in output and JSON. + pub fn as_str(self) -> &'static str { + match self { + Content::Elf => "elf", + Content::Pe => "pe", + Content::MachO => "macho", + Content::Archive => "archive", + Content::Zip => "zip", + Content::Gzip => "gzip", + Content::Bzip2 => "bzip2", + Content::Xz => "xz", + Content::Zstd => "zstd", + Content::SevenZip => "7z", + Content::Cabinet => "cab", + Content::Rar => "rar", + Content::Certificate => "certificate", + Content::Text => "text", + Content::HighEntropy => "high-entropy", + Content::Unknown => "unknown", + } + } +} + +impl std::fmt::Display for Content { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Bytes in the file that no header accounts for. +#[derive(Debug, Clone, PartialEq)] +pub struct Overlay { + /// Where it starts: the end of everything the headers describe. + pub offset: u64, + /// How many bytes there are. + pub size: u64, + /// Shannon entropy of the whole overlay, zero to eight. + pub entropy: f64, + /// What the first bytes look like. + pub content: Content, +} + +/// Entropy of one window inside a section. +#[derive(Debug, Clone, PartialEq)] +pub struct Window { + /// File offset of the window's first byte. + pub offset: u64, + /// Bytes in it, which is [`WINDOW`] except for the last one. + pub size: u64, + /// Shannon entropy, zero to eight. + pub entropy: f64, +} + +/// Entropy of one section and of each window inside it. +#[derive(Debug, Clone, PartialEq)] +pub struct SectionEntropy { + /// Section name as the container spells it. + pub name: String, + /// Where its bytes are in the file. + pub file_offset: u64, + /// How many of them exist, after clipping to the file. + pub size: u64, + /// Entropy over the whole section. + pub entropy: f64, + /// Entropy per [`WINDOW`] bytes, in file order. Empty when the window + /// budget ran out, which the report's warnings record. + pub windows: Vec, +} + +impl SectionEntropy { + /// The highest-entropy window, which is what a packed region inside an + /// ordinary-looking section shows up as. + pub fn peak(&self) -> Option<&Window> { + self.windows + .iter() + .max_by(|a, b| a.entropy.total_cmp(&b.entropy)) + } +} + +/// What kind of observation a finding records. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + /// A section is both writable and executable. + WritableExecutable, + /// A section's file bytes are far fewer than the memory it claims, which + /// is what a region unpacked at run time looks like. + RawSmallerThanVirtual, + /// High entropy in a section marked executable. + HighEntropyCode, + /// The entry point is not inside any section that carries code. + EntryOutsideCode, + /// The import table names almost nothing, so the real imports are resolved + /// at run time. + FewImports, + /// A section name no ordinary toolchain emits. + UnusualSectionName, + /// A section name that is a named packer's signature. + PackerSectionName, + /// Bytes past everything the headers describe. + OverlayPresent, + /// A section the container declares but whose file bytes do not exist. + SectionOutsideFile, +} + +impl Kind { + /// The name used in output and JSON. + pub fn as_str(self) -> &'static str { + match self { + Kind::WritableExecutable => "writable-executable-section", + Kind::RawSmallerThanVirtual => "raw-smaller-than-virtual", + Kind::HighEntropyCode => "high-entropy-code", + Kind::EntryOutsideCode => "entry-outside-code", + Kind::FewImports => "few-imports", + Kind::UnusualSectionName => "unusual-section-name", + Kind::PackerSectionName => "packer-section-name", + Kind::OverlayPresent => "overlay", + Kind::SectionOutsideFile => "section-outside-file", + } + } +} + +impl std::fmt::Display for Kind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// One observation about the image, with what was seen and how much it weighs. +#[derive(Debug, Clone, PartialEq)] +pub struct Finding { + /// What was observed. + pub kind: Kind, + /// The section or region it is about, when it is about one. + pub section: Option, + /// What was actually seen, in numbers a reader can check against the file. + pub detail: String, + /// How much the observation weighs. Nothing here is ever + /// [`Strength::Proven`] as a statement about packing: the fact is proven, + /// the packing is not. + pub strength: Strength, +} + +/// Everything this module found about one image. +#[derive(Debug, Clone, Default)] +pub struct Report { + /// The overlay, when there is one. + pub overlay: Option, + /// The end of everything the headers describe, which is where an overlay + /// would start. + pub described_end: u64, + /// Entropy per section. + pub sections: Vec, + /// Observations, strongest first. + pub findings: Vec, + /// What could not be measured, most often a budget running out. + pub warnings: Vec, +} + +impl Report { + /// Findings at or above a strength, which is how a caller asks for "only + /// the ones worth waking someone for". + pub fn at_least(&self, strength: Strength) -> impl Iterator { + self.findings.iter().filter(move |f| f.strength >= strength) + } +} + +/// Run every measurement over a loaded image and the bytes it came from. +pub fn analyze(obj: &Object, data: &[u8]) -> Report { + let described_end = described_end(obj, data); + let overlay = overlay_at(described_end, data); + let (sections, warnings) = section_entropy(obj, data); + let findings = evidence(obj, data, §ions, overlay.as_ref()); + Report { + overlay, + described_end, + sections, + findings, + warnings, + } +} + +/// Shannon entropy of a byte string, in bits per byte, so zero to eight. +pub fn entropy(bytes: &[u8]) -> f64 { + if bytes.is_empty() { + return 0.0; + } + let mut counts = [0u32; 256]; + for &b in bytes { + counts[b as usize] += 1; + } + let total = bytes.len() as f64; + let mut out = 0.0; + for &c in counts.iter() { + if c != 0 { + let p = c as f64 / total; + out -= p * p.log2(); + } + } + out +} + +/// Where everything the headers describe ends. +/// +/// The loaded [`Object`] carries sections, which is most of the answer, but +/// not the tables that sit outside them: a section header table, an ELF +/// program header, a Mach-O `__LINKEDIT`, a PE certificate. Those are read +/// back out of the raw bytes here, because the alternative is a false overlay +/// on every signed binary and every unstripped ELF. +pub fn described_end(obj: &Object, data: &[u8]) -> u64 { + let len = data.len() as u64; + let mut end = 0u64; + for s in &obj.sections { + if s.file_size == 0 { + continue; + } + end = end.max(s.file_offset.saturating_add(s.file_size).min(len)); + } + let tail = match obj.format { + Format::Elf => elf_tail(data), + Format::Pe => pe_tail(data), + Format::MachO => macho_tail(data), + Format::Raw => len, + }; + end.max(tail.min(len)) +} + +/// An overlay is whatever is left after `end`. +fn overlay_at(end: u64, data: &[u8]) -> Option { + let len = data.len() as u64; + if end >= len { + return None; + } + let bytes = &data[end as usize..]; + let e = entropy(bytes); + Some(Overlay { + offset: end, + size: len - end, + entropy: e, + content: classify(bytes, e), + }) +} + +/// Label a run of bytes by magic, falling back to how random it is. +fn classify(bytes: &[u8], entropy: f64) -> Content { + let starts = |m: &[u8]| bytes.len() >= m.len() && &bytes[..m.len()] == m; + if starts(b"\x7fELF") { + return Content::Elf; + } + if starts(b"MZ") { + return Content::Pe; + } + if starts(b"\xcf\xfa\xed\xfe") || starts(b"\xce\xfa\xed\xfe") || starts(b"\xca\xfe\xba\xbe") { + return Content::MachO; + } + if starts(b"!\n") || starts(b"!\n") { + return Content::Archive; + } + if starts(b"PK\x03\x04") || starts(b"PK\x05\x06") || starts(b"PK\x07\x08") { + return Content::Zip; + } + if starts(b"\x1f\x8b") { + return Content::Gzip; + } + if starts(b"BZh") { + return Content::Bzip2; + } + if starts(b"\xfd7zXZ\x00") { + return Content::Xz; + } + if starts(b"\x28\xb5\x2f\xfd") { + return Content::Zstd; + } + if starts(b"7z\xbc\xaf\x27\x1c") { + return Content::SevenZip; + } + if starts(b"MSCF") { + return Content::Cabinet; + } + if starts(b"Rar!\x1a\x07") { + return Content::Rar; + } + // A WIN_CERTIFICATE is a length, a revision, a type of 0x0002, then DER. + if bytes.len() >= 10 && bytes[6] == 0x02 && bytes[7] == 0x00 && bytes[8] == 0x30 { + return Content::Certificate; + } + if entropy >= HIGH_ENTROPY { + return Content::HighEntropy; + } + let printable = bytes + .iter() + .take(1024) + .filter(|&&b| (0x20..0x7f).contains(&b) || b == b'\n' || b == b'\r' || b == b'\t') + .count(); + if printable * 10 >= bytes.len().min(1024) * 9 { + return Content::Text; + } + Content::Unknown +} + +/// End of the ELF tables that are not sections: the program header table and +/// the section header table, plus every segment's file extent. +fn elf_tail(data: &[u8]) -> u64 { + let r = Reader::le(data); + let Ok(ident) = r.bytes_at("e_ident", 0, 16) else { + return 0; + }; + let wide = ident[4] == 2; + let endian = if ident[5] == 2 { + Endian::Big + } else { + Endian::Little + }; + let r = Reader::new(data, endian); + let mut h = r; + // e_phoff sits after e_ident, e_type, e_machine, e_version and e_entry. + let at = 16 + 2 + 2 + 4 + if wide { 8 } else { 4 }; + if h.seek("elf header", at).is_err() { + return 0; + } + let (Ok(phoff), Ok(shoff)) = (h.uword("e_phoff", wide), h.uword("e_shoff", wide)) else { + return 0; + }; + // e_flags, e_ehsize, then the two header table geometries. + if h.skip("e_flags", 4 + 2).is_err() { + return 0; + } + let (Ok(phentsize), Ok(phnum), Ok(shentsize), Ok(shnum)) = ( + h.u16("e_phentsize"), + h.u16("e_phnum"), + h.u16("e_shentsize"), + h.u16("e_shnum"), + ) else { + return 0; + }; + let mut end = phoff.saturating_add(phentsize as u64 * phnum as u64); + end = end.max(shoff.saturating_add(shentsize as u64 * shnum as u64)); + // Segments, because a file with its section headers stripped still has + // them and they are what the loader actually reads. + if phentsize as u64 >= if wide { 56 } else { 32 } { + for i in 0..phnum as u64 { + let at = phoff.saturating_add(i * phentsize as u64); + let off_at = if wide { at + 8 } else { at + 4 }; + let mut p = r; + if p.seek("program header", off_at).is_err() { + break; + } + let Ok(p_offset) = p.uword("p_offset", wide) else { + break; + }; + if wide { + // 64-bit puts p_vaddr and p_paddr between p_offset and p_filesz. + if p.skip("p_vaddr", 16).is_err() { + break; + } + } else if p.skip("p_vaddr", 8).is_err() { + break; + } + let Ok(p_filesz) = p.uword("p_filesz", wide) else { + break; + }; + end = end.max(p_offset.saturating_add(p_filesz)); + } + } + end +} + +/// End of everything a PE or COFF header describes. +/// +/// Three things live past the last section and would otherwise read as an +/// overlay: the COFF symbol table and its string table, which every `.o` has; +/// a section's relocation table; and the certificate directory, whose address +/// is a file offset rather than an RVA, which is why every signed Windows +/// binary looks like it has an overlay to a tool that forgets it. +fn pe_tail(data: &[u8]) -> u64 { + let r = Reader::le(data); + // An image puts the COFF header behind `PE\0\0`; an object starts with it. + let coff_at = if data.starts_with(b"MZ") { + let mut h = r; + if h.seek("e_lfanew", 0x3c).is_err() { + return 0; + } + let Ok(at) = h.u32("e_lfanew") else { + return 0; + }; + let mut c = r; + if c.seek("pe signature", at as u64).is_err() { + return 0; + } + if c.array::<4>("pe signature").ok() != Some(*b"PE\0\0") { + return 0; + } + at as u64 + 4 + } else { + 0 + }; + + let mut c = r; + if c.seek("coff header", coff_at).is_err() { + return 0; + } + if c.skip("machine", 2).is_err() { + return 0; + } + let (Ok(nsections), Ok(_timestamp), Ok(symtab), Ok(nsyms), Ok(opt_size)) = ( + c.u16("number of sections"), + c.u32("timestamp"), + c.u32("pointer to symbol table"), + c.u32("number of symbols"), + c.u16("size of optional header"), + ) else { + return 0; + }; + let opt_at = coff_at + 20; + let sections_at = opt_at + opt_size as u64; + let mut end = sections_at.saturating_add(40 * nsections as u64); + + // The symbol table is 18-byte records followed by a string table whose + // first four bytes are its own length. + if symtab != 0 && nsyms != 0 { + let strings_at = (symtab as u64).saturating_add(18 * nsyms as u64); + end = end.max(strings_at); + let mut t = r; + if t.seek("string table", strings_at).is_ok() { + if let Ok(n) = t.u32("string table size") { + end = end.max(strings_at.saturating_add(n as u64)); + } + } + } + + // Relocation and line-number tables, which an object keeps behind the + // section data they belong to. + for i in 0..nsections as u64 { + let mut sh = r; + if sh + .seek("section header", sections_at + i * 40 + 24) + .is_err() + { + break; + } + let (Ok(relocs), Ok(lines), Ok(nrelocs), Ok(nlines)) = ( + sh.u32("pointer to relocations"), + sh.u32("pointer to line numbers"), + sh.u16("number of relocations"), + sh.u16("number of line numbers"), + ) else { + break; + }; + if relocs != 0 { + end = end.max((relocs as u64).saturating_add(10 * nrelocs as u64)); + } + if lines != 0 { + end = end.max((lines as u64).saturating_add(6 * nlines as u64)); + } + } + + if opt_size == 0 { + return end; + } + let mut o = r; + if o.seek("optional header", opt_at).is_err() { + return end; + } + let Ok(magic) = o.u16("optional header magic") else { + return end; + }; + let wide = magic == 0x20b; + // SizeOfHeaders sits at the same place in both optional header layouts. + let mut s = r; + if s.seek("size of headers", opt_at + 60).is_ok() { + if let Ok(size_of_headers) = s.u32("size of headers") { + end = end.max(size_of_headers as u64); + } + } + // Data directory 4 is the certificate table: a file offset and a size. + let dirs_at = opt_at + if wide { 112 } else { 96 }; + let mut d = r; + if d.seek("certificate directory", dirs_at + 4 * 8).is_ok() { + if let (Ok(off), Ok(size)) = (d.u32("certificate offset"), d.u32("certificate size")) { + if off != 0 { + end = end.max((off as u64).saturating_add(size as u64)); + } + } + } + end +} + +/// End of every Mach-O segment's file extent, which is what covers +/// `__LINKEDIT` and the code signature the loaded sections do not. +fn macho_tail(data: &[u8]) -> u64 { + let r = Reader::le(data); + let mut h = r; + let Ok(magic) = h.u32("mach magic") else { + return 0; + }; + // A fat file describes its slices and nothing past them. + if magic == 0xbebafeca || magic == 0xcafebabe { + let mut f = Reader::new(data, Endian::Big); + if f.seek("fat header", 4).is_err() { + return 0; + } + let Ok(n) = f.u32("nfat_arch") else { + return 0; + }; + // The count is not trusted for anything but a loop bound that the + // reader stops anyway; each entry is 20 bytes of the same file. + let mut end = 8 + 20 * (n as u64).min(data.len() as u64 / 20); + for i in 0..(n as u64).min(data.len() as u64 / 20) { + let mut a = Reader::new(data, Endian::Big); + if a.seek("fat arch", 8 + i * 20 + 8).is_err() { + break; + } + let (Ok(off), Ok(size)) = (a.u32("fat offset"), a.u32("fat size")) else { + break; + }; + end = end.max((off as u64).saturating_add(size as u64)); + } + return end; + } + let (wide, endian) = match magic { + 0xfeedface => (false, Endian::Little), + 0xfeedfacf => (true, Endian::Little), + 0xcefaedfe => (false, Endian::Big), + 0xcffaedfe => (true, Endian::Big), + _ => return 0, + }; + let r = Reader::new(data, endian); + let mut c = r; + if c.seek("mach header", 16).is_err() { + return 0; + } + let Ok(ncmds) = c.u32("ncmds") else { + return 0; + }; + let header_len = if wide { 32u64 } else { 28 }; + let mut end = header_len; + let mut at = header_len; + // Bounded by the file: every command declares a length and the walk stops + // the moment one does not move forward. + for _ in 0..ncmds.min(1 << 16) { + let mut l = r; + if l.seek("load command", at).is_err() { + break; + } + let (Ok(cmd), Ok(cmdsize)) = (l.u32("cmd"), l.u32("cmdsize")) else { + break; + }; + if cmdsize < 8 { + break; + } + // LC_SEGMENT and LC_SEGMENT_64 put fileoff and filesize after a + // sixteen-byte name and the two virtual-memory fields. + if cmd == 0x01 || cmd == 0x19 { + let seg_wide = cmd == 0x19; + let at_off = at + 8 + 16 + if seg_wide { 16 } else { 8 }; + let mut s = r; + if s.seek("segment", at_off).is_ok() { + if let (Ok(fileoff), Ok(filesize)) = + (s.uword("fileoff", seg_wide), s.uword("filesize", seg_wide)) + { + end = end.max(fileoff.saturating_add(filesize)); + } + } + } + // LC_CODE_SIGNATURE and friends are a (dataoff, datasize) pair. + if matches!(cmd, 0x1d | 0x1a | 0x1b | 0x26 | 0x29 | 0x2b | 0x2e | 0x2f) { + let mut s = r; + if s.seek("linkedit command", at + 8).is_ok() { + if let (Ok(off), Ok(size)) = (s.u32("dataoff"), s.u32("datasize")) { + end = end.max((off as u64).saturating_add(size as u64)); + } + } + } + // LC_SYMTAB: symbol table offset and string table extent. + if cmd == 0x02 { + let mut s = r; + if s.seek("symtab", at + 8 + 8).is_ok() { + if let (Ok(stroff), Ok(strsize)) = (s.u32("stroff"), s.u32("strsize")) { + end = end.max((stroff as u64).saturating_add(strsize as u64)); + } + } + } + at = at.saturating_add(cmdsize as u64); + if at >= data.len() as u64 { + break; + } + } + end +} + +/// Entropy of every section, whole and per window. +pub fn section_entropy(obj: &Object, data: &[u8]) -> (Vec, Vec) { + let len = data.len() as u64; + let mut out = Vec::with_capacity(obj.sections.len()); + let mut warnings = Vec::new(); + let mut budget = MAX_WINDOWS; + for s in &obj.sections { + // The declared extent is clipped to what exists before anything reads + // it, so a section claiming four gigabytes measures what is there. + let start = s.file_offset.min(len); + let end = s.file_offset.saturating_add(s.file_size).min(len); + let body = &data[start as usize..end as usize]; + let mut windows = Vec::new(); + if !body.is_empty() { + let want = body.len().div_ceil(WINDOW as usize); + if want > budget { + warnings.push(format!( + "section {} has {want} windows, past the {MAX_WINDOWS}-window budget; \ + its map is omitted", + s.name + )); + } else { + budget -= want; + for (i, chunk) in body.chunks(WINDOW as usize).enumerate() { + windows.push(Window { + offset: start + i as u64 * WINDOW, + size: chunk.len() as u64, + entropy: entropy(chunk), + }); + } + } + } + out.push(SectionEntropy { + name: s.name.clone(), + file_offset: start, + size: body.len() as u64, + entropy: entropy(body), + windows, + }); + } + (out, warnings) +} + +/// Observations about the image, strongest first. +/// +/// Every one names what it saw. None of them says the file is packed, because +/// none of them can: a legitimate installer has an overlay, a legitimate +/// binary has a compressed resource, and a writable executable section is a +/// linker script away from ordinary. +pub fn evidence( + obj: &Object, + data: &[u8], + sections: &[SectionEntropy], + overlay: Option<&Overlay>, +) -> Vec { + let mut out = Vec::new(); + let len = data.len() as u64; + + for s in &obj.sections { + if s.exec && s.write && !s.range.is_empty() { + out.push(Finding { + kind: Kind::WritableExecutable, + section: Some(s.name.clone()), + detail: format!( + "section {} is mapped writable and executable at {}", + s.name, s.range + ), + strength: Strength::Inferred, + }); + } + let virt = s.range.len(); + if virt > 0 && s.file_size > 0 && virt >= s.file_size.saturating_mul(4) { + out.push(Finding { + kind: Kind::RawSmallerThanVirtual, + section: Some(s.name.clone()), + detail: format!( + "section {} holds {} bytes in the file and claims {virt} in memory", + s.name, s.file_size + ), + strength: Strength::Inferred, + }); + } + if s.file_size > 0 && s.file_offset.saturating_add(s.file_size) > len { + out.push(Finding { + kind: Kind::SectionOutsideFile, + section: Some(s.name.clone()), + detail: format!( + "section {} runs to {:#x}, past the {len:#x} bytes the file has", + s.name, + s.file_offset.saturating_add(s.file_size) + ), + strength: Strength::Proven, + }); + } + if let Some((name, packer)) = PACKER_NAMES.iter().find(|(n, _)| *n == s.name) { + out.push(Finding { + kind: Kind::PackerSectionName, + section: Some(s.name.clone()), + detail: format!("section is named {name}, which is {packer}'s own signature"), + strength: Strength::Inferred, + }); + } else if !s.name.is_empty() && !usual_name(&s.name) { + out.push(Finding { + kind: Kind::UnusualSectionName, + section: Some(s.name.clone()), + detail: format!( + "section {} is not a name an ordinary toolchain emits", + s.name + ), + strength: Strength::Heuristic, + }); + } + } + + for (s, e) in obj.sections.iter().zip(sections) { + if !s.exec || e.size == 0 { + continue; + } + if e.entropy >= HIGH_ENTROPY { + out.push(Finding { + kind: Kind::HighEntropyCode, + section: Some(s.name.clone()), + detail: format!( + "executable section {} scores {:.2} bits per byte over {} bytes", + s.name, e.entropy, e.size + ), + strength: Strength::Inferred, + }); + } else if let Some(w) = e.peak() { + if w.entropy >= HIGH_ENTROPY && w.size == WINDOW { + out.push(Finding { + kind: Kind::HighEntropyCode, + section: Some(s.name.clone()), + detail: format!( + "executable section {} averages {:.2} but its window at {:#x} scores \ + {:.2}", + s.name, e.entropy, w.offset, w.entropy + ), + strength: Strength::Heuristic, + }); + } + } + } + + if obj.entry.is_some_and(|e| !entry_in_code(obj, e)) { + let entry = obj.entry.unwrap_or(Addr::ZERO); + out.push(Finding { + kind: Kind::EntryOutsideCode, + section: None, + detail: match obj.section_at(entry) { + Some(s) => format!( + "entry point {entry} is in section {}, which is not marked executable", + s.name + ), + None => format!("entry point {entry} is in no declared section"), + }, + strength: Strength::Inferred, + }); + } + + // An import table is only evidence when the container has one at all and + // when there is enough code for it to be too short for: a relocatable + // object, a static ELF and a hello world are not hiding anything. + let dynamic = obj.format == Format::Pe || obj.metadata.contains_key("elf.needed"); + let code_bytes: u64 = obj + .sections + .iter() + .filter(|s| s.exec) + .map(|s| s.file_size.min(len)) + .sum(); + if dynamic && obj.imports.len() < FEW_IMPORTS && code_bytes >= CODE_NEEDING_IMPORTS { + out.push(Finding { + kind: Kind::FewImports, + section: None, + detail: format!( + "{code_bytes} bytes of code import only {} symbols, so the rest are \ + resolved at run time", + obj.imports.len() + ), + strength: Strength::Inferred, + }); + } + + if let Some(o) = overlay { + out.push(Finding { + kind: Kind::OverlayPresent, + section: None, + detail: format!( + "{} bytes at {:#x} that no header describes, looking like {} at {:.2} bits \ + per byte", + o.size, o.offset, o.content, o.entropy + ), + strength: if o.content == Content::Certificate { + Strength::Heuristic + } else { + Strength::Inferred + }, + }); + } + + // Strongest first, then a stable order so two runs agree. + out.sort_by(|a, b| { + b.strength + .cmp(&a.strength) + .then_with(|| a.kind.as_str().cmp(b.kind.as_str())) + .then_with(|| a.detail.cmp(&b.detail)) + }); + out +} + +/// True when a name is one an ordinary toolchain emits, allowing for the +/// suffixes a section can carry: `.text.unlikely`, `.debug_info`, `.rela.text`. +fn usual_name(name: &str) -> bool { + USUAL_NAMES + .iter() + .any(|u| name == *u || (name.starts_with(u) && name.as_bytes().get(u.len()) == Some(&b'.'))) + || name.starts_with(".debug") + || name.starts_with(".note") + || name.starts_with(".gnu") + || name.starts_with(".rel") + || name.starts_with("__") + || name.starts_with(".llvm") + || name.starts_with(".stab") +} + +/// True when the entry point lands in a section that carries code. +fn entry_in_code(obj: &Object, entry: Addr) -> bool { + match obj.section_at(entry) { + Some(s) => s.exec, + // No sections at all is a stripped-to-segments ELF, not a finding; the + // memory map is then the only authority on what is executable. + None => obj.sections.is_empty() || obj.memory.is_executable(entry), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn entropy_ends_are_right() { + assert_eq!(entropy(&[]), 0.0); + assert_eq!(entropy(&[7u8; 4096]), 0.0); + // Every byte value exactly once is exactly eight bits. + let all: Vec = (0..=255u8).collect(); + assert!((entropy(&all) - 8.0).abs() < 1e-12, "{}", entropy(&all)); + // Two values evenly split is one bit. + let half: Vec = (0..1024).map(|i| (i % 2) as u8).collect(); + assert!((entropy(&half) - 1.0).abs() < 1e-12); + } + + #[test] + fn magic_is_recognized_before_entropy() { + assert_eq!(classify(b"PK\x03\x04rest", 0.5), Content::Zip); + assert_eq!(classify(b"MSCFrest", 7.9), Content::Cabinet); + assert_eq!(classify(b"hello world\n", 1.0), Content::Text); + } + + #[test] + fn header_readers_survive_nonsense() { + // The point is that they return, not what they return. + for n in 0..300 { + let v = vec![0xffu8; n]; + let _ = elf_tail(&v); + let _ = pe_tail(&v); + let _ = macho_tail(&v); + } + let mut pe = vec![0u8; 0x400]; + pe[..2].copy_from_slice(b"MZ"); + pe[0x3c..0x40].copy_from_slice(&0xffff_fff0u32.to_le_bytes()); + assert_eq!(pe_tail(&pe), 0); + } + + #[test] + fn unusual_names_allow_the_ordinary_suffixes() { + assert!(usual_name(".text")); + assert!(usual_name(".text.unlikely")); + assert!(usual_name(".debug_info")); + assert!(usual_name(".rela.text")); + assert!(!usual_name("UPX1")); + assert!(!usual_name(".mystery")); + } +} diff --git a/crates/e5r-format/src/pdata.rs b/crates/e5r-format/src/pdata.rs new file mode 100644 index 0000000..c73a375 --- /dev/null +++ b/crates/e5r-format/src/pdata.rs @@ -0,0 +1,876 @@ +//! The PE exception directory and the unwind data behind it. +//! +//! On x86-64 and ARM64 the Windows ABI requires every function that touches the +//! stack to declare how to unwind it, so `.pdata` is a linker-built table of +//! function boundaries that survives stripping. There is nothing better in a +//! stripped PE: no prologue scan competes with a start and an end the file +//! states outright. +//! +//! Three layers, each weaker than the one above it: +//! +//! 1. `RUNTIME_FUNCTION`. Proven. The format defines the entry. +//! 2. `UNWIND_INFO` in `.xdata`, reached by an RVA the entry carries: the +//! prologue's frame setup, the handler routine, and the chain to a parent +//! record when this one is a funclet. Also proven; also stated outright. +//! 3. The language-specific data after the handler. Its shape depends on which +//! handler it is, and the image does not say which. What follows is parsed +//! as `__C_specific_handler`'s scope table only when every field validates, +//! and dropped with a warning otherwise. A C++ image puts a `FuncInfo` RVA +//! here instead, and that fails the very first bound. +//! +//! ARM64 gets boundaries and handlers but not decoded prologue opcodes; ARM32 +//! gets starts only. Both say so rather than guessing. + +use e5r_core::{Addr, Arch, Caps, Evidence, Provenance}; + +use crate::pe::Image; +use crate::{FunctionHint, Object}; + +// UNWIND_INFO flags. +const UNW_FLAG_EHANDLER: u8 = 0x1; +const UNW_FLAG_UHANDLER: u8 = 0x2; +const UNW_FLAG_CHAININFO: u8 = 0x4; + +/// How deep a chain of `UNW_FLAG_CHAININFO` records is followed. Real chains +/// are one or two long; the limit is what stops a cycle in a hostile file. +const MAX_CHAIN: u32 = 8; + +/// Entries accepted in one scope table. A function with more `__try` blocks +/// than this does not exist; a file that claims one is lying. +const MAX_SCOPE_ENTRIES: u64 = 4096; + +/// One entry of the exception directory. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeFunction { + /// First byte of the function. + pub start: Addr, + /// One past its last byte. x86-64 states this directly; ARM64 takes it from + /// the unwind data; ARM32 is left unknown rather than guessed. + pub end: Option, + /// RVA of the `UNWIND_INFO` record, when the entry points at one. + pub unwind_rva: Option, + /// ARM64's packed form, used instead of an `.xdata` record when the frame + /// is simple enough to fit in the entry itself. + pub packed: Option, +} + +/// ARM64 unwind data packed into the `.pdata` entry. +/// +/// Field names follow the ARM64 exception handling documentation so an entry +/// can be diffed against it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Arm64Packed { + /// 1 for a whole function, 2 for a fragment. + pub flag: u8, + /// Function length in bytes. + pub function_length: u32, + /// Count of saved non-volatile floating-point registers, d8 upward. + pub reg_f: u8, + /// Count of saved non-volatile integer registers, x19 upward. + pub reg_i: u8, + /// True when the function homes its integer parameter registers. + pub homed_parameters: bool, + /// Chained-return, frame-chain and signing state, as the two-bit CR field. + pub cr: u8, + /// Bytes of stack the frame allocates. + pub frame_size: u32, +} + +/// One operation in an x86-64 prologue, as `UNWIND_CODE` records it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnwindOp { + /// `push` of a non-volatile integer register. + PushNonVolatile { + /// Register number, as [`x64_register`] names it. + reg: u8, + }, + /// A subtraction from `rsp`, in either the small or the large encoding. + Alloc { + /// Bytes allocated. + bytes: u64, + }, + /// The frame pointer is established here. + SetFramePointer, + /// A non-volatile integer register stored at a frame offset. + SaveNonVolatile { + /// Register number. + reg: u8, + /// Offset from the frame base. + offset: u64, + }, + /// The low 128 bits of a non-volatile XMM register stored at an offset. + SaveXmm128 { + /// XMM register number. + reg: u8, + /// Offset from the frame base. + offset: u64, + }, + /// A machine frame pushed by the processor, as an interrupt entry has. + PushMachineFrame { + /// True when the frame carries an error code. + error_code: bool, + }, + /// Version 2's epilogue marker, which describes an epilogue rather than the + /// prologue and so says nothing about frame setup. + Epilogue, + /// An opcode this parser does not know. Recorded rather than dropped: the + /// difference between "the prologue does something unusual" and "the + /// prologue is what we listed" is the whole point of the evidence model. + Unknown { + /// The opcode nibble. + op: u8, + /// The info nibble. + info: u8, + }, +} + +/// One `UNWIND_CODE`, with the prologue offset it applies at. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UnwindCode { + /// Offset into the prologue of the instruction that follows this operation. + pub prolog_offset: u8, + /// What the instruction did. + pub op: UnwindOp, +} + +/// What a scope table entry names as its handler. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScopeHandler { + /// A filter or termination-handler address. + Address(Addr), + /// The compiler folded a constant filter: 0 is `EXCEPTION_CONTINUE_SEARCH`, + /// 1 is `EXCEPTION_EXECUTE_HANDLER`. `__except(1)` produces the latter. + Constant(u32), +} + +/// One `__try` region of a `__C_specific_handler` scope table. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ScopeEntry { + /// First address the region covers. + pub begin: Addr, + /// One past the last address it covers. + pub end: Addr, + /// The filter, for `__except`; the termination handler, for `__finally`. + pub handler: ScopeHandler, + /// Where control resumes when the filter accepts. Absent for `__finally`. + pub target: Option, +} + +/// A decoded `UNWIND_INFO` record. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnwindInfo { + /// The function this describes. + pub function: Addr, + /// Where the record itself is, as an RVA. + pub rva: u32, + /// Format version, 1 or 2. + pub version: u8, + /// The `UNW_FLAG_*` bits. + pub flags: u8, + /// Bytes of prologue. + pub prolog_size: u8, + /// Register holding the frame pointer, absent when the function addresses + /// its frame through `rsp`. + pub frame_register: Option, + /// Distance from `rsp` at the point the frame pointer was established. + pub frame_offset: u32, + /// The prologue, in the order the unwinder undoes it, which is the reverse + /// of the order the processor executed it. Codes from a chained parent + /// follow this record's own. + pub codes: Vec, + /// Total bytes the prologue subtracts from `rsp`, chained records included. + pub stack_alloc: u64, + /// The language-specific handler routine, when the record names one. + pub handler: Option, + /// Start of the `RUNTIME_FUNCTION` this record chains to. A funclet points + /// at the function it was split out of, and the prologue lives there. + pub chained: Option, + /// The scope table, when the language-specific data validated as one. + pub scopes: Vec, +} + +impl UnwindInfo { + /// True when the record names a handler that runs during the first pass. + pub fn has_exception_handler(&self) -> bool { + self.flags & UNW_FLAG_EHANDLER != 0 + } + + /// True when the record names a handler that runs while unwinding. + pub fn has_termination_handler(&self) -> bool { + self.flags & UNW_FLAG_UHANDLER != 0 + } +} + +/// The x86-64 integer register a `UNWIND_CODE` register number names. +pub fn x64_register(n: u8) -> &'static str { + const NAMES: [&str; 16] = [ + "rax", "rcx", "rdx", "rbx", "rsp", "rbp", "rsi", "rdi", "r8", "r9", "r10", "r11", "r12", + "r13", "r14", "r15", + ]; + NAMES[(n & 0xf) as usize] +} + +/// Everything the exception directory and its unwind data produced. +#[derive(Debug, Clone, Default)] +pub struct Exceptions { + /// One per `RUNTIME_FUNCTION`, in directory order. + pub functions: Vec, + /// The `UNWIND_INFO` records that were reachable and decodable, x86-64 + /// only. Shorter than `functions` whenever a record was absent or refused. + pub unwind: Vec, +} + +/// Bytes per `RUNTIME_FUNCTION` for a machine, or `None` when the machine has +/// no exception directory in this shape. +fn entry_size(arch: &Arch) -> Option { + match arch { + Arch::X86_64 => Some(12), + // Both ARM forms store a start and one word of unwind data, with no + // end address: the length is inside the unwind data. + Arch::AArch64 | Arch::Arm | Arch::Thumb => Some(8), + _ => None, + } +} + +/// Walk the exception directory, emitting a proven hint per entry. +pub fn read( + img: &Image<'_>, + dir_rva: u32, + dir_size: u32, + caps: &Caps, + obj: &mut Object, +) -> Exceptions { + let mut out = Exceptions::default(); + if dir_rva == 0 || dir_size == 0 { + return out; + } + let Some(stride) = entry_size(img.arch()) else { + obj.warnings.push(format!( + "exception directory present but {} has no RUNTIME_FUNCTION layout; skipped", + img.arch() + )); + return out; + }; + let Some(at) = img.offset_of(dir_rva) else { + obj.warnings + .push("exception directory RVA is not inside any section".into()); + return out; + }; + // Bound the count by the bytes that actually exist before touching any of + // them: a size field is attacker-controlled and a capacity is not a promise. + let available = img.len().saturating_sub(at); + let count = (dir_size as u64 / stride) + .min(available / stride) + .min(caps.symbols); + if count == 0 { + obj.warnings.push(format!( + "exception directory claims {dir_size} bytes but no whole entry fits in the file" + )); + return out; + } + if (dir_size as u64) / stride > count { + obj.warnings.push(format!( + "exception directory claims {} entries, {count} fit in the file", + dir_size as u64 / stride + )); + } + + let mut unmapped = 0u64; + let mut arm32 = 0u64; + for i in 0..count { + let Ok(mut e) = img + .reader() + .slice_at("runtime function", at + i * stride, stride) + else { + break; + }; + let Ok(begin) = e.u32("BeginAddress") else { + break; + }; + if begin == 0 { + // A terminator, or a hole the linker left. + continue; + } + let start = img.addr_of(begin); + let func = match img.arch() { + Arch::X86_64 => { + let (Ok(end), Ok(unwind)) = (e.u32("EndAddress"), e.u32("UnwindInfoAddress")) + else { + break; + }; + if end <= begin { + obj.warnings.push(format!( + "runtime function at {start} ends at or before it starts; ignored" + )); + continue; + } + RuntimeFunction { + start, + end: Some(img.addr_of(end)), + unwind_rva: (unwind != 0).then_some(unwind), + packed: None, + } + } + Arch::AArch64 => { + let Ok(data) = e.u32("UnwindData") else { break }; + arm64_entry(img, start, data, obj) + } + // The ARM32 packed form counts in halfwords and its `.xdata` header + // differs from ARM64's. Neither is decoded here, so the end stays + // unknown rather than being guessed from the ARM64 layout. + _ => { + let Ok(data) = e.u32("UnwindData") else { break }; + arm32 += 1; + RuntimeFunction { + start, + end: None, + unwind_rva: (data & 3 == 0 && data != 0).then_some(data), + packed: None, + } + } + }; + + if !img.is_code(func.start) { + unmapped += 1; + continue; + } + obj.function_hints.push(FunctionHint { + addr: func.start, + size: func + .end + .and_then(|e| e.get().checked_sub(func.start.get())) + .filter(|n| *n > 0), + name: None, + provenance: Provenance::new(Evidence::PeUnwind), + }); + out.functions.push(func); + } + if unmapped != 0 { + obj.warnings.push(format!( + "{unmapped} runtime functions start outside every executable section; ignored" + )); + } + if arm32 != 0 { + obj.warnings.push(format!( + "{arm32} ARM32 runtime functions gave starts only; their unwind encoding is not \ + decoded, so no end address is claimed" + )); + } + + if *img.arch() == Arch::X86_64 { + for f in &out.functions { + let Some(rva) = f.unwind_rva else { continue }; + if let Some(info) = read_unwind_info(img, f, rva, 0, caps, obj) { + out.unwind.push(info); + } + } + } + out +} + +/// One ARM64 `.pdata` entry: either a packed frame description or an RVA into +/// `.xdata`, decided by the low two bits. +fn arm64_entry(img: &Image<'_>, start: Addr, data: u32, obj: &mut Object) -> RuntimeFunction { + if data & 3 == 0 { + let rva = data; + let (length, handler) = arm64_xdata(img, rva, obj); + // The handler is code nothing else in the image references. + if let Some(h) = handler { + if img.is_code(h) { + obj.function_hints.push(FunctionHint { + addr: h, + size: None, + name: None, + provenance: Provenance::new(Evidence::PeUnwind), + }); + } else { + obj.warnings.push(format!( + "ARM64 unwind handler for the function at {start} is {h}, which is not \ + executable" + )); + } + } + return RuntimeFunction { + start, + end: length.and_then(|n| start.checked_add(n)), + unwind_rva: Some(rva), + packed: None, + }; + } + // Packed: the whole frame description is these 32 bits. Lengths count + // instructions, so the byte length is four times the field. + let packed = Arm64Packed { + flag: (data & 3) as u8, + function_length: ((data >> 2) & 0x7ff) * 4, + reg_f: ((data >> 13) & 0x7) as u8, + reg_i: ((data >> 16) & 0xf) as u8, + homed_parameters: (data >> 20) & 1 != 0, + cr: ((data >> 21) & 0x3) as u8, + frame_size: ((data >> 23) & 0x1ff) * 16, + }; + RuntimeFunction { + start, + end: start.checked_add(packed.function_length as u64), + unwind_rva: None, + packed: Some(packed), + } +} + +/// The ARM64 `.xdata` header, for the function length and the handler flag. +/// +/// The unwind opcodes that follow are not decoded: they describe the prologue +/// in an encoding of their own and nothing above this crate asks for it yet. +fn arm64_xdata(img: &Image<'_>, rva: u32, obj: &mut Object) -> (Option, Option) { + let Some(at) = img.offset_of(rva) else { + obj.warnings + .push(format!("ARM64 unwind data at RVA {rva:#x} is not mapped")); + return (None, None); + }; + let Ok(mut r) = img.reader().slice_at("arm64 unwind header", at, 4) else { + return (None, None); + }; + let Ok(word) = r.u32("header") else { + return (None, None); + }; + // Lengths count instructions, and every A64 instruction is four bytes. + let length = (word & 0x3_ffff) as u64 * 4; + let version = (word >> 18) & 0x3; + let has_exception_data = (word >> 20) & 1 != 0; + // E: one packed epilogue, in which case the epilogue-count field is an + // index into the opcodes rather than a count of scope words. + let packed_epilogue = (word >> 21) & 1 != 0; + let epilog_field = (word >> 22) & 0x1f; + let code_words = (word >> 27) & 0x1f; + if version != 0 { + obj.warnings.push(format!( + "ARM64 unwind data at RVA {rva:#x} is version {version}, which this parser does not \ + know; only its length was read" + )); + return (Some(length), None); + } + + // A zero in both counts means an extension word carries the real ones. + let (epilog_field, code_words, header_words) = if epilog_field == 0 && code_words == 0 { + let Ok(mut ext) = img.reader().slice_at("arm64 unwind extension", at + 4, 4) else { + return (Some(length), None); + }; + let Ok(w) = ext.u32("extension") else { + return (Some(length), None); + }; + (w & 0xffff, (w >> 16) & 0xff, 2u64) + } else { + (epilog_field, code_words, 1u64) + }; + + if !has_exception_data { + return (Some(length), None); + } + // Header words, then one word per epilogue scope unless the single + // epilogue was packed into the header, then the opcode words. The handler + // RVA follows all of it. + let epilog_words = if packed_epilogue { + 0 + } else { + epilog_field as u64 + }; + let handler_at = at + + (header_words + epilog_words + code_words as u64) + .saturating_mul(4) + .min(u32::MAX as u64); + let handler = img + .reader() + .slice_at("arm64 exception handler", handler_at, 4) + .ok() + .and_then(|mut h| h.u32("handler RVA").ok()) + .filter(|rva| *rva != 0) + .map(|rva| img.addr_of(rva)); + (Some(length), handler) +} + +/// Decode one `UNWIND_INFO`, following a chain to the record that holds the +/// prologue when this one is a funclet. +fn read_unwind_info( + img: &Image<'_>, + func: &RuntimeFunction, + rva: u32, + depth: u32, + caps: &Caps, + obj: &mut Object, +) -> Option { + let at = match img.offset_of(rva) { + Some(at) => at, + None => { + obj.warnings.push(format!( + "unwind info for the function at {} is at RVA {rva:#x}, which no section maps", + func.start + )); + return None; + } + }; + let mut h = img.reader().slice_at("UNWIND_INFO", at, 4).ok()?; + let version_flags = h.u8("VersionAndFlags").ok()?; + let prolog_size = h.u8("SizeOfProlog").ok()?; + let count_of_codes = h.u8("CountOfCodes").ok()?; + let frame = h.u8("FrameRegisterAndOffset").ok()?; + let version = version_flags & 0x7; + let flags = version_flags >> 3; + if version == 0 || version > 2 { + obj.warnings.push(format!( + "unwind info for the function at {} claims version {version}, which is not 1 or 2", + func.start + )); + return None; + } + + // Codes are two bytes each and the array is padded to a four-byte boundary, + // so an odd count reserves one slot more than it uses. + let slots = (count_of_codes as u64).next_multiple_of(2); + let codes_bytes = slots * 2; + let body = img + .reader() + .slice_at("unwind codes", at + 4, codes_bytes) + .ok()?; + + let mut info = UnwindInfo { + function: func.start, + rva, + version, + flags, + prolog_size, + frame_register: (frame & 0xf != 0).then_some(frame & 0xf), + frame_offset: (frame >> 4) as u32 * 16, + codes: Vec::new(), + stack_alloc: 0, + handler: None, + chained: None, + scopes: Vec::new(), + }; + decode_codes(&body, count_of_codes, version, &mut info, obj); + + let tail = at + 4 + codes_bytes; + if flags & (UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER) != 0 { + if let Ok(mut t) = img.reader().slice_at("exception handler RVA", tail, 4) { + if let Ok(handler_rva) = t.u32("ExceptionHandler") { + if handler_rva != 0 { + let addr = img.addr_of(handler_rva); + if img.is_code(addr) { + info.handler = Some(addr); + obj.function_hints.push(FunctionHint { + addr, + size: None, + name: None, + provenance: Provenance::new(Evidence::PeUnwind), + }); + } else { + obj.warnings.push(format!( + "unwind handler for the function at {} points at {addr}, which is not \ + executable", + func.start + )); + } + } + read_scope_table(img, func, tail + 4, caps, &mut info, obj); + } + } + } else if flags & UNW_FLAG_CHAININFO != 0 && depth < MAX_CHAIN { + if let Ok(mut c) = img.reader().slice_at("chained RUNTIME_FUNCTION", tail, 12) { + if let (Ok(begin), Ok(_end), Ok(parent_rva)) = ( + c.u32("BeginAddress"), + c.u32("EndAddress"), + c.u32("UnwindInfoAddress"), + ) { + let parent_start = img.addr_of(begin); + info.chained = Some(parent_start); + if img.is_code(parent_start) { + obj.function_hints.push(FunctionHint { + addr: parent_start, + size: None, + name: None, + provenance: Provenance::new(Evidence::PeUnwind), + }); + } + // The parent holds the prologue this funclet shares, so its + // codes belong in the same frame description. + if parent_rva != 0 && parent_rva != rva { + let parent_fn = RuntimeFunction { + start: parent_start, + end: None, + unwind_rva: Some(parent_rva), + packed: None, + }; + if let Some(parent) = + read_unwind_info(img, &parent_fn, parent_rva, depth + 1, caps, obj) + { + info.codes.extend_from_slice(&parent.codes); + info.stack_alloc += parent.stack_alloc; + if info.frame_register.is_none() { + info.frame_register = parent.frame_register; + info.frame_offset = parent.frame_offset; + } + } + } + } + } + } else if flags & UNW_FLAG_CHAININFO != 0 { + obj.warnings.push(format!( + "unwind chain from the function at {} is more than {MAX_CHAIN} deep; stopped", + func.start + )); + } + + Some(info) +} + +/// Walk the `UNWIND_CODE` array. Every arm advances at least one slot, so the +/// walk terminates whatever the bytes say. +fn decode_codes( + body: &e5r_core::Reader<'_>, + count: u8, + version: u8, + info: &mut UnwindInfo, + obj: &mut Object, +) { + let slot = |i: u64| -> Option { + body.slice_at("unwind code", i * 2, 2) + .ok()? + .u16("code") + .ok() + }; + let mut i = 0u64; + while i < count as u64 { + let Some(raw) = slot(i) else { break }; + let prolog_offset = (raw & 0xff) as u8; + let op = ((raw >> 8) & 0xf) as u8; + let extra = ((raw >> 12) & 0xf) as u8; + let (nodes, decoded) = match op { + 0 => (1, UnwindOp::PushNonVolatile { reg: extra }), + 1 => match extra { + 0 => match slot(i + 1) { + Some(n) => ( + 2, + UnwindOp::Alloc { + bytes: n as u64 * 8, + }, + ), + None => break, + }, + 1 => match (slot(i + 1), slot(i + 2)) { + (Some(lo), Some(hi)) => ( + 3, + UnwindOp::Alloc { + bytes: lo as u64 | ((hi as u64) << 16), + }, + ), + _ => break, + }, + info_bits => ( + 1, + UnwindOp::Unknown { + op, + info: info_bits, + }, + ), + }, + 2 => ( + 1, + UnwindOp::Alloc { + bytes: extra as u64 * 8 + 8, + }, + ), + 3 => (1, UnwindOp::SetFramePointer), + 4 => match slot(i + 1) { + Some(n) => ( + 2, + UnwindOp::SaveNonVolatile { + reg: extra, + offset: n as u64 * 8, + }, + ), + None => break, + }, + 5 => match (slot(i + 1), slot(i + 2)) { + (Some(lo), Some(hi)) => ( + 3, + UnwindOp::SaveNonVolatile { + reg: extra, + offset: lo as u64 | ((hi as u64) << 16), + }, + ), + _ => break, + }, + // Version 2 reuses 6 and 7 for epilogue description. Version 1 left + // them undefined, and an undefined opcode is not a guess to make. + 6 if version >= 2 => (2, UnwindOp::Epilogue), + 7 if version >= 2 => (3, UnwindOp::Unknown { op, info: extra }), + 8 => match slot(i + 1) { + Some(n) => ( + 2, + UnwindOp::SaveXmm128 { + reg: extra, + offset: n as u64 * 16, + }, + ), + None => break, + }, + 9 => match (slot(i + 1), slot(i + 2)) { + (Some(lo), Some(hi)) => ( + 3, + UnwindOp::SaveXmm128 { + reg: extra, + offset: lo as u64 | ((hi as u64) << 16), + }, + ), + _ => break, + }, + 10 => ( + 1, + UnwindOp::PushMachineFrame { + error_code: extra == 1, + }, + ), + _ => { + obj.warnings.push(format!( + "unwind code {op} in the function at {} is not a documented operation; the \ + rest of the prologue was not read", + info.function + )); + info.codes.push(UnwindCode { + prolog_offset, + op: UnwindOp::Unknown { op, info: extra }, + }); + break; + } + }; + if let UnwindOp::Alloc { bytes } = decoded { + info.stack_alloc += bytes; + } + info.codes.push(UnwindCode { + prolog_offset, + op: decoded, + }); + i += nodes; + } +} + +/// Parse the language-specific data as a `__C_specific_handler` scope table, +/// and refuse it whole if any field fails. +/// +/// The image does not say which handler the data belongs to, so acceptance is +/// the evidence. A C++ image stores a `FuncInfo` RVA in the same place; read as +/// a count it is far larger than the bytes that follow, which is the first +/// check below. +fn read_scope_table( + img: &Image<'_>, + func: &RuntimeFunction, + at: u64, + caps: &Caps, + info: &mut UnwindInfo, + obj: &mut Object, +) { + let Some(end) = func.end else { return }; + let Ok(mut head) = img.reader().slice_at("scope table count", at, 4) else { + return; + }; + let Ok(count) = head.u32("Count") else { return }; + if count == 0 { + return; + } + // The table cannot run past the section holding it, and it cannot be larger + // than the file. Both bounds come before any allocation. + let room = img + .section_bytes_after(at) + .unwrap_or_else(|| img.len().saturating_sub(at)); + let want = 4u64 + count as u64 * 16; + if count as u64 > MAX_SCOPE_ENTRIES.min(caps.jump_table_entries) || want > room { + obj.warnings.push(format!( + "language-specific data for the function at {} is not a scope table: it claims \ + {count} entries, which does not fit the {room} bytes that follow", + func.start + )); + return; + } + + let body = func.start..end; + let mut entries = Vec::with_capacity(count as usize); + for i in 0..count as u64 { + let Ok(mut e) = img + .reader() + .slice_at("scope table entry", at + 4 + i * 16, 16) + else { + return; + }; + let (Ok(begin), Ok(stop), Ok(handler), Ok(target)) = ( + e.u32("BeginAddress"), + e.u32("EndAddress"), + e.u32("HandlerAddress"), + e.u32("JumpTarget"), + ) else { + return; + }; + let begin = img.addr_of(begin); + let stop = img.addr_of(stop); + // Every guarded region belongs to the function that declares it. This + // is the check that keeps a C++ FuncInfo from being read as a table. + if begin >= stop || !body.contains(&begin) || stop > end { + obj.warnings.push(format!( + "language-specific data for the function at {} is not a scope table: entry {i} \ + guards {begin}..{stop}, which is not inside the function", + func.start + )); + return; + } + entries.push(ScopeEntry { + begin, + end: stop, + handler: if handler <= 1 { + ScopeHandler::Constant(handler) + } else { + ScopeHandler::Address(img.addr_of(handler)) + }, + target: (target != 0).then(|| img.addr_of(target)), + }); + } + + // Only now, with every entry validated, are the addresses worth acting on. + for entry in &entries { + if let ScopeHandler::Address(addr) = entry.handler { + claim(img, func, addr, "scope table filter", obj); + } + if let Some(target) = entry.target { + claim(img, func, target, "scope table handler", obj); + } + } + info.scopes = entries; +} + +/// Turn one scope-table address into a hint, a nothing, or a warning. +/// +/// Inside the declaring function it is a block, not an entry, so it is recorded +/// in the scope table and nowhere else. Outside the function and executable, it +/// is a funclet that nothing else references. Outside every section it is not +/// evidence at all. +fn claim(img: &Image<'_>, func: &RuntimeFunction, addr: Addr, what: &str, obj: &mut Object) { + if let Some(end) = func.end { + if (func.start..end).contains(&addr) { + return; + } + } + if img.is_code(addr) { + obj.function_hints.push(FunctionHint { + addr, + size: None, + name: None, + provenance: Provenance::new(Evidence::PeUnwind), + }); + } else if img.is_mapped(addr) { + obj.warnings.push(format!( + "{what} for the function at {} is {addr}, which is mapped but not executable", + func.start + )); + } else { + obj.warnings.push(format!( + "{what} for the function at {} is {addr}, which is outside every section", + func.start + )); + } +} diff --git a/crates/e5r-format/src/pdb.rs b/crates/e5r-format/src/pdb.rs new file mode 100644 index 0000000..c0c5dd8 --- /dev/null +++ b/crates/e5r-format/src/pdb.rs @@ -0,0 +1,2371 @@ +//! Reading a Windows program database. +//! +//! MSVC keeps almost nothing in the image: the PE carries a GUID, an age and a +//! path, and everything the compiler knew lives in a separate file. This reads +//! that file and produces the same [`crate::dwarf::DebugInfo`] the DWARF reader +//! does, so everything downstream is told what the compiler knew in one shape +//! rather than two. +//! +//! Written from the public documentation of the format. A leaf or a symbol kind +//! this does not know is stepped over by its declared length rather than +//! guessed at, so an unfamiliar producer costs coverage and not correctness. + +use std::collections::BTreeMap; +use std::fmt::Write as _; + +use e5r_core::{Addr, AddrRange, Evidence, Provenance, Reader}; +use e5r_types::ctype::{Composite, Enumeration, Field, Signature, Type, TypeId, Types}; + +use crate::dwarf::{ + DebugFunction, DebugInfo, DebugLocal, DebugVariable, InlinedFrame, LineRow, Location, + LocationRange, +}; + +/// A cursor over one stream. +/// +/// Same reason the DWARF reader has one: the core reader labels every read for +/// its error messages, thousands of fields here would carry the same label, and +/// every caller turns a failure into `None` anyway. A program database is +/// little endian wherever it is written. +struct Cur<'a>(Reader<'a>); + +impl<'a> Cur<'a> { + fn new(data: &'a [u8]) -> Cur<'a> { + Cur(Reader::le(data)) + } + fn position(&self) -> usize { + self.0.pos() as usize + } + fn len(&self) -> usize { + self.0.len() + } + fn remaining(&self) -> usize { + self.0.remaining() + } + fn is_empty(&self) -> bool { + self.0.remaining() == 0 + } + fn seek(&mut self, at: usize) -> bool { + self.0.seek("pdb", at as u64).is_ok() + } + /// Step to the next four byte boundary, where the next entry of an aligned + /// table starts. False at the end, which ends the walk that called it. + fn align(&mut self) -> bool { + let at = self.position().next_multiple_of(4); + self.seek(at) + } + fn peek(&self) -> Option { + self.0.data().get(self.position()).copied() + } + fn u8(&mut self) -> Option { + self.0.u8("pdb").ok() + } + fn u16(&mut self) -> Option { + self.0.u16("pdb").ok() + } + fn u32(&mut self) -> Option { + self.0.u32("pdb").ok() + } + fn i32(&mut self) -> Option { + self.0.i32("pdb").ok() + } + fn u64(&mut self) -> Option { + self.0.u64("pdb").ok() + } + fn bytes(&mut self, n: usize) -> Option<&'a [u8]> { + self.0.bytes("pdb", n as u64).ok() + } + fn cstr(&mut self) -> Option { + let at = self.0.pos(); + let bytes = self.0.cstr_at("pdb", at, self.0.remaining() as u64).ok()?; + let out = String::from_utf8_lossy(bytes).into_owned(); + let _ = self.0.seek("pdb", at + bytes.len() as u64 + 1); + Some(out) + } +} + +/// The container: fixed size blocks, and a directory saying which blocks each +/// stream is scattered across. +/// +/// Public because matching and reading are separate jobs: a caller that only +/// wants to know whether this file belongs to that image should not pay for +/// the type graph. +pub struct Msf<'a> { + data: &'a [u8], + block_size: usize, + streams: Vec<(usize, Vec)>, +} + +impl<'a> Msf<'a> { + /// Open a container, or `None` when these bytes are not one. + pub fn open(data: &'a [u8]) -> Option> { + let mut r = Cur::new(data); + if r.bytes(MAGIC.len())? != MAGIC { + return None; + } + let block_size = r.u32()? as usize; + let _free_block_map = r.u32()?; + let block_count = r.u32()? as usize; + let directory_bytes = r.u32()? as usize; + let _unknown = r.u32()?; + let block_map = r.u32()? as usize; + // Every offset in the file is a block number multiplied by this, so it + // is checked before any of them are used: a power of two in the range + // the format allows, in a file that holds the blocks it claims. + if !(512..=0x10000).contains(&block_size) || !block_size.is_power_of_two() { + return None; + } + if block_count.checked_mul(block_size)? > data.len() || directory_bytes > data.len() { + return None; + } + // The directory is itself a stream, and the superblock names the one + // place its own block list lives. + let mut map = Cur::new(data); + if !map.seek(block_map.checked_mul(block_size)?) { + return None; + } + let mut blocks = Vec::with_capacity(directory_bytes.div_ceil(block_size)); + for _ in 0..directory_bytes.div_ceil(block_size) { + blocks.push(map.u32()?); + } + let directory = gather(data, block_size, &blocks, directory_bytes)?; + + let mut d = Cur::new(&directory); + let count = d.u32()? as usize; + // A stream costs four bytes of size in the directory before it costs + // anything else, so the directory cannot name more streams than a + // quarter of its own length. + if count > directory_bytes / 4 { + return None; + } + let mut sizes = Vec::with_capacity(count); + for _ in 0..count { + let size = d.u32()?; + // The all-ones size marks a stream that is not there at all. + sizes.push(if size == u32::MAX { 0 } else { size as usize }); + } + let mut streams = Vec::with_capacity(count); + for size in sizes { + // A stream cannot be longer than the file it is stored in. The + // block list is then reserved against what is left of the + // directory rather than against the claimed size: the blocks are + // read through the directory's own cursor, so a lying length runs + // out of directory, but reserving for it first would have + // allocated four bytes per claimed block before finding out. + if size > data.len() { + return None; + } + let want = size.div_ceil(block_size); + if want > d.remaining() / 4 { + return None; + } + let mut blocks = Vec::with_capacity(want); + for _ in 0..want { + blocks.push(d.u32()?); + } + streams.push((size, blocks)); + } + Some(Msf { + data, + block_size, + streams, + }) + } + + /// How many streams the directory names. + pub fn count(&self) -> usize { + self.streams.len() + } + + /// One stream's bytes, gathered from the blocks holding them. + pub fn stream(&self, index: usize) -> Option> { + let (size, blocks) = self.streams.get(index)?; + gather(self.data, self.block_size, blocks, *size) + } +} + +/// Copy a stream out of the blocks it is scattered across. +fn gather(data: &[u8], block_size: usize, blocks: &[u32], size: usize) -> Option> { + let mut out = Vec::with_capacity(size); + for block in blocks { + let at = (*block as usize).checked_mul(block_size)?; + let chunk = data.get(at..at.checked_add(block_size)?)?; + let take = block_size.min(size - out.len()); + out.extend_from_slice(&chunk[..take]); + if out.len() == size { + break; + } + } + (out.len() == size).then_some(out) +} + +/// What identifies a program database to the image that references it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Identity { + /// The GUID the linker generated, in the order the debug directory stores + /// it: two little endian words, then eight bytes. + pub guid: [u8; 16], + /// How many times the database has been written since. + pub age: u32, + /// The timestamp signature, which is all a database older than GUIDs has. + pub signature: u32, +} + +impl Identity { + /// The key a symbol server is indexed by: the GUID's fields as Windows + /// prints them, then the age. + pub fn key(&self) -> String { + let g = &self.guid; + let mut out = format!( + "{:08X}{:04X}{:04X}", + u32::from_le_bytes([g[0], g[1], g[2], g[3]]), + u16::from_le_bytes([g[4], g[5]]), + u16::from_le_bytes([g[6], g[7]]), + ); + for b in &g[8..] { + let _ = write!(out, "{b:02X}"); + } + let _ = write!(out, "{:X}", self.age); + out + } + + /// True when this is the database a debug directory asked for. Both halves + /// matter: a rebuild keeps the GUID and bumps the age. + pub fn matches(&self, guid: &[u8; 16], age: u32) -> bool { + self.guid == *guid && self.age == age + } +} + +/// The identity and path a PE debug directory's CodeView record names. +/// +/// This is the other half of matching: the image says which database it wants, +/// [`identity`] says what a database is, and [`Identity::matches`] decides. It +/// takes the record's bytes rather than the image so that the PE loader, which +/// already has them, needs one line to use it. +pub fn codeview_identity(record: &[u8]) -> Option<(Identity, String)> { + let mut r = Cur::new(record); + if r.bytes(4)? != CODEVIEW_RSDS { + return None; + } + let mut out = Identity::default(); + out.guid.copy_from_slice(r.bytes(16)?); + out.age = r.u32()?; + Some((out, r.cstr()?)) +} + +/// What a program database says it is, without reading the rest of it. +/// +/// Matching is what a caller needs first: symbols from the wrong build are +/// worse than no symbols, because they are confidently wrong. +pub fn identity(data: &[u8]) -> Option { + let msf = Msf::open(data)?; + Some(info(&msf.stream(STREAM_INFO)?)?.0) +} + +/// Stream one: the identity, and the names given to the streams that have one. +fn info(data: &[u8]) -> Option<(Identity, BTreeMap)> { + let mut r = Cur::new(data); + let version = r.u32()?; + let signature = r.u32()?; + let age = r.u32()?; + let mut out = Identity { + age, + signature, + ..Default::default() + }; + // Before this version there is no GUID and no name table, only the + // timestamp, which is still enough to reject the wrong file. + if version < VERSION_VC70 { + return Some((out, BTreeMap::new())); + } + out.guid.copy_from_slice(r.bytes(16)?); + + let names = r.u32()? as usize; + let names = r.bytes(names)?; + let count = r.u32()? as usize; + let _capacity = r.u32()?; + // Two bit vectors say which slots are in use. Their words are read through + // the cursor, so a lying word count runs out of stream. + for _ in 0..2 { + let words = r.u32()? as usize; + if words > r.remaining() / 4 { + return Some((out, BTreeMap::new())); + } + r.bytes(words * 4)?; + } + // An entry is a name offset and a stream number, so the stream bounds how + // many of them there can be. + if count > r.remaining() / 8 { + return Some((out, BTreeMap::new())); + } + let mut named = BTreeMap::new(); + for _ in 0..count { + let at = r.u32()?; + let stream = r.u32()?; + named.insert(cstring(names, at), stream as u16); + } + Some((out, named)) +} + +/// A NUL terminated string at an offset into a buffer. +fn cstring(data: &[u8], at: u32) -> String { + let Some(rest) = data.get(at as usize..) else { + return String::new(); + }; + let end = rest.iter().position(|b| *b == 0).unwrap_or(rest.len()); + String::from_utf8_lossy(&rest[..end]).into_owned() +} + +/// Where the DBI stream says the rest of the information lives. +#[derive(Debug, Default)] +struct Dbi { + /// The stream holding the global symbol records. + symbols: u16, + /// One entry per object file that went into the link. + modules: Vec, + /// The linker's copy of the image's section headers, which is how a + /// section and offset pair becomes an address without the image. + section_headers: Option, + /// What the image targets, which decides how a register number in a + /// location record is read. + machine: u16, + /// Which module contributed each run of bytes to which section. + contributions: Vec, +} + +/// One module: where its symbols and its line information are. +#[derive(Debug, Default)] +struct Module { + stream: u16, + symbol_bytes: u32, + c11_bytes: u32, + c13_bytes: u32, + /// The object file, or the archive member, the entry names. + name: String, + /// The object the linker read it out of, which is the archive for a + /// member and the same name again otherwise. + object: String, +} + +/// A section contribution before the section bases are known. +#[derive(Debug, Clone, Copy)] +struct RawContribution { + section: u16, + offset: u32, + size: u32, + characteristics: u32, + module: u16, +} + +/// One run of bytes a module put into a section. +/// +/// This is the map from address back to object file, which is what says which +/// translation unit a piece of code came from when the symbol records do not. +#[derive(Debug, Clone)] +pub struct Contribution { + /// The addresses it covers. + pub range: AddrRange, + /// Index into [`Pdb::modules`]. + pub module: u16, + /// The section characteristics the linker recorded for the run. + pub characteristics: u32, +} + +/// What a module entry says about itself. +#[derive(Debug, Clone, Default)] +pub struct ModuleInfo { + /// The object file the entry names. + pub name: String, + /// The archive it came out of, when it came out of one. + pub object: String, + /// The producer string from the module's `S_COMPILE3` record, when it has + /// one. `* Linker *` modules carry the linker's own version here. + pub producer: Option, +} + +/// What a procedure's frame record says about its stack frame. +/// +/// `DebugFunction` has nowhere to put this because DWARF describes a frame +/// with an expression rather than a size, so it is reported alongside rather +/// than folded in. +#[derive(Debug, Clone, Default)] +pub struct Frame { + /// Bytes of stack frame, not counting the return address. + pub total_bytes: u32, + /// Bytes of that which are padding. + pub padding_bytes: u32, + /// Bytes of callee saved registers inside the frame. + pub callee_saved_bytes: u32, + /// The exception handler the frame installs, when it installs one. + pub exception_handler: Option, + /// Which register locals are addressed from, as a DWARF register number, + /// when the encoding names one this knows. + pub local_base: Option, + /// The same for parameters. + pub param_base: Option, + /// The flag word as the record spells it. + pub flags: u32, +} + +/// Stream three: the header, then substreams laid end to end in a fixed order. +fn dbi(data: &[u8]) -> Option { + let mut r = Cur::new(data); + if r.i32()? != -1 { + return None; + } + let _version = r.u32()?; + let _age = r.u32()?; + let _globals = r.u16()?; + let _build = r.u16()?; + let _publics = r.u16()?; + let _pdb_dll = r.u16()?; + let symbols = r.u16()?; + let _rebuild = r.u16()?; + // Every substream length is signed in the header; a negative one is not a + // short substream, it is a malformed file. + let modules = usize::try_from(r.i32()?).ok()?; + let contributions = usize::try_from(r.i32()?).ok()?; + let section_map = usize::try_from(r.i32()?).ok()?; + let sources = usize::try_from(r.i32()?).ok()?; + let type_servers = usize::try_from(r.i32()?).ok()?; + let _mfc = r.u32()?; + let optional = usize::try_from(r.i32()?).ok()?; + let edit_continue = usize::try_from(r.i32()?).ok()?; + let _flags = r.u16()?; + let machine = r.u16()?; + let _padding = r.u32()?; + + let start = r.position(); + let mut at = start.checked_add(modules)?; + let modules = module_list(data.get(start..at).unwrap_or_default()); + let contributions = { + let end = at.checked_add(contributions)?; + let body = data.get(at..end).unwrap_or_default(); + at = end; + contribution_list(body) + }; + // The optional header is last, behind four more substreams whose lengths + // are all from the file, so the walk to it is checked at every step. + for n in [section_map, sources, type_servers, edit_continue] { + at = at.checked_add(n)?; + } + let header = data.get(at..at.checked_add(optional)?).unwrap_or_default(); + let section_headers = header + .get(DBG_SECTION_HEADERS * 2..DBG_SECTION_HEADERS * 2 + 2) + .map(|b| u16::from_le_bytes([b[0], b[1]])) + .filter(|n| *n != u16::MAX); + Some(Dbi { + symbols, + modules, + section_headers, + machine, + contributions, + }) +} + +/// The section contribution substream: a version word, then fixed size +/// entries. The later version appends a field to each entry rather than +/// changing the ones in front of it. +fn contribution_list(data: &[u8]) -> Vec { + let mut r = Cur::new(data); + let Some(version) = r.u32() else { + return Vec::new(); + }; + let each = match version { + SECTION_CONTRIB_V2 => CONTRIBUTION_ENTRY + 4, + SECTION_CONTRIB_V1 => CONTRIBUTION_ENTRY, + // An unknown version has an unknown entry size, so stepping through it + // would be guessing at where each one ends. + _ => return Vec::new(), + }; + // Every entry costs `each` bytes, so the substream bounds the count. + let mut out = Vec::with_capacity(r.remaining() / each); + while r.remaining() >= each { + let before = r.position(); + let (Some(section), Some(_pad), Some(offset), Some(size)) = + (r.u16(), r.u16(), r.i32(), r.i32()) + else { + break; + }; + let (Some(characteristics), Some(module)) = (r.u32(), r.u16()) else { + break; + }; + if !r.seek(before + each) { + break; + } + // A negative offset or size is not a short run, it is a malformed + // entry, and a zero length run covers nothing worth reporting. + let (Ok(offset), Ok(size)) = (u32::try_from(offset), u32::try_from(size)) else { + continue; + }; + if size == 0 { + continue; + } + out.push(RawContribution { + section, + offset, + size, + characteristics, + module, + }); + } + out +} + +/// The modules a DBI substream lists. +fn module_list(data: &[u8]) -> Vec { + let mut r = Cur::new(data); + let mut out = Vec::new(); + // An entry is sixty four fixed bytes and two names, so the substream + // bounds the count on its own and the walk cannot fail to advance. + while r.remaining() > MODULE_ENTRY { + let before = r.position(); + let Some(module) = one_module(&mut r) else { + break; + }; + out.push(module); + if r.position() <= before || !r.align() { + break; + } + } + out +} + +fn one_module(r: &mut Cur<'_>) -> Option { + let _unused = r.u32()?; + let _contribution = r.bytes(28)?; + let _flags = r.u16()?; + let stream = r.u16()?; + let symbol_bytes = r.u32()?; + let c11_bytes = r.u32()?; + let c13_bytes = r.u32()?; + let _source_files = r.u16()?; + let _padding = r.u16()?; + let _unused = r.u32()?; + let _source_name = r.u32()?; + let _pdb_path = r.u32()?; + let name = r.cstr()?; + let object = r.cstr()?; + Some(Module { + stream, + symbol_bytes, + c11_bytes, + c13_bytes, + name, + object, + }) +} + +/// Where each section starts, so a section and offset pair becomes an address. +/// +/// The caller's headers win when it has them: they carry the image base, where +/// the linker's copy inside the database knows only relative addresses. +fn section_bases(sections: &[crate::Section], msf: &Msf<'_>, dbi: &Dbi) -> Vec { + if !sections.is_empty() { + return sections.iter().map(|s| s.range.start()).collect(); + } + let Some(stream) = dbi.section_headers.and_then(|n| msf.stream(n as usize)) else { + return Vec::new(); + }; + let mut r = Cur::new(&stream); + let mut out = Vec::new(); + // A header is forty bytes, so the stream bounds the count. + while r.remaining() >= SECTION_HEADER { + let (Some(_name), Some(_size), Some(rva), Some(_rest)) = + (r.bytes(8), r.u32(), r.u32(), r.bytes(24)) + else { + break; + }; + out.push(Addr(rva as u64)); + } + out +} + +/// A section and offset pair as an address. +fn address(bases: &[Addr], section: u16, offset: u32) -> Option { + // Sections are numbered from one; zero means the symbol is in none of them. + let base = bases.get((section as usize).checked_sub(1)?)?; + base.checked_add(offset as u64) +} + +/// A CodeView register number as the DWARF number for the same register. +/// +/// The two numberings are unrelated, and [`Location::Register`] is documented +/// as a DWARF number, so a register is translated here or reported as unknown. +/// A register the tables do not cover yields `None` rather than some other +/// register: a location naming the wrong register is worse than no location. +fn dwarf_register(machine: u16, reg: u16) -> Option { + match machine { + MACHINE_AMD64 => amd64_register(reg), + MACHINE_ARM64 => arm64_register(reg), + // i386 and 32-bit ARM are readable the same way, but nothing here has + // produced one to measure against, so they are left unmapped. + _ => None, + } +} + +/// x86-64. The byte, word and doubleword names are the same machine register +/// at another width, and DWARF numbers the register rather than the width. +fn amd64_register(reg: u16) -> Option { + // ax cx dx bx sp bp si di, in the System V numbering. + const WIDE: [u16; 8] = [0, 2, 1, 3, 7, 6, 4, 5]; + // al cl dl bl ah ch dh bh, which name the same four registers twice. + const LOW: [u16; 8] = [0, 2, 1, 3, 0, 2, 1, 3]; + // rax rbx rcx rdx rsi rdi rbp rsp r8..r15, in the order CodeView lists + // them, which is not the order DWARF numbers them in. + const QUAD: [u16; 16] = [0, 3, 2, 1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + Some(match reg { + CV_AMD64_AL..=CV_AMD64_BH => LOW[(reg - CV_AMD64_AL) as usize], + CV_AMD64_AX..=CV_AMD64_DI => WIDE[(reg - CV_AMD64_AX) as usize], + CV_AMD64_EAX..=CV_AMD64_EDI => WIDE[(reg - CV_AMD64_EAX) as usize], + CV_AMD64_RAX..=CV_AMD64_R15 => QUAD[(reg - CV_AMD64_RAX) as usize], + _ => return None, + }) +} + +/// AArch64. The vector registers are deliberately absent: the enumeration's +/// numbering for them has not been checked against a producer here, and a +/// guess would be an invention. +fn arm64_register(reg: u16) -> Option { + Some(match reg { + CV_ARM64_W0..=CV_ARM64_W30 => reg - CV_ARM64_W0, + CV_ARM64_X0..=CV_ARM64_X28 => reg - CV_ARM64_X0, + CV_ARM64_FP => 29, + CV_ARM64_LR => 30, + CV_ARM64_SP => 31, + _ => return None, + }) +} + +/// Type ids already built, so a graph with a cycle in it is walked once. +type Made = BTreeMap; + +/// The type records of one stream, indexed so a reference resolves on demand. +#[derive(Default)] +struct TypeTable<'a> { + records: BTreeMap, + /// The defining record for each tag, so a forward reference reaches the + /// members instead of the empty shell that names them. + definitions: BTreeMap, + /// True when the stream defers to a type server, which is a separate file + /// holding the records the modules that used it refer to. + type_server: bool, +} + +/// What a field list yielded: members for a structure, values for an enum. +#[derive(Default)] +struct FieldList { + members: Vec, + values: Vec<(String, i64)>, +} + +impl<'a> TypeTable<'a> { + /// Index a type or item stream. A malformed header leaves the table empty + /// rather than failing the load, because the symbols are worth having + /// without the types. + fn scan(data: &'a [u8]) -> TypeTable<'a> { + let mut out = TypeTable::default(); + let mut r = Cur::new(data); + let (Some(_version), Some(header), Some(begin)) = (r.u32(), r.u32(), r.u32()) else { + return out; + }; + if (header as usize) < TPI_HEADER || !r.seek(header as usize) { + return out; + } + let mut index = begin; + while !r.is_empty() { + let Some(length) = r.u16() else { break }; + // A record that cannot hold its own kind field cannot be stepped + // over, so the walk stops rather than spinning on it. Every other + // record consumes at least four bytes, which bounds how many of + // them a stream can hold. + if length < 2 { + break; + } + let Some(body) = r.bytes(length as usize) else { + break; + }; + let kind = u16::from_le_bytes([body[0], body[1]]); + let body = &body[2..]; + if kind == LF_TYPESERVER2 { + out.type_server = true; + } + if let Some(tag) = tag(kind, body) { + out.definitions.entry(tag).or_insert(index); + } + out.records.insert(index, (kind, body)); + let Some(next) = index.checked_add(1) else { + break; + }; + index = next; + } + out + } + + /// The C type a type index names, built once and remembered. + fn build(&self, index: u32, types: &mut Types, made: &mut Made, depth: u32) -> Option { + if depth > 32 { + return None; + } + if index < FIRST_RECORD { + return basic(index, types); + } + if let Some(id) = made.get(&index) { + return Some(*id); + } + let (kind, body) = *self.records.get(&index)?; + let mut r = Cur::new(body); + let id = match kind { + // Qualifiers do not change the layout, and carrying them through + // the decompiler would clutter every declaration. + LF_MODIFIER => { + let inner = r.u32()?; + self.build(inner, types, made, depth + 1)? + } + LF_POINTER => { + let inner = r.u32()?; + let to = self + .build(inner, types, made, depth + 1) + .unwrap_or(Types::VOID); + types.add(Type::Pointer(to)) + } + LF_ARRAY => { + let element = r.u32()?; + let _subscript = r.u32()?; + let bytes = numeric(&mut r)?; + let inner = self + .build(element, types, made, depth + 1) + .unwrap_or(Types::VOID); + // The record gives a size in bytes where C declares a count. + let count = types + .size_of(inner) + .filter(|n| *n > 0) + .map(|n| bytes.max(0) as u64 / n); + types.add(Type::Array(inner, count)) + } + LF_STRUCTURE | LF_CLASS | LF_INTERFACE | LF_UNION => { + let union = kind == LF_UNION; + let _count = r.u16()?; + let property = r.u16()?; + let fields = r.u32()?; + if !union { + let _derived = r.u32()?; + let _vshape = r.u32()?; + } + let size = numeric(&mut r)?; + let name = r.cstr()?; + if property & FORWARD_REFERENCE != 0 { + let at = self.definitions.get(&name).copied().filter(|a| *a != index); + if let Some(id) = at.and_then(|a| self.build(a, types, made, depth + 1)) { + made.insert(index, id); + return Some(id); + } + } + // Reserved first, so a member pointing back at this structure + // finds it instead of recursing forever. + let placeholder = types.reserve(&name); + made.insert(index, placeholder); + let mut list = FieldList::default(); + self.fields(fields, types, made, depth + 1, &mut list); + list.members.sort_by_key(|f| f.offset); + types.define( + placeholder, + Type::Composite(Composite { + name: Some(name), + union, + size: Some(size.max(0) as u64), + fields: list.members, + }), + ); + placeholder + } + LF_ENUM => { + let _count = r.u16()?; + let property = r.u16()?; + let underlying = r.u32()?; + let fields = r.u32()?; + let name = r.cstr()?; + if property & FORWARD_REFERENCE != 0 { + let at = self.definitions.get(&name).copied().filter(|a| *a != index); + if let Some(id) = at.and_then(|a| self.build(a, types, made, depth + 1)) { + made.insert(index, id); + return Some(id); + } + } + let mut list = FieldList::default(); + self.fields(fields, types, made, depth + 1, &mut list); + let size = self + .build(underlying, types, made, depth + 1) + .and_then(|u| types.size_of(u)) + .unwrap_or(4) as u8; + types.add(Type::Enum(Enumeration { + name: Some(name), + size, + values: list.values, + })) + } + LF_PROCEDURE => { + let returns = r.u32()?; + let (_call, _attributes, _count) = (r.u8()?, r.u8()?, r.u16()?); + let arguments = r.u32()?; + let signature = self.signature(returns, arguments, types, made, depth + 1); + types.add(Type::Function(signature)) + } + LF_MFUNCTION => { + let returns = r.u32()?; + let (_class, _this) = (r.u32()?, r.u32()?); + let (_call, _attributes, _count) = (r.u8()?, r.u8()?, r.u16()?); + let arguments = r.u32()?; + let signature = self.signature(returns, arguments, types, made, depth + 1); + types.add(Type::Function(signature)) + } + _ => return None, + }; + made.insert(index, id); + Some(id) + } + + /// The signature a procedure record describes. + fn signature( + &self, + returns: u32, + arguments: u32, + types: &mut Types, + made: &mut Made, + depth: u32, + ) -> Signature { + let mut out = Signature { + returns: self.build(returns, types, made, depth), + ..Default::default() + }; + let Some((LF_ARGLIST, body)) = self.records.get(&arguments).copied() else { + return out; + }; + let mut r = Cur::new(body); + let Some(count) = r.u32() else { return out }; + // A parameter costs four bytes, so the record bounds the count. + if count as usize > r.remaining() / 4 { + return out; + } + for _ in 0..count { + let Some(index) = r.u32() else { break }; + // A variadic function ends its list with the no-type index. + if index == T_NOTYPE { + out.varargs = true; + continue; + } + let ty = self.build(index, types, made, depth).unwrap_or(Types::VOID); + out.parameters.push((None, ty)); + } + out + } + + /// Walk a field list, collecting what a structure or an enum needs from it. + fn fields( + &self, + index: u32, + types: &mut Types, + made: &mut Made, + depth: u32, + out: &mut FieldList, + ) { + if depth > 32 { + return; + } + let Some((LF_FIELDLIST, body)) = self.records.get(&index).copied() else { + return; + }; + let mut r = Cur::new(body); + loop { + // Entries are aligned by a padding leaf whose low nibble says how + // far to step. A nibble of zero would not move, so it ends the + // walk rather than spinning. + while let Some(pad) = r.peek().filter(|b| *b >= LF_PAD) { + if pad & 0x0f == 0 || !r.seek(r.position() + (pad & 0x0f) as usize) { + return; + } + } + if r.is_empty() { + return; + } + let before = r.position(); + let Some(leaf) = r.u16() else { return }; + match leaf { + LF_MEMBER => { + let (Some(_attributes), Some(ty)) = (r.u16(), r.u32()) else { + return; + }; + let (Some(offset), Some(name)) = (numeric(&mut r), r.cstr()) else { + return; + }; + let (ty, bits) = self.member(ty, types, made, depth); + out.members.push(Field { + name, + ty, + offset: offset.max(0) as u64, + bits, + }); + } + LF_ENUMERATE => { + let Some(_attributes) = r.u16() else { return }; + let (Some(value), Some(name)) = (numeric(&mut r), r.cstr()) else { + return; + }; + out.values.push((name, value)); + } + // A base class occupies its offset like any other member, and + // that is what the recovered layout has to say. + LF_BCLASS => { + let (Some(_attributes), Some(ty)) = (r.u16(), r.u32()) else { + return; + }; + let Some(offset) = numeric(&mut r) else { + return; + }; + if let Some(id) = self.build(ty, types, made, depth + 1) { + out.members.push(Field { + name: types.name_of(id).replace(' ', "_"), + ty: id, + offset: offset.max(0) as u64, + bits: None, + }); + } + } + LF_VBCLASS | LF_IVBCLASS => { + let (Some(_attributes), Some(_base), Some(_pointer)) = + (r.u16(), r.u32(), r.u32()) + else { + return; + }; + if numeric(&mut r).is_none() || numeric(&mut r).is_none() { + return; + } + } + LF_VFUNCTAB => { + if r.u16().is_none() || r.u32().is_none() { + return; + } + } + LF_STMEMBER | LF_NESTTYPE => { + if r.u16().is_none() || r.u32().is_none() || r.cstr().is_none() { + return; + } + } + LF_METHOD => { + if r.u16().is_none() || r.u32().is_none() || r.cstr().is_none() { + return; + } + } + LF_ONEMETHOD => { + let (Some(attributes), Some(_ty)) = (r.u16(), r.u32()) else { + return; + }; + // A method that introduces a virtual carries its slot. + if matches!((attributes >> 2) & 0x7, 4 | 6) && r.u32().is_none() { + return; + } + if r.cstr().is_none() { + return; + } + } + // The list continues in another record when it outgrew this + // one; it is a jump, so this record is finished. + LF_INDEX => { + let (Some(_pad), Some(next)) = (r.u16(), r.u32()) else { + return; + }; + self.fields(next, types, made, depth + 1, out); + return; + } + // A leaf this does not know has no declared length, so the + // list stops here rather than reading its bytes as an entry. + _ => return, + } + if r.position() <= before { + return; + } + } + } + + /// A member's type, and its width when the member is a bitfield. + fn member( + &self, + index: u32, + types: &mut Types, + made: &mut Made, + depth: u32, + ) -> (TypeId, Option) { + if let Some((LF_BITFIELD, body)) = self.records.get(&index).copied() { + let mut r = Cur::new(body); + if let (Some(inner), Some(width)) = (r.u32(), r.u8()) { + let ty = self + .build(inner, types, made, depth + 1) + .unwrap_or(Types::VOID); + return (ty, Some(width)); + } + } + let ty = self + .build(index, types, made, depth + 1) + .unwrap_or(Types::VOID); + (ty, None) + } + + /// The name a function identity record carries, which is how an inlined + /// call says which function was inlined. + fn item_name(&self, index: u32) -> Option { + let (kind, body) = self.records.get(&index).copied()?; + if kind != LF_FUNC_ID && kind != LF_MFUNC_ID { + return None; + } + let mut r = Cur::new(body); + let (_scope, _ty) = (r.u32()?, r.u32()?); + r.cstr() + } + + /// The type index a function identity record points at, for the symbol + /// records that name the item stream rather than the type stream. + fn function_type(&self, index: u32) -> Option { + let (kind, body) = self.records.get(&index).copied()?; + if kind != LF_FUNC_ID && kind != LF_MFUNC_ID { + return None; + } + let mut r = Cur::new(body); + let _scope = r.u32()?; + r.u32() + } +} + +/// The tag a record defines, or nothing when it is a forward reference or has +/// no tag at all. +fn tag(kind: u16, body: &[u8]) -> Option { + let mut r = Cur::new(body); + let _count = r.u16()?; + let property = r.u16()?; + if property & FORWARD_REFERENCE != 0 { + return None; + } + match kind { + LF_STRUCTURE | LF_CLASS | LF_INTERFACE => { + let (_fields, _derived, _vshape) = (r.u32()?, r.u32()?, r.u32()?); + numeric(&mut r)?; + } + LF_UNION => { + let _fields = r.u32()?; + numeric(&mut r)?; + } + LF_ENUM => { + let (_underlying, _fields) = (r.u32()?, r.u32()?); + } + _ => return None, + } + r.cstr() +} + +/// A numeric leaf: a small value is the field itself, a large one is a tag +/// followed by the value, so nothing after one sits at a fixed offset. +fn numeric(r: &mut Cur<'_>) -> Option { + let value = r.u16()?; + if value < LF_NUMERIC { + return Some(value as i64); + } + Some(match value { + LF_CHAR => r.u8()? as i8 as i64, + LF_SHORT => r.u16()? as i16 as i64, + LF_USHORT => r.u16()? as i64, + LF_LONG => r.u32()? as i32 as i64, + LF_ULONG => r.u32()? as i64, + LF_QUADWORD | LF_UQUADWORD => r.u64()? as i64, + // A real or a complex is never a size or an offset, but it still has + // to be stepped over for the name behind it to be read. + LF_REAL32 => { + r.bytes(4)?; + 0 + } + LF_REAL64 | LF_COMPLEX32 => { + r.bytes(8)?; + 0 + } + LF_REAL80 => { + r.bytes(10)?; + 0 + } + LF_REAL128 | LF_OCTWORD | LF_UOCTWORD | LF_COMPLEX64 => { + r.bytes(16)?; + 0 + } + _ => return None, + }) +} + +/// One of the built-in type indices, which are not records at all: the low +/// byte says what it is, and the mode above it makes it a pointer to that. +fn basic(index: u32, types: &mut Types) -> Option { + if (index >> 8) & 0xf != 0 { + let inner = basic(index & 0xff, types)?; + return Some(types.add(Type::Pointer(inner))); + } + Some(match index { + T_NOTYPE | T_VOID => Types::VOID, + T_BOOL08 => types.add(Type::Bool), + T_CHAR | T_RCHAR | T_INT1 => types.integer(1, true), + T_UCHAR | T_UINT1 => types.integer(1, false), + T_SHORT | T_INT2 => types.integer(2, true), + T_WCHAR | T_USHORT | T_UINT2 => types.integer(2, false), + T_LONG | T_INT4 => types.integer(4, true), + T_ULONG | T_UINT4 => types.integer(4, false), + T_QUAD | T_INT8 => types.integer(8, true), + T_UQUAD | T_UINT8 => types.integer(8, false), + T_INT16 => types.integer(16, true), + T_UINT16 => types.integer(16, false), + T_REAL32 => types.add(Type::Float { size: 4 }), + T_REAL64 => types.add(Type::Float { size: 8 }), + T_REAL80 | T_REAL128 => types.add(Type::Float { size: 16 }), + _ => return None, + }) +} + +/// Split a symbol stream into records, which every stream that holds them uses +/// the same shape for. +fn records(data: &[u8]) -> Vec<(u16, &[u8])> { + let mut r = Cur::new(data); + let mut out = Vec::new(); + while !r.is_empty() { + let Some(length) = r.u16() else { break }; + // As in the type streams: a record too short to hold its own kind + // cannot be stepped over, and every other one costs four bytes, which + // bounds how many the stream can hold. + if length < 2 { + break; + } + let Some(body) = r.bytes(length as usize) else { + break; + }; + out.push((u16::from_le_bytes([body[0], body[1]]), &body[2..])); + } + out +} + +/// What turning symbol records into entries needs to have at hand. +struct Symbols<'a> { + tpi: &'a TypeTable<'a>, + ipi: &'a TypeTable<'a>, + bases: &'a [Addr], + /// Which machine, so a register number can be translated. + machine: u16, + types: &'a mut Types, + made: Made, + functions: Vec, + variables: Vec, + /// Frame descriptions, keyed by the procedure they belong to. + frames: BTreeMap, +} + +/// What one scope held, gathered before it is attached to anything. +/// +/// A scope's contents cannot be written straight into the function it belongs +/// to, because an inlined call's locals belong to the frame rather than to the +/// function, and the frame is itself one of the things being collected. +#[derive(Default)] +struct Scope { + locals: Vec, + inlines: Vec, + /// Separated code the scope declared, which is body outside the main run. + ranges: Vec, +} + +impl Symbols<'_> { + /// Walk one symbol stream, whether it is a module's or the global one. + fn walk(&mut self, data: &[u8]) { + let records = records(data); + let mut n = 0; + while n < records.len() { + let (kind, body) = records[n]; + n += 1; + match kind { + S_PUB32 => self.public(body), + S_GDATA32 | S_LDATA32 => { + if let Some(v) = self.data(body) { + self.variables.push(v); + } + } + S_GPROC32 | S_LPROC32 | S_GPROC32_ID | S_LPROC32_ID => { + let mut found = self.procedure(kind, body); + // A record that could not be read still opened a scope, + // and its contents have to be stepped over rather than + // attributed to whatever procedure comes after it. + let mut sink = DebugFunction::default(); + let at = found.as_ref().map(|f| f.low_pc).unwrap_or(Addr::ZERO); + let scope = self.scope(&records, &mut n, at, 0); + let f = found.as_mut().unwrap_or(&mut sink); + f.locals = scope.locals; + f.inlines = scope.inlines; + // Ranges are only worth listing when the body is in more + // than one piece; otherwise the start and size say it. + if !scope.ranges.is_empty() { + if let Some(main) = f.size.and_then(|n| AddrRange::sized(f.low_pc, n)) { + f.ranges.push(main); + } + f.ranges.extend(scope.ranges); + f.ranges.sort_by_key(|r| r.start()); + } + if let Some(f) = found { + self.functions.push(f); + } + } + // A thunk is real code with a real name, and nothing else in + // the file says the name belongs to that address. + S_THUNK32 => { + let found = self.thunk(body); + let at = found.as_ref().map(|f| f.low_pc).unwrap_or(Addr::ZERO); + let _ = self.scope(&records, &mut n, at, 0); + if let Some(f) = found { + self.functions.push(f); + } + } + // A scope opened by something this does not model is still + // stepped over as a scope, so its end record is consumed. + _ if opens_scope(kind) => { + let _ = self.scope(&records, &mut n, Addr::ZERO, 0); + } + _ => {} + } + } + } + + /// Consume one scope's records, up to and including the record closing it. + /// + /// `base` is the procedure the scope sits inside, which is what an inline + /// site's code offsets are measured from. `depth` is how many inlined + /// calls deep the scope already is. + fn scope(&mut self, records: &[(u16, &[u8])], n: &mut usize, base: Addr, depth: u32) -> Scope { + self.scope_at(records, n, base, depth, 0) + } + + /// The same, counting how deep the recursion is. Nesting comes out of the + /// file, so a stream that opens a scope and never closes it would descend + /// as far as it has records; past the limit the opening records are read + /// as plain ones and the next end record closes the scope that is open. + fn scope_at( + &mut self, + records: &[(u16, &[u8])], + n: &mut usize, + base: Addr, + depth: u32, + nesting: u32, + ) -> Scope { + let mut out = Scope::default(); + let deep = nesting >= MAX_NESTING; + // A local is described by the record naming it and then by however + // many range records follow, so it is held until something else ends + // it. + let mut pending: Option = None; + let mut ranges: Vec = Vec::new(); + while *n < records.len() { + let (kind, body) = records[*n]; + *n += 1; + if !is_range_record(kind) { + finish_local(&mut pending, &mut ranges, &mut out.locals); + } + match kind { + S_END | S_INLINESITE_END | S_PROC_ID_END => break, + S_FRAMEPROC => { + if let Some(frame) = self.frame(body) { + self.frames.insert(base, frame); + } + } + S_LOCAL => pending = self.local(body), + k if is_range_record(k) => { + if pending.is_some() { + self.location(k, body, &mut ranges); + } + } + S_BPREL32 | S_REGREL32 => { + if let Some(local) = self.frame_local(kind, body) { + out.locals.push(local); + } + } + // A value the optimizer folded away still has a name and a + // type, and the value itself is the only place it lives. + S_CONSTANT => { + if let Some(local) = self.constant(body) { + out.locals.push(local); + } + } + S_INLINESITE | S_INLINESITE2 if !deep => { + let frame = self.inline_site(kind, body, base, depth); + let inner = self.scope_at(records, n, base, depth + 1, nesting + 1); + match frame { + Some(mut frame) => { + frame.locals = inner.locals; + out.inlines.push(frame); + // Deeper frames are kept beside this one rather + // than inside it, which is the shape the DWARF + // reader produces. + out.inlines.extend(inner.inlines); + } + None => out.locals.extend(inner.locals), + } + out.ranges.extend(inner.ranges); + } + S_SEPCODE if !deep => { + if let Some(range) = self.separated(body) { + out.ranges.push(range); + } + let inner = self.scope_at(records, n, base, depth, nesting + 1); + out.locals.extend(inner.locals); + out.inlines.extend(inner.inlines); + out.ranges.extend(inner.ranges); + } + k if opens_scope(k) && !deep => { + let inner = self.scope_at(records, n, base, depth, nesting + 1); + out.locals.extend(inner.locals); + out.inlines.extend(inner.inlines); + out.ranges.extend(inner.ranges); + } + _ => {} + } + } + finish_local(&mut pending, &mut ranges, &mut out.locals); + out + } + + /// A procedure: the name, where it starts, and how long it is. + fn procedure(&mut self, kind: u16, body: &[u8]) -> Option { + let mut r = Cur::new(body); + let _links = r.bytes(12)?; + let size = r.u32()?; + let (_debug_start, _debug_end) = (r.u32()?, r.u32()?); + let index = r.u32()?; + let offset = r.u32()?; + let section = r.u16()?; + let _flags = r.u8()?; + let name = r.cstr()?; + let low_pc = address(self.bases, section, offset)?; + // The identity records index the item stream, where a function + // identity carries the real type index. + let index = match kind { + S_GPROC32_ID | S_LPROC32_ID => self.ipi.function_type(index).unwrap_or(T_NOTYPE), + _ => index, + }; + let signature = self + .tpi + .build(index, self.types, &mut self.made, 0) + .and_then(|id| match self.types.get(id) { + Some(Type::Function(signature)) => Some(signature.clone()), + _ => None, + }) + .unwrap_or_default(); + Some(DebugFunction { + name, + low_pc, + size: Some(size as u64), + signature, + ..Default::default() + }) + } + + /// A thunk: a stub the linker generated, with its own name and extent. + fn thunk(&mut self, body: &[u8]) -> Option { + let mut r = Cur::new(body); + let _links = r.bytes(12)?; + let offset = r.u32()?; + let section = r.u16()?; + let length = r.u16()?; + let _ordinal = r.u8()?; + let name = r.cstr()?; + Some(DebugFunction { + name, + low_pc: address(self.bases, section, offset)?, + size: Some(length as u64), + ..Default::default() + }) + } + + /// Separated code: a piece of a function the linker put somewhere else. + fn separated(&mut self, body: &[u8]) -> Option { + let mut r = Cur::new(body); + let _links = r.bytes(8)?; + let length = r.u32()?; + let _flags = r.u32()?; + let offset = r.u32()?; + let _parent_offset = r.u32()?; + let section = r.u16()?; + AddrRange::sized(address(self.bases, section, offset)?, length as u64) + } + + /// The frame description a procedure carries. + fn frame(&mut self, body: &[u8]) -> Option { + let mut r = Cur::new(body); + let total_bytes = r.u32()?; + let padding_bytes = r.u32()?; + let _padding_offset = r.u32()?; + let callee_saved_bytes = r.u32()?; + let handler_offset = r.u32()?; + let handler_section = r.u16()?; + let flags = r.u32()?; + Some(Frame { + total_bytes, + padding_bytes, + callee_saved_bytes, + exception_handler: address(self.bases, handler_section, handler_offset), + local_base: self.base_register((flags >> FRAME_LOCAL_BASE_SHIFT) & 3), + param_base: self.base_register((flags >> FRAME_PARAM_BASE_SHIFT) & 3), + flags, + }) + } + + /// The two bit encoding a frame record uses for the register its offsets + /// are measured from. Which register each code means depends on the + /// machine, and zero means the frame has no base register at all. + fn base_register(&self, code: u32) -> Option { + match (self.machine, code) { + (MACHINE_AMD64, 1) => Some(7), // rsp + (MACHINE_AMD64, 2) => Some(6), // rbp + (MACHINE_AMD64, 3) => Some(13), // r13 + (MACHINE_ARM64, 1) => Some(31), // sp + (MACHINE_ARM64, 2) => Some(29), // fp + _ => None, + } + } + + /// A data symbol: a variable at a fixed address. + fn data(&mut self, body: &[u8]) -> Option { + let mut r = Cur::new(body); + let index = r.u32()?; + let offset = r.u32()?; + let section = r.u16()?; + let name = r.cstr()?; + let addr = address(self.bases, section, offset)?; + let ty = self + .tpi + .build(index, self.types, &mut self.made, 0) + .unwrap_or(Types::VOID); + Some(DebugVariable { name, ty, addr }) + } + + /// A public symbol, which is a name, an address and a flag saying which of + /// the two tables it belongs in. It carries no type at all. + fn public(&mut self, body: &[u8]) { + let mut r = Cur::new(body); + let (Some(flags), Some(offset), Some(section)) = (r.u32(), r.u32(), r.u16()) else { + return; + }; + let (Some(name), Some(addr)) = (r.cstr(), address(self.bases, section, offset)) else { + return; + }; + if flags & PUBLIC_FUNCTION != 0 { + self.functions.push(DebugFunction { + name, + low_pc: addr, + ..Default::default() + }); + } else { + self.variables.push(DebugVariable { + name, + ty: Types::VOID, + addr, + }); + } + } + + /// A local at a frame offset, which is how the older records spell one. + fn frame_local(&mut self, kind: u16, body: &[u8]) -> Option { + let mut r = Cur::new(body); + let offset = r.i32()?; + let index = r.u32()?; + let register = match kind { + S_REGREL32 => Some(r.u16()?), + _ => None, + }; + let name = r.cstr()?; + let ty = self.type_of(index); + // A frame relative record names the register itself, so it can say + // where the value is and not only how far up the frame it sits. + let location = match register.and_then(|n| dwarf_register(self.machine, n)) { + Some(n) => Some(Location::RegisterOffset(n, offset as i64)), + None if kind == S_BPREL32 => Some(Location::FrameOffset(offset as i64)), + None => None, + }; + Some(DebugLocal { + name, + ty, + frame_offset: Some(offset as i64), + location, + ..Default::default() + }) + } + + /// A constant the optimizer folded into the code it generated. + fn constant(&mut self, body: &[u8]) -> Option { + let mut r = Cur::new(body); + let index = r.u32()?; + let value = numeric(&mut r)?; + let name = r.cstr()?; + Some(DebugLocal { + name, + ty: self.type_of(index), + location: Some(Location::Constant(value)), + ..Default::default() + }) + } + + /// The record naming a local, before the records saying where it lives. + fn local(&mut self, body: &[u8]) -> Option { + let mut r = Cur::new(body); + let index = r.u32()?; + let _flags = r.u16()?; + let name = r.cstr()?; + Some(DebugLocal { + name, + ty: self.type_of(index), + ..Default::default() + }) + } + + /// One range record: where a local lives over some run of addresses. + fn location(&mut self, kind: u16, body: &[u8], out: &mut Vec) { + let mut r = Cur::new(body); + let where_ = match kind { + S_DEFRANGE_REGISTER => { + let (Some(reg), Some(_attr)) = (r.u16(), r.u16()) else { + return; + }; + match dwarf_register(self.machine, reg) { + Some(n) => Location::Register(n), + None => return, + } + } + S_DEFRANGE_FRAMEPOINTER_REL => { + let Some(offset) = r.i32() else { return }; + Location::FrameOffset(offset as i64) + } + S_DEFRANGE_REGISTER_REL => { + let (Some(reg), Some(flags), Some(offset)) = (r.u16(), r.u16(), r.i32()) else { + return; + }; + // The spilled-member bit says the record describes part of an + // aggregate rather than the whole local, which nothing here + // can express. + if flags & DEFRANGE_SPILLED_MEMBER != 0 { + return; + } + match dwarf_register(self.machine, reg) { + Some(n) => Location::RegisterOffset(n, offset as i64), + None => return, + } + } + // The full scope form has no range at all: it is where the local + // lives for as long as the scope lasts. + S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE => { + let Some(offset) = r.i32() else { return }; + out.push(LocationRange { + range: AddrRange::empty_at(Addr::ZERO), + location: Location::FrameOffset(offset as i64), + }); + return; + } + _ => return, + }; + self.ranges_of(&mut r, where_, out); + } + + /// The addresses a range record covers, with the gaps it lists punched + /// out of them. + fn ranges_of(&self, r: &mut Cur<'_>, location: Location, out: &mut Vec) { + let (Some(offset), Some(section), Some(length)) = (r.u32(), r.u16(), r.u16()) else { + return; + }; + let Some(start) = address(self.bases, section, offset) else { + return; + }; + let length = length as u64; + // Gaps are offsets from the start of the range, in order. Anything out + // of order ends the walk rather than producing a range running + // backwards. + let mut pieces = Vec::new(); + let mut at = 0u64; + while r.remaining() >= 4 { + let (Some(gap), Some(gap_len)) = (r.u16(), r.u16()) else { + break; + }; + let gap = gap as u64; + if gap < at || gap > length { + break; + } + if gap > at { + pieces.push((at, gap - at)); + } + at = (gap + gap_len as u64).min(length); + } + if at < length { + pieces.push((at, length - at)); + } + for (from, n) in pieces { + let piece = start.checked_add(from).and_then(|a| AddrRange::sized(a, n)); + if let Some(range) = piece { + out.push(LocationRange { + range, + location: location.clone(), + }); + } + } + } + + /// An inlined call: which function, and which addresses it occupies. + fn inline_site( + &mut self, + kind: u16, + body: &[u8], + base: Addr, + depth: u32, + ) -> Option { + let mut r = Cur::new(body); + let (_parent, _end) = (r.u32()?, r.u32()?); + let inlinee = r.u32()?; + if kind == S_INLINESITE2 { + let _invocations = r.u32()?; + } + let ranges = annotations(&mut r, base); + Some(InlinedFrame { + name: self.ipi.item_name(inlinee).unwrap_or_default(), + // The item stream index takes the place DWARF gives the abstract + // origin's offset: it is what joins this to the out-of-line copy. + abstract_origin: Some(inlinee as u64), + entry_pc: ranges.iter().map(|r| r.start()).min(), + ranges, + depth, + ..Default::default() + }) + } + + /// A type index as a type, remembering what was built. + fn type_of(&mut self, index: u32) -> TypeId { + self.tpi + .build(index, self.types, &mut self.made, 0) + .unwrap_or(Types::VOID) + } +} + +/// Attach the local that the records just walked described. +fn finish_local( + pending: &mut Option, + ranges: &mut Vec, + out: &mut Vec, +) { + let Some(mut local) = pending.take() else { + ranges.clear(); + return; + }; + let found = std::mem::take(ranges); + // A whole scope record has no addresses to attach, so it is the one + // location rather than one entry in a list. + if let [one] = found.as_slice() { + if one.range.is_empty() { + local.frame_offset = match one.location { + Location::FrameOffset(n) => Some(n), + _ => None, + }; + local.location = Some(one.location.clone()); + out.push(local); + return; + } + } + // The frame offset is only stated when every range agrees on one: a local + // that is in a register for part of the function does not have one. + let mut offsets = found.iter().map(|r| match r.location { + Location::FrameOffset(n) => Some(n), + _ => None, + }); + let first = offsets.next().flatten(); + local.frame_offset = first.filter(|n| offsets.all(|o| o == Some(*n))); + local.locations = found; + out.push(local); +} + +/// True for the records that say where the local in front of them lives. +fn is_range_record(kind: u16) -> bool { + (S_DEFRANGE..=S_DEFRANGE_REGISTER_REL).contains(&kind) +} + +/// True for the records that open a scope, which then runs to an end record. +fn opens_scope(kind: u16) -> bool { + matches!( + kind, + S_GPROC32 + | S_LPROC32 + | S_GPROC32_ID + | S_LPROC32_ID + | S_BLOCK32 + | S_THUNK32 + | S_SEPCODE + | S_INLINESITE + | S_INLINESITE2 + ) +} + +/// A compressed unsigned integer, as the binary annotation stream spells one: +/// the top bits of the first byte say how many bytes it occupies. +fn compressed(r: &mut Cur<'_>) -> Option { + let b0 = r.u8()? as u32; + if b0 & 0x80 == 0 { + return Some(b0); + } + if b0 & 0xc0 == 0x80 { + return Some(((b0 & 0x3f) << 8) | r.u8()? as u32); + } + if b0 & 0xe0 == 0xc0 { + let (b1, b2, b3) = (r.u8()? as u32, r.u8()? as u32, r.u8()? as u32); + return Some(((b0 & 0x1f) << 24) | (b1 << 16) | (b2 << 8) | b3); + } + // The remaining prefixes are not assigned, so the stream stops here rather + // than being read as something else. + None +} + +/// The code ranges an inline site's binary annotations describe. +/// +/// Offsets are relative to the procedure the site sits in, and start again at +/// zero for each record, including a record nested inside another site. A +/// range is emitted only when the stream said how long it is: a start with no +/// length is dropped rather than extended to somewhere it might not reach. +fn annotations(r: &mut Cur<'_>, base: Addr) -> Vec { + let mut out = Vec::new(); + let mut offset = 0u64; + let mut open: Option = None; + let emit = |start: u64, len: u64, out: &mut Vec| { + if let Some(range) = base + .checked_add(start) + .and_then(|a| AddrRange::sized(a, len)) + { + out.push(range); + } + }; + while !r.is_empty() { + let Some(op) = compressed(r) else { break }; + match op { + BA_END => break, + BA_CODE_OFFSET | BA_CHANGE_CODE_OFFSET => { + let Some(delta) = compressed(r) else { break }; + offset += delta as u64; + open = Some(offset); + } + BA_CHANGE_CODE_OFFSET_AND_LINE_OFFSET => { + let Some(packed) = compressed(r) else { break }; + // The low nibble is the code delta; the rest is a line delta, + // which nothing here records. + offset += (packed & 0xf) as u64; + open = Some(offset); + } + BA_CHANGE_CODE_LENGTH => { + let Some(len) = compressed(r) else { break }; + if let Some(start) = open.take() { + emit(start, len as u64, &mut out); + } + offset += len as u64; + } + BA_CHANGE_CODE_LENGTH_AND_CODE_OFFSET => { + let (Some(len), Some(delta)) = (compressed(r), compressed(r)) else { + break; + }; + offset += delta as u64; + emit(offset, len as u64, &mut out); + offset += len as u64; + open = None; + } + // One operand each, and none of them moves the code offset. + BA_CHANGE_FILE + | BA_CHANGE_LINE_OFFSET + | BA_CHANGE_LINE_END_DELTA + | BA_CHANGE_RANGE_KIND + | BA_CHANGE_COLUMN_START + | BA_CHANGE_COLUMN_END_DELTA + | BA_CHANGE_COLUMN_END => { + if compressed(r).is_none() { + break; + } + } + // Changing the section the offsets are measured in is something + // this does not model, so the walk stops rather than placing the + // rest of the ranges in the wrong section. + _ => break, + } + } + out +} + +/// The line rows one module's debug subsections carry. +fn lines(c13: &[u8], names: &[u8], bases: &[Addr], out: &mut Vec) { + let files = checksums(c13, names); + let mut r = Cur::new(c13); + // Every subsection costs its eight byte header, so the walk advances. + while !r.is_empty() { + let (Some(kind), Some(length)) = (r.u32(), r.u32()) else { + break; + }; + let Some(body) = r.bytes(length as usize) else { + break; + }; + if kind == DEBUG_S_LINES { + line_block(body, &files, bases, out); + } + if !r.align() { + break; + } + } +} + +/// The file names the checksum subsection lists, keyed by the offset the line +/// blocks refer to them by. +fn checksums(c13: &[u8], names: &[u8]) -> BTreeMap { + let mut out = BTreeMap::new(); + let mut r = Cur::new(c13); + while !r.is_empty() { + let (Some(kind), Some(length)) = (r.u32(), r.u32()) else { + break; + }; + let Some(body) = r.bytes(length as usize) else { + break; + }; + if kind == DEBUG_S_FILECHKSMS { + let mut e = Cur::new(body); + // An entry costs six bytes before its checksum, so the subsection + // bounds how many there are. + while !e.is_empty() { + let at = e.position() as u32; + let (Some(name), Some(size), Some(_kind)) = (e.u32(), e.u8(), e.u8()) else { + break; + }; + if e.bytes(size as usize).is_none() { + break; + } + out.insert(at, string_at(names, name)); + if !e.align() { + break; + } + } + } + if !r.align() { + break; + } + } + out +} + +/// A string from the name table the line information indexes into. +fn string_at(names: &[u8], offset: u32) -> String { + let mut r = Cur::new(names); + if r.u32() != Some(NAMES_MAGIC) { + return String::new(); + } + match names.get(NAMES_HEADER..) { + Some(buffer) => cstring(buffer, offset), + None => String::new(), + } +} + +/// One lines subsection: a run of code, then blocks of rows within it. +fn line_block(body: &[u8], files: &BTreeMap, bases: &[Addr], out: &mut Vec) { + let mut r = Cur::new(body); + let (Some(offset), Some(section), Some(flags), Some(length)) = + (r.u32(), r.u16(), r.u16(), r.u32()) + else { + return; + }; + let Some(start) = address(bases, section, offset) else { + return; + }; + let columns = flags & LINES_HAVE_COLUMNS != 0; + // Every block costs its twelve byte header, so the walk advances. + while !r.is_empty() { + let (Some(file), Some(count), Some(_size)) = (r.u32(), r.u32(), r.u32()) else { + return; + }; + // A row is eight bytes, twelve where there are columns, so the bytes + // left in the subsection bound the count. + let each = if columns { 12 } else { 8 }; + if count as usize > r.remaining() / each { + return; + } + let name = files.get(&file).cloned().unwrap_or_default(); + let at = r.position(); + for n in 0..count as usize { + let (Some(delta), Some(bits)) = (r.u32(), r.u32()) else { + return; + }; + // The columns for a block follow all of its rows rather than + // travelling with them. + let column = match columns { + true => body + .get(at + count as usize * 8 + n * 4..) + .and_then(|b| b.get(..2)) + .map(|b| u16::from_le_bytes([b[0], b[1]]) as u32) + .unwrap_or(0), + false => 0, + }; + out.push(LineRow { + addr: start.checked_add(delta as u64).unwrap_or(start), + file: name.clone(), + line: bits & 0x00ff_ffff, + column, + end: false, + statement: bits & 0x8000_0000 != 0, + }); + } + if columns && !r.seek((at + count as usize * 12).min(r.len())) { + return; + } + } + // Where the run ends, so a lookup past the last row does not extend it. + out.push(LineRow { + addr: start.checked_add(length as u64).unwrap_or(start), + file: String::new(), + line: 0, + column: 0, + end: true, + statement: false, + }); +} + +/// Everything one program database says. +/// +/// [`Pdb::debug`] is the same shape the DWARF reader produces, so nothing +/// above this layer has to know which format the information came from. The +/// other fields are what a database carries and DWARF does not. +#[derive(Debug, Clone)] +pub struct Pdb { + /// What the database says it is, for matching against an image. + pub identity: Identity, + /// What the compiler knew, in the shape the DWARF reader also produces. + pub debug: DebugInfo, + /// The object files that went into the link, in the order the DBI stream + /// lists them, which is the order a contribution's index refers to. + pub modules: Vec, + /// Which module contributed which addresses. + pub contributions: Vec, + /// Stack frame descriptions, by the procedure they belong to. + pub frames: BTreeMap, + /// The machine the DBI stream names, as a PE machine number. + pub machine: u16, + /// What was noticed but not acted on, for the report. + pub warnings: Vec, +} + +impl Pdb { + /// The function entries the database proves. + /// + /// A symbol record naming a procedure is a declaration by the compiler + /// that a function starts there, which is as strong as evidence gets; a + /// public symbol flagged as code is the same claim with no size attached. + pub fn hints(&self) -> Vec { + let mut out = Vec::with_capacity(self.debug.functions.len()); + for (addr, f) in &self.debug.functions { + if f.name.is_empty() { + continue; + } + out.push(crate::FunctionHint { + addr: *addr, + size: f.size.filter(|n| *n > 0), + name: Some(f.name.clone()), + provenance: Provenance::new(Evidence::Pdb), + }); + } + out + } +} + +/// Read a program database, or `None` when the bytes are not one. +/// +/// The image's section headers turn the section and offset pairs the records +/// carry into addresses. Without them the linker's own copy is used instead, +/// and the addresses come out relative to the image base. +pub fn parse(data: &[u8], sections: &[crate::Section]) -> Option { + read(data, sections).map(|p| p.debug) +} + +/// Read a program database whole, including what does not fit in +/// [`DebugInfo`]. +pub fn read(data: &[u8], sections: &[crate::Section]) -> Option { + let msf = Msf::open(data)?; + let (identity, named) = msf + .stream(STREAM_INFO) + .and_then(|s| info(&s)) + .unwrap_or_default(); + let dbi = msf + .stream(STREAM_DBI) + .and_then(|s| dbi(&s)) + .unwrap_or_default(); + let bases = section_bases(sections, &msf, &dbi); + let tpi_stream = msf.stream(STREAM_TPI).unwrap_or_default(); + let ipi_stream = msf.stream(STREAM_IPI).unwrap_or_default(); + let tpi = TypeTable::scan(&tpi_stream); + let ipi = TypeTable::scan(&ipi_stream); + let names = named + .get(NAMES_STREAM) + .and_then(|n| msf.stream(*n as usize)) + .unwrap_or_default(); + + let mut warnings = Vec::new(); + // A type server holds the records for every module that used it, and it is + // a separate file this was not given. Saying so is the difference between + // "no types" and "the types are somewhere else". + if tpi.type_server { + warnings.push("types live in a type server this was not given".into()); + } + + let mut types = Types::new(); + let mut symbols = Symbols { + tpi: &tpi, + ipi: &ipi, + bases: &bases, + machine: dbi.machine, + types: &mut types, + made: Made::new(), + functions: Vec::new(), + variables: Vec::new(), + frames: BTreeMap::new(), + }; + let mut rows = Vec::new(); + let mut modules = Vec::with_capacity(dbi.modules.len()); + for module in &dbi.modules { + let mut info = ModuleInfo { + name: module.name.clone(), + object: module.object.clone(), + producer: None, + }; + let Some(stream) = msf.stream(module.stream as usize) else { + modules.push(info); + continue; + }; + // A module stream is a signature, then the symbols, then the two + // generations of line information, each as long as the header said. + // The signature is inside the symbol substream, and a module with no + // symbols has no substream rather than an empty one, so the line + // information starts at the front. + let symbols_end = module.symbol_bytes as usize; + let c11_end = symbols_end + module.c11_bytes as usize; + let c13_end = c11_end + module.c13_bytes as usize; + if symbols_end > 4 { + let body = stream.get(4..symbols_end).unwrap_or_default(); + info.producer = producer(body); + symbols.walk(body); + } + if let Some(c13) = stream.get(c11_end..c13_end) { + lines(c13, &names, &bases, &mut rows); + } + modules.push(info); + } + // The global records last: a public says less than a procedure record + // about the same address, so it must not displace one. + if let Some(stream) = msf.stream(dbi.symbols as usize) { + symbols.walk(&stream); + } + let (mut functions, variables, frames) = (symbols.functions, symbols.variables, symbols.frames); + + rows.sort_by_key(|r| r.addr); + // A local addressed from the register the frame record names as the local + // base is at a frame offset, which is the same fact the older records + // state directly. Saying it both ways costs nothing and means a caller + // that only reads `frame_offset` sees the stack slots either way. + for f in &mut functions { + let Some(base) = frames.get(&f.low_pc).and_then(|frame| frame.local_base) else { + continue; + }; + for local in &mut f.locals { + if local.frame_offset.is_some() { + continue; + } + let mut offsets = local.locations.iter().map(|r| match r.location { + Location::RegisterOffset(reg, n) if reg == base => Some(n), + _ => None, + }); + let first = offsets.next().flatten(); + local.frame_offset = first.filter(|n| offsets.all(|o| o == Some(*n))); + } + } + // The declaration site is in the line table rather than on the record, so + // it comes from the row the function starts at. + for f in &mut functions { + let n = rows.partition_point(|r| r.addr < f.low_pc); + if let Some(row) = rows.get(n).filter(|r| r.addr == f.low_pc && !r.end) { + f.decl_file = Some(row.file.clone()); + f.decl_line = Some(row.line as u64); + } + // An inlined call's own lines are in its annotation stream, but the + // line table proper attributes inlined code to where the outermost + // call was written, which is exactly the call site of a frame inlined + // directly into this function. It says nothing about where a nested + // frame was called from, so those are left unanswered. + for frame in &mut f.inlines { + let Some(at) = frame.entry_pc.filter(|_| frame.depth == 0) else { + continue; + }; + let n = rows.partition_point(|r| r.addr <= at); + if let Some(row) = n + .checked_sub(1) + .and_then(|n| rows.get(n)) + .filter(|r| !r.end) + { + frame.call_file = Some(row.file.clone()); + frame.call_line = Some(row.line); + frame.call_column = Some(row.column); + } + } + } + + let contributions = dbi + .contributions + .iter() + .filter_map(|c| { + let start = address(&bases, c.section, c.offset)?; + Some(Contribution { + range: AddrRange::sized(start, c.size as u64)?, + module: c.module, + characteristics: c.characteristics, + }) + }) + .collect(); + + let mut debug = DebugInfo { + types, + units: dbi.modules.len(), + lines: rows, + ..Default::default() + }; + for f in functions { + debug.functions.entry(f.low_pc).or_insert(f); + } + for v in variables { + debug.variables.entry(v.addr).or_insert(v); + } + Some(Pdb { + identity, + debug, + modules, + contributions, + frames, + machine: dbi.machine, + warnings, + }) +} + +/// The compiler string a module's first records carry, when it has one. +fn producer(symbols: &[u8]) -> Option { + for (kind, body) in records(symbols) { + if kind != S_COMPILE3 { + continue; + } + let mut r = Cur::new(body); + // Flags and language, the machine, then four version numbers as pairs + // of words, and the string behind them. + let _flags = r.u32()?; + let _machine = r.u16()?; + r.bytes(16)?; + return r.cstr(); + } + None +} + +/// The signature every version of the container since 7.00 starts with. +const MAGIC: &[u8] = b"Microsoft C/C++ MSF 7.00\r\n\x1aDS\0\0\0"; + +// Fixed stream numbers. +const STREAM_INFO: usize = 1; +const STREAM_TPI: usize = 2; +const STREAM_DBI: usize = 3; +const STREAM_IPI: usize = 4; +/// The signature a debug directory's CodeView record carries when it names a +/// program database by GUID. +const CODEVIEW_RSDS: &[u8] = b"RSDS"; +/// The name the line information's string table is filed under. +const NAMES_STREAM: &str = "/names"; +const NAMES_MAGIC: u32 = 0xeffe_effe; +const NAMES_HEADER: usize = 12; + +/// The first version that carries a GUID rather than only a timestamp. +const VERSION_VC70: u32 = 20000404; +/// Slot of the section headers in the DBI stream's optional header. +const DBG_SECTION_HEADERS: usize = 5; +/// Fixed bytes of a module entry, before its two names. +const MODULE_ENTRY: usize = 64; +/// Bytes of one image section header. +const SECTION_HEADER: usize = 40; +/// How deep one scope may open another. Blocks nest a handful deep in real +/// code and inlined calls a little more; this is only here so that a file +/// claiming a million nested scopes cannot exhaust the stack. +const MAX_NESTING: u32 = 64; +/// Smallest type stream header, which is where the records begin. +const TPI_HEADER: usize = 56; +/// Below this a type index is one of the built-in types, not a record. +const FIRST_RECORD: u32 = 0x1000; +/// The property bit that says a record only names a type. +const FORWARD_REFERENCE: u16 = 0x0080; +/// The public symbol flag that says the address is code. +const PUBLIC_FUNCTION: u32 = 0x0002; +/// Bytes of one section contribution entry, before the later version's extra +/// field. +const CONTRIBUTION_ENTRY: usize = 28; +/// The two versions of the section contribution substream, which differ only +/// in the field the later one appends to each entry. +const SECTION_CONTRIB_V1: u32 = 0xeffe_0000 + 19970605; +const SECTION_CONTRIB_V2: u32 = 0xeffe_0000 + 20140516; +/// Where a frame record's flag word encodes the register locals and parameters +/// are addressed from. +const FRAME_LOCAL_BASE_SHIFT: u32 = 14; +const FRAME_PARAM_BASE_SHIFT: u32 = 16; +/// In a register relative range record, the bit saying the record describes a +/// member spilled out of an aggregate and the field giving that member's +/// offset. Either one means the record is about part of the local rather than +/// all of it, which nothing here can express. +const DEFRANGE_SPILLED_MEMBER: u16 = 0xfff1; + +// PE machine numbers, which is how the DBI stream names the target. +const MACHINE_AMD64: u16 = 0x8664; +const MACHINE_ARM64: u16 = 0xaa64; + +// CodeView register numbers. The byte, word and doubleword names of the first +// eight x86-64 registers are numbered in the order the 8086 numbered them, and +// the sixty four bit names are a separate block added later. +const CV_AMD64_AL: u16 = 1; +const CV_AMD64_BH: u16 = 8; +const CV_AMD64_AX: u16 = 9; +const CV_AMD64_DI: u16 = 16; +const CV_AMD64_EAX: u16 = 17; +const CV_AMD64_EDI: u16 = 24; +const CV_AMD64_RAX: u16 = 328; +const CV_AMD64_R15: u16 = 343; +const CV_ARM64_W0: u16 = 10; +const CV_ARM64_W30: u16 = 40; +const CV_ARM64_X0: u16 = 50; +const CV_ARM64_X28: u16 = 78; +const CV_ARM64_FP: u16 = 79; +const CV_ARM64_LR: u16 = 80; +const CV_ARM64_SP: u16 = 81; + +// Binary annotation opcodes, which describe an inlined call's extent. +const BA_END: u32 = 0; +const BA_CODE_OFFSET: u32 = 1; +const BA_CHANGE_CODE_OFFSET: u32 = 3; +const BA_CHANGE_CODE_LENGTH: u32 = 4; +const BA_CHANGE_FILE: u32 = 5; +const BA_CHANGE_LINE_OFFSET: u32 = 6; +const BA_CHANGE_LINE_END_DELTA: u32 = 7; +const BA_CHANGE_RANGE_KIND: u32 = 8; +const BA_CHANGE_COLUMN_START: u32 = 9; +const BA_CHANGE_COLUMN_END_DELTA: u32 = 10; +const BA_CHANGE_CODE_OFFSET_AND_LINE_OFFSET: u32 = 11; +const BA_CHANGE_CODE_LENGTH_AND_CODE_OFFSET: u32 = 12; +const BA_CHANGE_COLUMN_END: u32 = 13; +/// The lines subsection flag that says columns follow the rows. +const LINES_HAVE_COLUMNS: u16 = 0x0001; + +// Built-in type indices. +const T_NOTYPE: u32 = 0x0000; +const T_VOID: u32 = 0x0003; +const T_CHAR: u32 = 0x0010; +const T_SHORT: u32 = 0x0011; +const T_LONG: u32 = 0x0012; +const T_QUAD: u32 = 0x0013; +const T_UCHAR: u32 = 0x0020; +const T_USHORT: u32 = 0x0021; +const T_ULONG: u32 = 0x0022; +const T_UQUAD: u32 = 0x0023; +const T_BOOL08: u32 = 0x0030; +const T_REAL32: u32 = 0x0040; +const T_REAL64: u32 = 0x0041; +const T_REAL80: u32 = 0x0042; +const T_REAL128: u32 = 0x0043; +const T_RCHAR: u32 = 0x0070; +const T_WCHAR: u32 = 0x0071; +const T_INT2: u32 = 0x0072; +const T_UINT2: u32 = 0x0073; +const T_INT4: u32 = 0x0074; +const T_UINT4: u32 = 0x0075; +const T_INT8: u32 = 0x0076; +const T_UINT8: u32 = 0x0077; +const T_INT16: u32 = 0x0078; +const T_UINT16: u32 = 0x0079; +const T_INT1: u32 = 0x0068; +const T_UINT1: u32 = 0x0069; + +// Type record leaves. +const LF_MODIFIER: u16 = 0x1001; +const LF_POINTER: u16 = 0x1002; +const LF_PROCEDURE: u16 = 0x1008; +const LF_MFUNCTION: u16 = 0x1009; +const LF_ARGLIST: u16 = 0x1201; +const LF_FIELDLIST: u16 = 0x1203; +const LF_BITFIELD: u16 = 0x1205; +const LF_BCLASS: u16 = 0x1400; +const LF_VBCLASS: u16 = 0x1401; +const LF_IVBCLASS: u16 = 0x1402; +const LF_INDEX: u16 = 0x1404; +const LF_VFUNCTAB: u16 = 0x1409; +const LF_ENUMERATE: u16 = 0x1502; +const LF_ARRAY: u16 = 0x1503; +const LF_CLASS: u16 = 0x1504; +const LF_STRUCTURE: u16 = 0x1505; +const LF_UNION: u16 = 0x1506; +const LF_ENUM: u16 = 0x1507; +const LF_MEMBER: u16 = 0x150d; +const LF_STMEMBER: u16 = 0x150e; +const LF_METHOD: u16 = 0x150f; +const LF_NESTTYPE: u16 = 0x1510; +const LF_ONEMETHOD: u16 = 0x1511; +const LF_INTERFACE: u16 = 0x1519; +const LF_TYPESERVER2: u16 = 0x1515; +const LF_FUNC_ID: u16 = 0x1601; +const LF_MFUNC_ID: u16 = 0x1602; + +// Numeric leaves, and the value below which a field is its own value. +const LF_NUMERIC: u16 = 0x8000; +const LF_CHAR: u16 = 0x8000; +const LF_SHORT: u16 = 0x8001; +const LF_USHORT: u16 = 0x8002; +const LF_LONG: u16 = 0x8003; +const LF_ULONG: u16 = 0x8004; +const LF_REAL32: u16 = 0x8005; +const LF_REAL64: u16 = 0x8006; +const LF_REAL80: u16 = 0x8007; +const LF_REAL128: u16 = 0x8008; +const LF_QUADWORD: u16 = 0x8009; +const LF_UQUADWORD: u16 = 0x800a; +const LF_COMPLEX32: u16 = 0x800c; +const LF_COMPLEX64: u16 = 0x800d; +const LF_OCTWORD: u16 = 0x8017; +const LF_UOCTWORD: u16 = 0x8018; +/// Padding leaves, whose low nibble is how far to step. +const LF_PAD: u8 = 0xf0; + +// Symbol records. +const S_END: u16 = 0x0006; +const S_FRAMEPROC: u16 = 0x1012; +const S_CONSTANT: u16 = 0x1107; +const S_COMPILE3: u16 = 0x113c; +const S_LOCAL: u16 = 0x113e; +/// The range records, which are contiguous: everything from here to +/// `S_DEFRANGE_REGISTER_REL` says where the local in front of it lives. +const S_DEFRANGE: u16 = 0x113f; +const S_DEFRANGE_REGISTER: u16 = 0x1141; +const S_DEFRANGE_FRAMEPOINTER_REL: u16 = 0x1142; +const S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE: u16 = 0x1144; +const S_DEFRANGE_REGISTER_REL: u16 = 0x1145; +const S_INLINESITE2: u16 = 0x115d; +const S_THUNK32: u16 = 0x1102; +const S_BLOCK32: u16 = 0x1103; +const S_LDATA32: u16 = 0x110c; +const S_GDATA32: u16 = 0x110d; +const S_PUB32: u16 = 0x110e; +const S_LPROC32: u16 = 0x110f; +const S_GPROC32: u16 = 0x1110; +const S_REGREL32: u16 = 0x1111; +const S_BPREL32: u16 = 0x110b; +const S_SEPCODE: u16 = 0x1132; +const S_INLINESITE: u16 = 0x114d; +const S_INLINESITE_END: u16 = 0x114e; +const S_PROC_ID_END: u16 = 0x114f; +const S_LPROC32_ID: u16 = 0x1146; +const S_GPROC32_ID: u16 = 0x1147; + +// Module debug subsections. +const DEBUG_S_LINES: u32 = 0xf2; +const DEBUG_S_FILECHKSMS: u32 = 0xf4; diff --git a/crates/e5r-format/src/pe.rs b/crates/e5r-format/src/pe.rs new file mode 100644 index 0000000..45b9ae2 --- /dev/null +++ b/crates/e5r-format/src/pe.rs @@ -0,0 +1,1016 @@ +//! PE and COFF. +//! +//! Two formats in one file. A linked image starts with a DOS stub whose +//! `e_lfanew` points at the PE signature; a COFF object starts with the same +//! file header the PE carries, and nothing else. They share the section table +//! and the symbol table, so they share most of this module. +//! +//! The exception directory is why PE is worth reading carefully: on x86-64 and +//! ARM64 every non-leaf function has a `RUNTIME_FUNCTION` entry giving its +//! exact start and end. It is the best function-boundary evidence any format +//! offers and it survives stripping, which is the same reason `.eh_frame` +//! matters on ELF. That directory, the unwind data behind it and the SEH scope +//! tables live in [`crate::pdata`]; the TLS callback array, the base relocation +//! table and the load config live in [`crate::windirs`]. +//! +//! One gap is deliberate and visible in the output. The control-flow-guard +//! function table and the SafeSEH handler table are both linker-built lists of +//! real function entries, but `Evidence` has no variant that describes them, +//! and every other variant would either overstate or understate the claim. They +//! are returned in [`WindowsInfo::load_config`] and counted in the metadata +//! rather than emitted as hints. Adding `Evidence::GuardTable` with +//! `Strength::Proven` to `e5r-core` is what would close it. + +use std::collections::BTreeMap; + +use e5r_core::{ + Addr, AddrRange, Arch, Bits, Caps, Endian, Error, Evidence, MemoryMap, Perms, Provenance, + Reader, Result, Segment, +}; + +use crate::{ + Binding, Export, Format, FunctionHint, Import, LoadOptions, Object, Section, Symbol, + SymbolKind, pdata, windirs, +}; + +const DOS_MAGIC: [u8; 2] = *b"MZ"; +const PE_MAGIC: [u8; 4] = [b'P', b'E', 0, 0]; + +// Machine numbers. +const IMAGE_FILE_MACHINE_I386: u16 = 0x014c; +const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664; +const IMAGE_FILE_MACHINE_ARM: u16 = 0x01c0; +const IMAGE_FILE_MACHINE_ARMNT: u16 = 0x01c4; +const IMAGE_FILE_MACHINE_ARM64: u16 = 0xaa64; + +// Section characteristics. +const SCN_CNT_CODE: u32 = 0x0000_0020; +const SCN_CNT_UNINITIALIZED: u32 = 0x0000_0080; +const SCN_MEM_EXECUTE: u32 = 0x2000_0000; +const SCN_MEM_READ: u32 = 0x4000_0000; +const SCN_MEM_WRITE: u32 = 0x8000_0000; + +// Data directory indices. +const DIR_EXPORT: usize = 0; +const DIR_IMPORT: usize = 1; +const DIR_EXCEPTION: usize = 3; +const DIR_BASERELOC: usize = 5; +const DIR_DEBUG: usize = 6; +const DIR_TLS: usize = 9; +const DIR_LOAD_CONFIG: usize = 10; +const DIR_DELAY_IMPORT: usize = 13; + +/// One section header, before names are resolved. +struct SecHdr { + name: String, + virtual_size: u32, + virtual_address: u32, + raw_size: u32, + raw_offset: u32, + characteristics: u32, +} + +/// A data directory entry: an address and a size, both relative to the image +/// base. +#[derive(Clone, Copy, Default)] +struct Dir { + rva: u32, + size: u32, +} + +/// The part of a loaded image a directory walk needs. +/// +/// Copied out of the section table rather than borrowed from the [`Object`], +/// because every walk both reads the sections and appends warnings to the +/// object they came from. +#[derive(Debug, Clone, Copy)] +struct Span { + range: AddrRange, + file_offset: u64, + file_size: u64, + exec: bool, +} + +/// A mapped PE image, and the three translations every data directory needs: +/// RVA to file offset, RVA to address, and the file's own absolute addresses to +/// addresses under whatever base the image was actually loaded at. +#[derive(Clone)] +pub struct Image<'a> { + data: Reader<'a>, + spans: Vec, + base: u64, + image_base: u64, + wide: bool, + arch: Arch, +} + +impl<'a> Image<'a> { + /// A view over an already-loaded object. + /// + /// `data` is the file the object came from. Exact for an image loaded where + /// it asked to be, which is every case the loader does not rebase; a + /// rebased image needs [`load_windows`], which keeps both bases. + pub fn of(data: &'a [u8], obj: &Object) -> Image<'a> { + let base = obj.image_base.get(); + Image::new( + Reader::new(data, Endian::Little), + &obj.sections, + base, + base, + obj.bits == Bits::Bits64, + obj.arch.clone(), + ) + } + + fn new( + data: Reader<'a>, + sections: &[Section], + base: u64, + image_base: u64, + wide: bool, + arch: Arch, + ) -> Image<'a> { + Image { + data, + spans: sections + .iter() + .filter(|s| !s.range.is_empty()) + .map(|s| Span { + range: s.range, + file_offset: s.file_offset, + file_size: s.file_size, + exec: s.exec, + }) + .collect(), + base, + image_base, + wide, + arch, + } + } + + /// The file, as a bounds-checked cursor. + pub fn reader(&self) -> Reader<'a> { + self.data + } + + /// Bytes in the file. + pub fn len(&self) -> u64 { + self.data.len() as u64 + } + + /// True when the file is empty. + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// True for PE32+, which is what decides every pointer width below. + pub fn wide(&self) -> bool { + self.wide + } + + /// The machine the image targets. + pub fn arch(&self) -> &Arch { + &self.arch + } + + /// Where an RVA lands once the image is loaded. + pub fn addr_of(&self, rva: u32) -> Addr { + Addr(self.base.wrapping_add(rva as u64)) + } + + /// An absolute address the file stores, moved to wherever the image was + /// actually loaded. PE writes these relative to the base it declares, so an + /// image loaded somewhere else needs the difference applied. `None` for + /// zero, which every one of these fields uses to mean absent, and for a + /// value below the declared base, which cannot be inside the image. + pub fn addr_of_va(&self, va: u64) -> Option { + if va == 0 || va < self.image_base { + return None; + } + Some(Addr(self.base.wrapping_add(va - self.image_base))) + } + + /// File offset holding the bytes an RVA names, when a section maps it and + /// those bytes are actually in the file. + pub fn offset_of(&self, rva: u32) -> Option { + self.offset_of_addr(self.addr_of(rva)) + } + + /// The same translation from an address rather than an RVA. + pub fn offset_of_addr(&self, addr: Addr) -> Option { + let s = self.span_at(addr)?; + let delta = addr.get() - s.range.start().get(); + // A `.bss`-like section is mapped but has no bytes behind it. + if delta >= s.file_size { + return None; + } + let off = s.file_offset.checked_add(delta)?; + (off < self.len()).then_some(off) + } + + /// Bytes between a file offset and the end of the section's raw data, which + /// is the bound a table with no count has to stop at. + pub fn section_bytes_after(&self, offset: u64) -> Option { + let s = self.spans.iter().find(|s| { + offset >= s.file_offset && offset < s.file_offset.saturating_add(s.file_size) + })?; + Some((s.file_offset + s.file_size).saturating_sub(offset)) + } + + /// True when the address is inside a section the file marks executable. + pub fn is_code(&self, addr: Addr) -> bool { + self.span_at(addr).is_some_and(|s| s.exec) + } + + /// True when any section maps the address at all. + pub fn is_mapped(&self, addr: Addr) -> bool { + self.span_at(addr).is_some() + } + + fn span_at(&self, addr: Addr) -> Option<&Span> { + self.spans.iter().find(|s| s.range.contains(addr)) + } +} + +/// What the Windows-specific data directories said. +/// +/// Everything here is also reflected in the [`Object`]: starts became function +/// hints, problems became warnings, counts became metadata. This is the full +/// structure, for a caller that wants the frame layout or the relocation map +/// rather than the summary. +#[derive(Debug, Clone, Default)] +pub struct WindowsInfo { + /// The TLS directory and its callback array. + pub tls: Option, + /// The exception directory, the unwind records and the scope tables. + pub exceptions: pdata::Exceptions, + /// Every base relocation, which is every absolute address in the image. + pub relocations: Vec, + /// The load config, including the guard tables. + pub load_config: Option, +} + +/// True when the buffer looks like a PE image or a COFF object. +pub fn is_pe(data: &[u8]) -> bool { + if data.len() >= 2 && data[..2] == DOS_MAGIC { + return true; + } + // A COFF object begins with a machine number we recognize followed by a + // plausible section count. Nothing else identifies one. + if data.len() >= 20 { + let machine = u16::from_le_bytes([data[0], data[1]]); + let sections = u16::from_le_bytes([data[2], data[3]]); + return matches!( + machine, + IMAGE_FILE_MACHINE_I386 + | IMAGE_FILE_MACHINE_AMD64 + | IMAGE_FILE_MACHINE_ARM + | IMAGE_FILE_MACHINE_ARMNT + | IMAGE_FILE_MACHINE_ARM64 + ) && sections > 0 + && sections < 4096; + } + false +} + +/// Parse a PE image or a COFF object. +pub fn load(data: &[u8], opts: &LoadOptions) -> Result { + load_windows(data, opts).map(|(obj, _)| obj) +} + +/// Parse a PE image and keep the Windows-specific directories in full. +/// +/// [`load`] is this with the second half dropped. A caller that wants the frame +/// layout of a function, the address of every relocation, or the guard tables +/// calls this instead; nothing else differs, and neither does the [`Object`]. +pub fn load_windows(data: &[u8], opts: &LoadOptions) -> Result<(Object, WindowsInfo)> { + if !is_pe(data) { + return Err(Error::NotRecognized { + expected: "PE or COFF", + }); + } + let caps = &opts.caps; + let mut warnings = Vec::new(); + let r = Reader::new(data, Endian::Little); + + // A linked image has a DOS stub; an object file starts at the COFF header. + let coff_at = if data[..2] == DOS_MAGIC { + let mut d = r; + d.seek("e_lfanew", 0x3c)?; + let at = d.u32("e_lfanew")? as u64; + let sig = r.bytes_at("PE signature", at, 4)?; + if sig != PE_MAGIC { + return Err(Error::BadField { + field: "PE signature", + value: u32::from_le_bytes([sig[0], sig[1], sig[2], sig[3]]) as u64, + reason: "is not \"PE\\0\\0\", so e_lfanew does not point at a PE header", + }); + } + at + 4 + } else { + 0 + }; + let is_image = coff_at != 0; + + let mut h = r; + h.seek("COFF header", coff_at)?; + let machine = h.u16("Machine")?; + let n_sections = h.u16("NumberOfSections")?; + let _timestamp = h.u32("TimeDateStamp")?; + let sym_table = h.u32("PointerToSymbolTable")?; + let n_symbols = h.u32("NumberOfSymbols")?; + let opt_size = h.u16("SizeOfOptionalHeader")?; + let characteristics = h.u16("Characteristics")?; + + caps.check("sections", n_sections as u64, caps.sections)?; + + // The optional header, which a linked image always has. + let mut image_base = 0u64; + let mut entry_rva = 0u32; + let mut wide = machine == IMAGE_FILE_MACHINE_AMD64 || machine == IMAGE_FILE_MACHINE_ARM64; + let mut dirs = [Dir::default(); 16]; + let mut subsystem = 0u16; + let mut dll_characteristics = 0u16; + + if opt_size > 0 { + let opt_at = coff_at + 20; + let mut o = r; + o.seek("optional header", opt_at)?; + let magic = o.u16("Magic")?; + wide = match magic { + 0x10b => false, + 0x20b => true, + other => { + return Err(Error::BadField { + field: "optional header Magic", + value: other as u64, + reason: "is neither PE32 (0x10b) nor PE32+ (0x20b)", + }); + } + }; + // Two linker version bytes and three sizes sit between Magic and the + // entry point. + o.skip("linker version and sizes", 14)?; + entry_rva = o.u32("AddressOfEntryPoint")?; + o.u32("BaseOfCode")?; + if !wide { + o.u32("BaseOfData")?; + } + image_base = o.uword("ImageBase", wide)?; + // Alignments, six version fields, Win32VersionValue, SizeOfImage, + // SizeOfHeaders and CheckSum sit between ImageBase and Subsystem. + o.skip("alignments, versions and sizes", 36)?; + subsystem = o.u16("Subsystem")?; + dll_characteristics = o.u16("DllCharacteristics")?; + // Four stack and heap sizes, then LoaderFlags and the directory count. + o.skip( + "stack and heap reserve and commit", + if wide { 32 } else { 16 }, + )?; + o.u32("LoaderFlags")?; + let n_dirs = o.u32("NumberOfRvaAndSizes")?.min(16); + for d in dirs.iter_mut().take(n_dirs as usize) { + d.rva = o.u32("directory RVA")?; + d.size = o.u32("directory size")?; + } + } + + let bits = if wide { Bits::Bits64 } else { Bits::Bits32 }; + let arch = machine_to_arch(machine); + + // Section headers follow the optional header. + let sec_at = coff_at + 20 + opt_size as u64; + let mut shdrs = Vec::with_capacity(n_sections as usize); + for i in 0..n_sections as u64 { + let mut s = match r.slice_at("section header", sec_at + i * 40, 40) { + Ok(s) => s, + Err(e) => { + warnings.push(format!("section header {i}: {e}")); + break; + } + }; + let raw_name = s.bytes("section name", 8)?; + let name = section_name(raw_name, &r, sym_table, n_symbols, caps); + shdrs.push(SecHdr { + name, + virtual_size: s.u32("VirtualSize")?, + virtual_address: s.u32("VirtualAddress")?, + raw_size: s.u32("SizeOfRawData")?, + raw_offset: s.u32("PointerToRawData")?, + characteristics: { + s.skip("relocation and line number fields", 12)?; + s.u32("Characteristics")? + }, + }); + } + + // An object file has no image base; lay its sections out so it can be + // analyzed, the way a relocatable ELF is laid out. + let base = opts + .base + .map(|b| b.get()) + .unwrap_or(if is_image { image_base } else { 0x14_0000 }); + + let mut memory = MemoryMap::new(); + let mut sections = Vec::with_capacity(shdrs.len()); + let mut next_free = base; + for sh in &shdrs { + let va = if is_image { + base.wrapping_add(sh.virtual_address as u64) + } else { + // Objects put every section at RVA zero. + let at = next_free.next_multiple_of(16); + next_free = at + sh.virtual_size.max(sh.raw_size) as u64; + at + }; + let mem_size = if sh.virtual_size == 0 { + sh.raw_size as u64 + } else { + sh.virtual_size as u64 + }; + let Some(range) = AddrRange::sized(Addr(va), mem_size) else { + warnings.push(format!( + "section {} does not fit the address space", + sh.name + )); + continue; + }; + let perms = Perms::new( + sh.characteristics & SCN_MEM_READ != 0 || !is_image, + sh.characteristics & SCN_MEM_WRITE != 0, + sh.characteristics & (SCN_MEM_EXECUTE | SCN_CNT_CODE) != 0, + ); + let file_size = if sh.characteristics & SCN_CNT_UNINITIALIZED != 0 { + 0 + } else { + (sh.raw_size as u64).min(mem_size) + }; + let body = match r.bytes_at("section body", sh.raw_offset as u64, file_size) { + Ok(b) => b.to_vec(), + Err(e) => { + warnings.push(format!("section {}: {e}", sh.name)); + Vec::new() + } + }; + if !range.is_empty() { + memory.add(Segment::new( + range, + perms, + sh.name.clone(), + sh.raw_offset as u64, + body, + )?); + } + sections.push(Section { + name: sh.name.clone(), + range, + file_offset: sh.raw_offset as u64, + file_size, + exec: perms.exec, + write: perms.write, + kind: sh.characteristics, + }); + } + + let mut obj = Object { + debug: None, + format: Format::Pe, + arch, + endian: Endian::Little, + bits, + entry: (is_image && entry_rva != 0).then(|| Addr(base.wrapping_add(entry_rva as u64))), + image_base: Addr(base), + // A DLL and a PIE-equivalent executable both relocate. + pic: dll_characteristics & 0x0040 != 0 || characteristics & 0x2000 != 0, + memory, + sections, + symbols: Vec::new(), + imports: Vec::new(), + exports: Vec::new(), + function_hints: Vec::new(), + metadata: BTreeMap::new(), + warnings, + }; + + obj.metadata.insert( + "pe.kind".into(), + if is_image { + if characteristics & 0x2000 != 0 { + "dll".into() + } else { + "executable".into() + } + } else { + "object".into() + }, + ); + obj.metadata + .insert("pe.machine".into(), format!("{machine:#x}")); + if is_image { + obj.metadata + .insert("pe.subsystem".into(), subsystem_name(subsystem).into()); + if dll_characteristics & 0x0100 != 0 { + obj.metadata.insert("pe.nx".into(), "true".into()); + } + if dll_characteristics & 0x0040 != 0 { + obj.metadata.insert("pe.aslr".into(), "true".into()); + } + if dll_characteristics & 0x4000 != 0 { + obj.metadata.insert("pe.cfg".into(), "true".into()); + } + } + + read_symbols(&r, sym_table, n_symbols, is_image, base, &mut obj, caps); + let mut win = WindowsInfo::default(); + if is_image { + read_imports(&r, &dirs[DIR_IMPORT], base, wide, &mut obj, caps, false); + read_imports( + &r, + &dirs[DIR_DELAY_IMPORT], + base, + wide, + &mut obj, + caps, + true, + ); + read_exports(&r, &dirs[DIR_EXPORT], base, &mut obj, caps); + read_debug(&r, &dirs[DIR_DEBUG], base, &mut obj, caps); + + let img = Image::new(r, &obj.sections, base, image_base, wide, obj.arch.clone()); + win.exceptions = pdata::read( + &img, + dirs[DIR_EXCEPTION].rva, + dirs[DIR_EXCEPTION].size, + caps, + &mut obj, + ); + win.tls = windirs::read_tls(&img, dirs[DIR_TLS].rva, dirs[DIR_TLS].size, caps, &mut obj); + win.relocations = windirs::read_relocations( + &img, + dirs[DIR_BASERELOC].rva, + dirs[DIR_BASERELOC].size, + caps, + &mut obj, + ); + win.load_config = windirs::read_load_config( + &img, + dirs[DIR_LOAD_CONFIG].rva, + dirs[DIR_LOAD_CONFIG].size, + caps, + &mut obj, + ); + record_windows_metadata(&win, &mut obj); + } + + if let Some(entry) = obj.entry { + obj.function_hints.push(FunctionHint { + addr: entry, + size: None, + name: Some("entry".into()), + provenance: Provenance::new(Evidence::EntryPoint), + }); + } + + obj.normalize_symbols(); + obj.normalize_hints(); + Ok((obj, win)) +} + +/// Summarize the Windows directories into the metadata map, so a report that +/// never looks at [`WindowsInfo`] still says what was there. +fn record_windows_metadata(win: &WindowsInfo, obj: &mut Object) { + if !win.exceptions.functions.is_empty() { + obj.metadata.insert( + "pe.runtime_functions".into(), + win.exceptions.functions.len().to_string(), + ); + let scopes: usize = win.exceptions.unwind.iter().map(|u| u.scopes.len()).sum(); + if scopes != 0 { + obj.metadata + .insert("pe.seh_scopes".into(), scopes.to_string()); + } + } + if let Some(tls) = &win.tls { + obj.metadata.insert("pe.tls".into(), "present".into()); + obj.metadata + .insert("pe.tls_callbacks".into(), tls.callbacks.len().to_string()); + } + if !win.relocations.is_empty() { + obj.metadata + .insert("pe.relocations".into(), win.relocations.len().to_string()); + } + if let Some(cfg) = &win.load_config { + obj.metadata + .insert("pe.load_config".into(), cfg.size.to_string()); + if !cfg.guard_functions.is_empty() { + obj.metadata.insert( + "pe.guard_functions".into(), + cfg.guard_functions.len().to_string(), + ); + } + if !cfg.se_handlers.is_empty() { + obj.metadata.insert( + "pe.safeseh_handlers".into(), + cfg.se_handlers.len().to_string(), + ); + } + } +} + +fn machine_to_arch(m: u16) -> Arch { + match m { + IMAGE_FILE_MACHINE_I386 => Arch::X86, + IMAGE_FILE_MACHINE_AMD64 => Arch::X86_64, + IMAGE_FILE_MACHINE_ARM | IMAGE_FILE_MACHINE_ARMNT => Arch::Arm, + IMAGE_FILE_MACHINE_ARM64 => Arch::AArch64, + other => Arch::Unknown(other as u32), + } +} + +fn subsystem_name(s: u16) -> &'static str { + match s { + 1 => "native", + 2 => "windows gui", + 3 => "windows console", + 9 => "windows ce", + 10 => "efi application", + _ => "unknown", + } +} + +/// A section name, following the `/N` escape into the string table that long +/// names in object files use. +fn section_name(raw: &[u8], r: &Reader<'_>, sym_table: u32, n_symbols: u32, caps: &Caps) -> String { + let trimmed: Vec = raw.iter().copied().take_while(|b| *b != 0).collect(); + if trimmed.first() == Some(&b'/') { + if let Ok(off) = String::from_utf8_lossy(&trimmed[1..]).trim().parse::() { + let strings = sym_table as u64 + n_symbols as u64 * 18; + if let Ok(name) = r.cstr_at("long section name", strings + off, caps.string_len) { + return String::from_utf8_lossy(name).into_owned(); + } + } + } + String::from_utf8_lossy(&trimmed).into_owned() +} + +/// The COFF symbol table, which object files carry and linked images rarely do. +fn read_symbols( + r: &Reader<'_>, + sym_table: u32, + n_symbols: u32, + is_image: bool, + base: u64, + obj: &mut Object, + caps: &Caps, +) { + if sym_table == 0 || n_symbols == 0 { + return; + } + if caps + .check("symbols", n_symbols as u64, caps.symbols) + .is_err() + { + obj.warnings + .push(format!("{n_symbols} symbols exceeds the cap; skipped")); + return; + } + let strings = sym_table as u64 + n_symbols as u64 * 18; + + let mut i = 0u32; + while i < n_symbols { + let at = sym_table as u64 + i as u64 * 18; + let Ok(mut s) = r.slice_at("symbol", at, 18) else { + break; + }; + let Ok(name_field) = s.bytes("symbol name", 8) else { + break; + }; + // A name of eight bytes or fewer is inline; anything longer is a zero + // word followed by an offset into the string table. + let name = if name_field[..4] == [0, 0, 0, 0] { + let off = + u32::from_le_bytes([name_field[4], name_field[5], name_field[6], name_field[7]]); + r.cstr_at("symbol name", strings + off as u64, caps.string_len) + .map(|b| String::from_utf8_lossy(b).into_owned()) + .unwrap_or_default() + } else { + String::from_utf8_lossy( + &name_field + .iter() + .copied() + .take_while(|b| *b != 0) + .collect::>(), + ) + .into_owned() + }; + let (Ok(value), Ok(section), Ok(kind), Ok(class), Ok(aux)) = ( + s.u32("Value"), + s.i16("SectionNumber"), + s.u16("Type"), + s.u8("StorageClass"), + s.u8("NumberOfAuxSymbols"), + ) else { + break; + }; + i += 1 + aux as u32; + + if name.is_empty() { + continue; + } + // Section numbers are one-based; zero means undefined. + let undefined = section <= 0; + let addr = if undefined { + Addr::ZERO + } else if is_image { + Addr(base.wrapping_add(value as u64)) + } else { + match obj.sections.get(section as usize - 1) { + Some(sec) => Addr(sec.range.start().get().wrapping_add(value as u64)), + None => Addr::ZERO, + } + }; + // Type 0x20 is a function; storage class 2 is external, 3 is static. + let is_function = kind == 0x20; + let sym_kind = match (undefined, is_function) { + (true, _) => SymbolKind::Undefined, + (_, true) => SymbolKind::Function, + _ => SymbolKind::Object, + }; + let binding = match class { + 2 => Binding::Global, + 3 => Binding::Local, + 105 => Binding::Weak, + _ => Binding::Local, + }; + + if undefined { + obj.imports.push(Import { + name: name.clone(), + library: None, + thunk: None, + }); + } else if is_function && addr != Addr::ZERO { + obj.function_hints.push(FunctionHint { + addr, + size: None, + name: Some(name.clone()), + provenance: Provenance::new(Evidence::SymbolTable), + }); + } + + obj.symbols.push(Symbol { + name, + addr, + size: 0, + kind: sym_kind, + binding, + dynamic: false, + }); + } +} + +/// Read an RVA-addressed structure out of the mapped image. +fn at_rva(r: &Reader<'_>, obj: &Object, base: u64, rva: u32) -> Option { + // Directories are addressed by RVA but stored at a file offset, so the + // section containing the RVA gives the translation. + let va = base.wrapping_add(rva as u64); + let sec = obj.sections.iter().find(|s| s.range.contains(Addr(va)))?; + let delta = va - sec.range.start().get(); + let off = sec.file_offset + delta; + (off < r.len() as u64).then_some(off) +} + +/// The import directory, and the delay-import directory, which has the same +/// shape with two extra fields in front. +fn read_imports( + r: &Reader<'_>, + dir: &Dir, + base: u64, + wide: bool, + obj: &mut Object, + caps: &Caps, + delay: bool, +) { + if dir.rva == 0 || dir.size == 0 { + return; + } + let Some(mut at) = at_rva(r, obj, base, dir.rva) else { + obj.warnings + .push("import directory RVA is not inside any section".into()); + return; + }; + let step = if delay { 32 } else { 20 }; + let thunk_size = if wide { 8 } else { 4 }; + + for _ in 0..(dir.size as u64 / step).max(1).min(caps.sections) { + let Ok(mut d) = r.slice_at("import descriptor", at, step) else { + break; + }; + // Delay descriptors begin with Attributes, then the name RVA. + if delay { + let _ = d.u32("Attributes"); + } + let (name_rva, first_thunk, original_thunk) = if delay { + let name = d.u32("DllNameRVA").unwrap_or(0); + let _module = d.u32("ModuleHandleRVA"); + let iat = d.u32("ImportAddressTableRVA").unwrap_or(0); + let int = d.u32("ImportNameTableRVA").unwrap_or(0); + (name, iat, int) + } else { + let original = d.u32("OriginalFirstThunk").unwrap_or(0); + let _stamp = d.u32("TimeDateStamp"); + let _forwarder = d.u32("ForwarderChain"); + let name = d.u32("NameRVA").unwrap_or(0); + let first = d.u32("FirstThunk").unwrap_or(0); + (name, first, original) + }; + at += step; + if name_rva == 0 && first_thunk == 0 { + break; + } + + let library = at_rva(r, obj, base, name_rva) + .and_then(|o| r.cstr_at("dll name", o, caps.string_len).ok()) + .map(|b| String::from_utf8_lossy(b).into_owned()); + + // The name table holds the names; the address table holds the slots + // the program calls through. Either may be absent. + let names_rva = if original_thunk != 0 { + original_thunk + } else { + first_thunk + }; + let Some(mut names_at) = at_rva(r, obj, base, names_rva) else { + continue; + }; + + for n in 0..caps.symbols { + let Ok(mut t) = r.slice_at("thunk", names_at, thunk_size) else { + break; + }; + let Ok(value) = t.uword("thunk", wide) else { + break; + }; + names_at += thunk_size; + if value == 0 { + break; + } + let ordinal_flag = if wide { 1u64 << 63 } else { 1u64 << 31 }; + let name = if value & ordinal_flag != 0 { + format!("ordinal_{}", value & 0xffff) + } else { + // The hint/name table entry is a two-byte hint then the name. + match at_rva(r, obj, base, value as u32) + .and_then(|o| r.cstr_at("import name", o + 2, caps.string_len).ok()) + { + Some(b) => String::from_utf8_lossy(b).into_owned(), + None => continue, + } + }; + // The slot the call goes through, which is what makes an indirect + // call print a name. + let thunk = (first_thunk != 0) + .then(|| Addr(base.wrapping_add(first_thunk as u64 + n * thunk_size))); + obj.imports.push(Import { + name, + library: library.clone(), + thunk, + }); + } + } +} + +fn read_exports(r: &Reader<'_>, dir: &Dir, base: u64, obj: &mut Object, caps: &Caps) { + if dir.rva == 0 { + return; + } + let Some(at) = at_rva(r, obj, base, dir.rva) else { + return; + }; + let Ok(mut d) = r.slice_at("export directory", at, 40) else { + return; + }; + let _ = d.skip("flags, stamp, versions", 12); + let name_rva = d.u32("NameRVA").unwrap_or(0); + let ordinal_base = d.u32("OrdinalBase").unwrap_or(1); + let n_functions = d.u32("NumberOfFunctions").unwrap_or(0); + let n_names = d.u32("NumberOfNames").unwrap_or(0); + let functions_rva = d.u32("AddressTableRVA").unwrap_or(0); + let names_rva = d.u32("NamePointerRVA").unwrap_or(0); + let ordinals_rva = d.u32("OrdinalTableRVA").unwrap_or(0); + + if let Some(name) = at_rva(r, obj, base, name_rva) + .and_then(|o| r.cstr_at("module name", o, caps.string_len).ok()) + { + obj.metadata.insert( + "pe.export_name".into(), + String::from_utf8_lossy(name).into_owned(), + ); + } + + if caps.check("exports", n_names as u64, caps.symbols).is_err() { + return; + } + let (Some(names_at), Some(ordinals_at), Some(functions_at)) = ( + at_rva(r, obj, base, names_rva), + at_rva(r, obj, base, ordinals_rva), + at_rva(r, obj, base, functions_rva), + ) else { + return; + }; + + for i in 0..n_names as u64 { + let Ok(mut np) = r.slice_at("export name pointer", names_at + i * 4, 4) else { + break; + }; + let Ok(name_rva) = np.u32("name RVA") else { + break; + }; + let Ok(mut op) = r.slice_at("export ordinal", ordinals_at + i * 2, 2) else { + break; + }; + let Ok(ordinal) = op.u16("ordinal") else { + break; + }; + if ordinal as u32 >= n_functions { + continue; + } + let Ok(mut fp) = r.slice_at("export address", functions_at + ordinal as u64 * 4, 4) else { + break; + }; + let Ok(func_rva) = fp.u32("function RVA") else { + break; + }; + let Some(name) = at_rva(r, obj, base, name_rva) + .and_then(|o| r.cstr_at("export name", o, caps.string_len).ok()) + .map(|b| String::from_utf8_lossy(b).into_owned()) + else { + continue; + }; + if func_rva == 0 { + continue; + } + let addr = Addr(base.wrapping_add(func_rva as u64)); + obj.exports.push(Export { + name: name.clone(), + addr, + ordinal: Some(ordinal as u32 + ordinal_base), + }); + if obj.memory.is_executable(addr) { + obj.function_hints.push(FunctionHint { + addr, + size: None, + name: Some(name), + provenance: Provenance::new(Evidence::Export), + }); + } + } +} + +/// The debug directory, for the PDB path, which says where the symbols went. +fn read_debug(r: &Reader<'_>, dir: &Dir, base: u64, obj: &mut Object, caps: &Caps) { + if dir.rva == 0 || dir.size == 0 { + return; + } + let Some(at) = at_rva(r, obj, base, dir.rva) else { + return; + }; + for i in 0..(dir.size as u64 / 28).min(64) { + let Ok(mut e) = r.slice_at("debug directory", at + i * 28, 28) else { + break; + }; + let _ = e.skip("characteristics, stamp, versions", 12); + let (Ok(kind), Ok(size), Ok(_addr), Ok(offset)) = ( + e.u32("Type"), + e.u32("SizeOfData"), + e.u32("AddressOfRawData"), + e.u32("PointerToRawData"), + ) else { + break; + }; + // IMAGE_DEBUG_TYPE_CODEVIEW + if kind != 2 || size < 24 { + continue; + } + let Ok(cv) = r.slice_at("codeview record", offset as u64, size as u64) else { + continue; + }; + // "RSDS" then a GUID, an age, and the path. + if cv.data().len() < 24 || &cv.data()[..4] != b"RSDS" { + continue; + } + if let Ok(path) = cv.cstr_at("pdb path", 24, caps.string_len) { + obj.metadata + .insert("pe.pdb".into(), String::from_utf8_lossy(path).into_owned()); + } + // The signature and age as well as the path, because a database from + // the wrong build is worse than none: it is confidently wrong. A + // caller that fetches one has to check these before believing it. + if let Some((id, _)) = crate::pdb::codeview_identity(cv.data()) { + obj.metadata.insert("pe.pdb.key".into(), id.key()); + obj.metadata.insert("pe.pdb.age".into(), id.age.to_string()); + } + } + let _ = base; +} diff --git a/crates/e5r-format/src/raw.rs b/crates/e5r-format/src/raw.rs new file mode 100644 index 0000000..455cff4 --- /dev/null +++ b/crates/e5r-format/src/raw.rs @@ -0,0 +1,55 @@ +//! A flat image with no container: firmware, a shellcode blob, a memory dump. +//! +//! The caller supplies the base address and the architecture, since nothing in +//! the bytes says either. + +use std::collections::BTreeMap; + +use e5r_core::{Addr, AddrRange, Error, MemoryMap, Perms, Result, Segment}; + +use crate::{Format, LoadOptions, Object, Section}; + +/// Map the whole buffer read-write-execute at the requested base. +pub fn load(data: &[u8], opts: &LoadOptions) -> Result { + let arch = opts + .arch + .clone() + .ok_or_else(|| Error::unsupported("a raw image needs an architecture"))?; + let base = opts.base.unwrap_or(Addr::ZERO); + let range = AddrRange::sized(base, data.len() as u64).ok_or(Error::BadField { + field: "base address", + value: base.get(), + reason: "puts the end of the image past the end of the address space", + })?; + + let mut memory = MemoryMap::new(); + memory.add(Segment::new(range, Perms::RWX, "image", 0, data.to_vec())?); + + let bits = arch.bits(); + Ok(Object { + debug: None, + format: Format::Raw, + endian: e5r_core::Endian::Little, + bits, + arch, + entry: Some(base), + image_base: base, + pic: false, + memory, + sections: vec![Section { + name: "image".into(), + range, + file_offset: 0, + file_size: data.len() as u64, + exec: true, + write: true, + kind: 0, + }], + symbols: Vec::new(), + imports: Vec::new(), + exports: Vec::new(), + function_hints: Vec::new(), + metadata: BTreeMap::new(), + warnings: Vec::new(), + }) +} diff --git a/crates/e5r-format/src/swift.rs b/crates/e5r-format/src/swift.rs new file mode 100644 index 0000000..4ba3456 --- /dev/null +++ b/crates/e5r-format/src/swift.rs @@ -0,0 +1,831 @@ +//! Swift reflection metadata. +//! +//! Swift records what its runtime needs for reflection, casts and protocol +//! dispatch in sections the linker keeps: the nominal type descriptors, a +//! field descriptor per type naming every stored property with its type, and +//! one record per protocol conformance. None of it is stripped, because +//! `swift_conformsToProtocol` and `Mirror` read it at run time. +//! +//! Everything here is reached by relative pointer: a 32-bit signed +//! displacement from the address of the field that holds it, and in a few +//! places a low bit meaning the displacement reaches a pointer to the thing +//! rather than the thing. Measuring one from the start of its record instead +//! of from the field is the usual way a Swift reader produces output that +//! looks plausible and is wrong, so displacement arithmetic happens in exactly +//! two functions below and nowhere else. +//! +//! Nothing trusts a count: a field descriptor's record count is clamped to the +//! bytes behind it before one record is read, and the walk over a descriptor +//! section has to land exactly on the section's end or the section is reported +//! as not understood rather than half-parsed. + +use e5r_core::{Addr, Evidence, Provenance}; + +use crate::metadata::{Image, MAX_NAME}; +use crate::{FunctionHint, Object, Section}; + +/// Warnings to keep. A hostile section can produce one per record, and a +/// thousand copies of the same complaint is noise rather than a report. +const MAX_WARNINGS: usize = 32; + +/// What the Swift sections said. +#[derive(Debug, Clone, Default)] +pub struct SwiftMetadata { + /// Nominal type descriptors, in the order the type section lists them. + pub types: Vec, + /// Every field descriptor, found by walking the field metadata section + /// rather than only by following the types that point at one. + pub field_descriptors: Vec, + /// Protocol conformance records. + pub conformances: Vec, + /// Protocol descriptors this image defines. + pub protocols: Vec, + /// What could not be read, reported rather than guessed at. + pub warnings: Vec, +} + +impl SwiftMetadata { + /// True when no Swift section held anything. + pub fn is_empty(&self) -> bool { + self.types.is_empty() + && self.field_descriptors.is_empty() + && self.conformances.is_empty() + && self.protocols.is_empty() + } + + /// One hint per metadata accessor. + /// + /// The accessor is the only address in this metadata that is a function. + /// A witness table and a descriptor are data, and a hint at either would + /// be an invention. + pub fn hints(&self) -> Vec { + self.types + .iter() + .filter_map(|t| { + Some(FunctionHint { + addr: t.accessor?, + size: None, + name: Some(format!("type metadata accessor for {}", t.qualified_name)), + provenance: Provenance::new(Evidence::SwiftMetadata), + }) + }) + .collect() + } +} + +/// Which kind of context a descriptor describes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContextKind { + /// A module. + Module, + /// An extension. + Extension, + /// An anonymous context: a function body or a local scope. + Anonymous, + /// A protocol. + Protocol, + /// An opaque type, which is what `some P` produces. + OpaqueType, + /// A class. + Class, + /// A struct. + Struct, + /// An enum. + Enum, + /// A kind this does not know, kept as the number the file spells. + Other(u8), +} + +impl ContextKind { + fn from_flags(flags: u32) -> ContextKind { + match (flags & 0x1f) as u8 { + 0 => ContextKind::Module, + 1 => ContextKind::Extension, + 2 => ContextKind::Anonymous, + 3 => ContextKind::Protocol, + 4 => ContextKind::OpaqueType, + 16 => ContextKind::Class, + 17 => ContextKind::Struct, + 18 => ContextKind::Enum, + other => ContextKind::Other(other), + } + } + + /// The name the source spells this with. + pub fn as_str(self) -> &'static str { + match self { + ContextKind::Module => "module", + ContextKind::Extension => "extension", + ContextKind::Anonymous => "anonymous", + ContextKind::Protocol => "protocol", + ContextKind::OpaqueType => "opaque type", + ContextKind::Class => "class", + ContextKind::Struct => "struct", + ContextKind::Enum => "enum", + ContextKind::Other(_) => "unknown", + } + } + + /// True when a descriptor of this kind carries a name, an accessor and a + /// field descriptor, which is what makes it a nominal type. + fn is_type(self) -> bool { + matches!( + self, + ContextKind::Class | ContextKind::Struct | ContextKind::Enum + ) + } +} + +/// One nominal type the image declares. +#[derive(Debug, Clone)] +pub struct SwiftType { + /// Where the descriptor is. + pub addr: Addr, + /// Class, struct or enum. + pub kind: ContextKind, + /// The type's own name, without its parents. + pub name: String, + /// The name with its parent contexts, module first, joined with dots. The + /// same as `name` when no parent could be read. + pub qualified_name: String, + /// Its metadata accessor function, when the descriptor names one that + /// lands in executable memory. + pub accessor: Option, + /// Where its field descriptor is, when it has one. + pub field_descriptor: Option, +} + +/// How a field descriptor's owner was declared. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FieldDescriptorKind { + /// A struct. + Struct, + /// A class. + Class, + /// An enum with one payload or none. + Enum, + /// An enum with several payload cases. + MultiPayloadEnum, + /// A Swift protocol. + Protocol, + /// A class-constrained protocol. + ClassProtocol, + /// An Objective-C protocol. + ObjcProtocol, + /// An Objective-C class. + ObjcClass, + /// A kind this does not know. + Other(u16), +} + +impl FieldDescriptorKind { + fn from_raw(v: u16) -> FieldDescriptorKind { + match v { + 0 => FieldDescriptorKind::Struct, + 1 => FieldDescriptorKind::Class, + 2 => FieldDescriptorKind::Enum, + 3 => FieldDescriptorKind::MultiPayloadEnum, + 4 => FieldDescriptorKind::Protocol, + 5 => FieldDescriptorKind::ClassProtocol, + 6 => FieldDescriptorKind::ObjcProtocol, + 7 => FieldDescriptorKind::ObjcClass, + other => FieldDescriptorKind::Other(other), + } + } +} + +/// One field descriptor: the stored properties or cases of one type. +#[derive(Debug, Clone)] +pub struct FieldDescriptor { + /// Where the descriptor is, which is what a type descriptor points at. + pub addr: Addr, + /// The mangled name of the type it describes, when it carries one. + pub mangled_type_name: Option, + /// The mangled name of the superclass, for a class that has one. + pub superclass: Option, + /// How the type was declared. + pub kind: FieldDescriptorKind, + /// Its fields or cases, in declaration order. + pub fields: Vec, +} + +/// One stored property or enum case. +#[derive(Debug, Clone)] +pub struct Field { + /// The name in the source. + pub name: String, + /// The mangled name of its type. Absent for an enum case with no payload, + /// which genuinely has no type. + pub mangled_type: Option, + /// True for an enum case whose payload is stored indirectly. + pub indirect_case: bool, + /// True for a `var`, false for a `let`. + pub mutable: bool, + /// True for a field the compiler made rather than the source. + pub artificial: bool, +} + +/// A Swift mangled name as reflection spells it. +/// +/// Reflection mangling is not plain text: a byte below 0x20 introduces a +/// symbolic reference, a relative pointer to the descriptor of the type that +/// belongs at that position. The text alone is therefore not the whole name, +/// and this says so rather than pretending otherwise. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct MangledName { + /// The printable characters, with the symbolic references taken out. + pub text: String, + /// True when the name carried at least one symbolic reference. + pub symbolic: bool, + /// What the symbolic references point at, in the order they appeared. + pub references: Vec, +} + +/// How a conformance record names the type that conforms. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeReference { + /// A relative pointer to the nominal type descriptor. + DirectDescriptor, + /// A relative pointer to a pointer to the descriptor. + IndirectDescriptor, + /// A relative pointer to an Objective-C class name. + DirectObjcClassName, + /// A relative pointer to a pointer to an Objective-C class object. + IndirectObjcClass, + /// A kind this does not know. + Other(u8), +} + +impl TypeReference { + fn from_flags(flags: u32) -> TypeReference { + match ((flags >> 3) & 0x7) as u8 { + 0 => TypeReference::DirectDescriptor, + 1 => TypeReference::IndirectDescriptor, + 2 => TypeReference::DirectObjcClassName, + 3 => TypeReference::IndirectObjcClass, + other => TypeReference::Other(other), + } + } +} + +/// One protocol conformance the image registers. +#[derive(Debug, Clone)] +pub struct Conformance { + /// Where the record is. + pub addr: Addr, + /// The protocol descriptor it names, when the reference resolves. + pub protocol_descriptor: Option, + /// The protocol's name, when the descriptor is in this image. + pub protocol_name: Option, + /// How the conforming type is named. + pub type_reference: TypeReference, + /// The conforming type's descriptor, when the reference reaches one here. + pub type_descriptor: Option, + /// The conforming type's name, when it could be read. + pub type_name: Option, + /// The witness table or its instantiation pattern. + pub witness_table: Option, + /// True when the conformance is declared outside both the protocol's + /// module and the type's. + pub retroactive: bool, + /// How many conditional requirements the conformance carries. The + /// requirements themselves are not parsed. + pub conditional_requirements: u32, +} + +/// One protocol the image defines. +#[derive(Debug, Clone)] +pub struct SwiftProtocol { + /// Where the descriptor is. + pub addr: Addr, + /// Its name. + pub name: String, + /// Its name with its parent contexts. + pub qualified_name: String, + /// How many requirements it declares. + pub requirements: u32, +} + +/// Read every Swift section a loaded image carries. +pub fn read(obj: &Object) -> SwiftMetadata { + let mut out = SwiftMetadata::default(); + let img = Image::new(obj); + out.field_descriptors = field_descriptors(&img, &mut out.warnings); + out.types = types(&img, &mut out.warnings); + out.conformances = conformances(&img, &mut out.warnings); + out.protocols = protocols(&img, &mut out.warnings); + out +} + +/// A 32-bit signed displacement from the address of the field holding it. +/// +/// Zero is null rather than a self-reference, which is how Swift spells an +/// absent pointer in a fixed-size record. +fn relative(img: &Image<'_>, at: Addr) -> Option { + let d = img.u32(at)? as i32; + (d != 0).then(|| at.wrapping_offset(d as i64)) +} + +/// The tagged form the type records use: the descriptor is four-byte aligned, +/// so the two bits below that carry the reference kind instead of address. +/// +/// Zero in those bits is the descriptor itself and one is a pointer to it, +/// which is what a type defined in another image looks like once the linker +/// has made a slot for it. +fn relative_tagged(img: &Image<'_>, at: Addr) -> Option<(Addr, u8)> { + let d = img.u32(at)? as i32; + if d == 0 { + return None; + } + Some((at.wrapping_offset((d & !3) as i64), (d & 3) as u8)) +} + +/// The indirectable form: the low bit of the displacement says the target is +/// a pointer to the thing rather than the thing itself. +/// +/// The bit is part of the displacement and has to be cleared before it is +/// added, not after. +fn relative_indirect(img: &Image<'_>, at: Addr) -> Option { + let d = img.u32(at)? as i32; + if d == 0 { + return None; + } + let target = at.wrapping_offset((d & !1) as i64); + if d & 1 != 0 { + img.ptr(target).filter(|p| *p != Addr::ZERO) + } else { + Some(target) + } +} + +/// True when an address is inside a section this image declares. +/// +/// A relative pointer that lands outside every section is a descriptor this +/// reader has misunderstood, or a file that is lying; either way it is a +/// warning and not a fact. +fn in_image(obj: &Object, at: Addr) -> bool { + obj.section_at(at).is_some() || obj.memory.segment_at(at).is_some() +} + +fn warn(warnings: &mut Vec, message: String) { + if warnings.len() < MAX_WARNINGS { + warnings.push(message); + } +} + +/// Sections a Swift reflection kind lives in. +/// +/// Mach-O prefixes the section name with its segment and ELF spells the same +/// section differently, so both names are checked. `__swift5_proto` and +/// `__swift5_protos` differ by one character and hold different things, which +/// is why this matches a whole name rather than a prefix. +fn sections<'a>(obj: &'a Object, mach: &'a str, elf: &'a str) -> impl Iterator { + obj.sections.iter().filter(move |s| { + !s.range.is_empty() + && s.file_size != 0 + && (s.name.ends_with(mach) || s.name == elf || s.name.strip_prefix('.') == Some(elf)) + }) +} + +/// The bytes of a section, and the address they start at. +fn body<'a>(img: &Image<'a>, s: &Section) -> Option<(Addr, &'a [u8])> { + let start = s.range.start(); + let bytes = img + .obj() + .memory + .slice(start, s.file_size.min(s.range.len()))?; + Some((start, bytes)) +} + +/// Every field descriptor, by walking the section they are packed into. +/// +/// The section is a run of variable-length records with no count in front of +/// it, so the walk is the only way to enumerate them, and the walk landing +/// exactly on the end is the check that the record layout was read correctly. +fn field_descriptors(img: &Image<'_>, warnings: &mut Vec) -> Vec { + let mut out = Vec::new(); + for s in sections(img.obj(), "__swift5_fieldmd", "swift5_fieldmd") { + let Some((start, bytes)) = body(img, s) else { + continue; + }; + let mut off = 0u64; + let limit = bytes.len() as u64; + while off + FIELD_DESCRIPTOR_SIZE <= limit { + // A linker pads a section to its alignment, and a run of zeros is + // padding rather than a descriptor with no name and no records. + if bytes[off as usize..].iter().all(|b| *b == 0) { + off = limit; + break; + } + let Some(at) = start.checked_add(off) else { + break; + }; + let Some((d, size)) = field_descriptor(img, at, limit - off, warnings) else { + warn( + warnings, + format!("swift: the field descriptor at {at} could not be read"), + ); + break; + }; + out.push(d); + // A record is at least its header, so the walk always advances. + off += size.max(FIELD_DESCRIPTOR_SIZE); + } + if off != limit { + warn( + warnings, + format!( + "swift: {} is {limit} bytes and its records account for {off}, \ + so the layout was not understood", + s.name + ), + ); + } + } + out +} + +/// One field descriptor and how many bytes it took. +fn field_descriptor( + img: &Image<'_>, + at: Addr, + room: u64, + warnings: &mut Vec, +) -> Option<(FieldDescriptor, u64)> { + let mangled_type_name = relative(img, at).and_then(|p| mangled_name(img, p, warnings)); + let superclass = relative(img, at.checked_add(4)?).and_then(|p| mangled_name(img, p, warnings)); + let kind = FieldDescriptorKind::from_raw(img.u16(at.checked_add(8)?)?); + let record_size = u64::from(img.u16(at.checked_add(10)?)?); + let declared = u64::from(img.u32(at.checked_add(12)?)?); + // Swift has only ever emitted twelve-byte records, and a record smaller + // than its three fields cannot be walked at all. + if record_size < FIELD_RECORD_SIZE { + warn( + warnings, + format!("swift: the field descriptor at {at} declares {record_size}-byte records"), + ); + return None; + } + // A count from the file cannot exceed the bytes behind it, and the bytes + // behind it are what is left of the section. + let fits = room.saturating_sub(FIELD_DESCRIPTOR_SIZE) / record_size; + if declared > fits { + warn( + warnings, + format!( + "swift: the field descriptor at {at} declares {declared} fields \ + and has room for {fits}" + ), + ); + } + let count = declared.min(fits); + let mut fields = Vec::with_capacity(usize::try_from(count).ok()?); + for i in 0..count { + let Some(record) = at.checked_add(FIELD_DESCRIPTOR_SIZE + i * record_size) else { + break; + }; + if let Some(f) = field(img, record, warnings) { + fields.push(f); + } + } + let size = FIELD_DESCRIPTOR_SIZE + count * record_size; + Some(( + FieldDescriptor { + addr: at, + mangled_type_name, + superclass, + kind, + fields, + }, + size, + )) +} + +/// One field record: flags, then the type and the name. +fn field(img: &Image<'_>, at: Addr, warnings: &mut Vec) -> Option { + let flags = img.u32(at)?; + let mangled_type = + relative(img, at.checked_add(4)?).and_then(|p| mangled_name(img, p, warnings)); + let name_at = relative(img, at.checked_add(8)?)?; + let Some(name) = img.cstr(name_at, MAX_NAME) else { + warn( + warnings, + format!("swift: the field record at {at} names no readable field at {name_at}"), + ); + return None; + }; + Some(Field { + name: name.to_string(), + mangled_type, + indirect_case: flags & FIELD_INDIRECT_CASE != 0, + mutable: flags & FIELD_VAR != 0, + artificial: flags & FIELD_ARTIFICIAL != 0, + }) +} + +/// A mangled name, which ends at a NUL but may carry symbolic references that +/// contain one. +/// +/// Stopping at the first zero byte is wrong for exactly that reason: a +/// symbolic reference is a control byte followed by a four-byte displacement, +/// and a displacement whose top byte is zero would cut the name short. +fn mangled_name(img: &Image<'_>, at: Addr, warnings: &mut Vec) -> Option { + let bytes = img.to_end(at)?; + let mut out = MangledName::default(); + let mut i = 0usize; + let pointer = usize::try_from(img.ptr_size()).ok()?; + while i < bytes.len() && i < MAX_NAME as usize { + let b = bytes[i]; + if b == 0 { + return Some(out); + } + if (0x01..=0x17).contains(&b) { + out.symbolic = true; + let Some(payload) = bytes.get(i + 1..i + 5) else { + break; + }; + let d = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + if let Some(field) = at.checked_add(i as u64 + 1) { + out.references.push(field.wrapping_offset(d as i64)); + } + i += 5; + continue; + } + if (0x18..=0x1f).contains(&b) { + // An absolute reference, which is a whole pointer rather than a + // displacement. The bytes are stepped over, not read: a linked + // image resolves them and a relocatable one does not. + out.symbolic = true; + i += 1 + pointer; + continue; + } + out.text.push(b as char); + i += 1; + } + warn( + warnings, + format!("swift: the mangled name at {at} has no terminator"), + ); + (!out.text.is_empty() || out.symbolic).then_some(out) +} + +/// Every nominal type descriptor the type section lists. +fn types(img: &Image<'_>, warnings: &mut Vec) -> Vec { + let mut out = Vec::new(); + for s in sections(img.obj(), "__swift5_types", "swift5_type_metadata") { + let Some((start, bytes)) = body(img, s) else { + continue; + }; + // An array of relative pointers, so its own length is the count. + for i in 0..bytes.len() as u64 / 4 { + let Some(slot) = start.checked_add(i * 4) else { + break; + }; + let Some((target, kind)) = relative_tagged(img, slot) else { + continue; + }; + if !in_image(img.obj(), target) { + warn( + warnings, + format!( + "swift: the type record at {slot} points at {target}, \ + outside the image" + ), + ); + continue; + } + let at = match kind { + 0 => target, + 1 => match img.ptr(target).filter(|p| *p != Addr::ZERO) { + Some(p) => p, + None => { + warn( + warnings, + format!("swift: the type record at {slot} reaches an empty slot"), + ); + continue; + } + }, + other => { + warn( + warnings, + format!("swift: the type record at {slot} has reference kind {other}"), + ); + continue; + } + }; + if !in_image(img.obj(), at) { + warn( + warnings, + format!("swift: the type record at {slot} points at {at}, outside the image"), + ); + continue; + } + if let Some(t) = nominal_type(img, at, warnings) { + out.push(t); + } + } + } + out +} + +/// One type context descriptor. +fn nominal_type(img: &Image<'_>, at: Addr, warnings: &mut Vec) -> Option { + let flags = img.u32(at)?; + let kind = ContextKind::from_flags(flags); + if !kind.is_type() { + warn( + warnings, + format!( + "swift: the descriptor at {at} is a {} rather than a nominal type", + kind.as_str() + ), + ); + return None; + } + let name = context_name(img, at)?; + let accessor = relative(img, at.checked_add(12)?) + .filter(|a| img.obj().section_at(*a).is_some_and(|s| s.exec)); + if accessor.is_none() && relative(img, at.checked_add(12)?).is_some() { + warn( + warnings, + format!("swift: the metadata accessor of {name} is not in executable memory"), + ); + } + let field_descriptor = relative(img, at.checked_add(16)?).filter(|p| in_image(img.obj(), *p)); + Some(SwiftType { + addr: at, + kind, + qualified_name: qualified(img, at, &name), + name, + accessor, + field_descriptor, + }) +} + +/// The name a context descriptor carries, which sits at the same offset for +/// every kind that has one. +fn context_name(img: &Image<'_>, at: Addr) -> Option { + let name_at = relative(img, at.checked_add(8)?)?; + Some(img.cstr(name_at, MAX_NAME)?.to_string()) +} + +/// A name with its parent contexts, module first. +/// +/// The chain is followed at most eight deep: a parent pointer is a number +/// from the file and can point back at its own child. +fn qualified(img: &Image<'_>, at: Addr, name: &str) -> String { + let mut parts = vec![name.to_string()]; + let mut current = at; + for _ in 0..8 { + let Some(parent) = current + .checked_add(4) + .and_then(|p| relative_indirect(img, p)) + .filter(|p| *p != current && in_image(img.obj(), *p)) + else { + break; + }; + let Some(flags) = img.u32(parent) else { break }; + // Only the kinds whose descriptor carries a plain name at that offset + // contribute one. An extension keeps a mangled type name there and an + // anonymous context keeps nothing, so both end the chain rather than + // spelling their bytes as an identifier. + if !matches!( + ContextKind::from_flags(flags), + ContextKind::Module + | ContextKind::Protocol + | ContextKind::Class + | ContextKind::Struct + | ContextKind::Enum + ) { + break; + } + match context_name(img, parent) { + Some(n) => parts.push(n), + None => break, + } + current = parent; + } + parts.reverse(); + parts.join(".") +} + +/// Every protocol conformance record. +fn conformances(img: &Image<'_>, warnings: &mut Vec) -> Vec { + let mut out = Vec::new(); + for s in sections(img.obj(), "__swift5_proto", "swift5_protocol_conformances") { + let Some((start, bytes)) = body(img, s) else { + continue; + }; + for i in 0..bytes.len() as u64 / 4 { + let Some(slot) = start.checked_add(i * 4) else { + break; + }; + let Some(at) = relative(img, slot) else { + continue; + }; + if !in_image(img.obj(), at) { + warn( + warnings, + format!("swift: the conformance record at {slot} points outside the image"), + ); + continue; + } + if let Some(c) = conformance(img, at) { + out.push(c); + } + } + } + out +} + +/// One `ProtocolConformanceDescriptor`. +fn conformance(img: &Image<'_>, at: Addr) -> Option { + let flags = img.u32(at.checked_add(12)?)?; + let type_reference = TypeReference::from_flags(flags); + let protocol_descriptor = relative_indirect(img, at).filter(|p| in_image(img.obj(), *p)); + let protocol_name = protocol_descriptor.and_then(|p| context_name(img, p)); + + let reference = at.checked_add(4)?; + let (type_descriptor, type_name) = match type_reference { + TypeReference::DirectDescriptor => { + let d = relative(img, reference).filter(|p| in_image(img.obj(), *p)); + (d, d.and_then(|d| context_name(img, d))) + } + TypeReference::IndirectDescriptor => { + let d = relative(img, reference) + .and_then(|p| img.ptr(p)) + .filter(|p| *p != Addr::ZERO && in_image(img.obj(), *p)); + (d, d.and_then(|d| context_name(img, d))) + } + TypeReference::DirectObjcClassName => { + let n = relative(img, reference).and_then(|p| img.cstr(p, MAX_NAME)); + (None, n.map(str::to_string)) + } + // The indirect form reaches a class object, whose name lives behind + // the Objective-C class layout rather than in this record. + TypeReference::IndirectObjcClass | TypeReference::Other(_) => (None, None), + }; + + Some(Conformance { + addr: at, + protocol_descriptor, + protocol_name, + type_reference, + type_descriptor, + type_name, + witness_table: relative(img, at.checked_add(8)?), + retroactive: flags & CONFORMANCE_RETROACTIVE != 0, + conditional_requirements: (flags >> 8) & 0xff, + }) +} + +/// Every protocol descriptor the image defines. +fn protocols(img: &Image<'_>, warnings: &mut Vec) -> Vec { + let mut out = Vec::new(); + for s in sections(img.obj(), "__swift5_protos", "swift5_protocols") { + let Some((start, bytes)) = body(img, s) else { + continue; + }; + for i in 0..bytes.len() as u64 / 4 { + let Some(slot) = start.checked_add(i * 4) else { + break; + }; + let Some(at) = relative_indirect(img, slot).filter(|p| in_image(img.obj(), *p)) else { + continue; + }; + let Some(flags) = img.u32(at) else { continue }; + if ContextKind::from_flags(flags) != ContextKind::Protocol { + warn( + warnings, + format!("swift: the protocol record at {slot} does not describe a protocol"), + ); + continue; + } + let Some(name) = context_name(img, at) else { + continue; + }; + out.push(SwiftProtocol { + addr: at, + qualified_name: qualified(img, at, &name), + name, + requirements: at + .checked_add(16) + .and_then(|p| img.u32(p)) + .unwrap_or_default(), + }); + } + } + out +} + +/// `MangledTypeName`, `Superclass`, `Kind`, `FieldRecordSize`, `NumFields`. +const FIELD_DESCRIPTOR_SIZE: u64 = 16; +/// `Flags`, `MangledTypeName`, `FieldName`. +const FIELD_RECORD_SIZE: u64 = 12; + +const FIELD_INDIRECT_CASE: u32 = 0x1; +const FIELD_VAR: u32 = 0x2; +const FIELD_ARTIFICIAL: u32 = 0x4; + +const CONFORMANCE_RETROACTIVE: u32 = 0x1 << 6; diff --git a/crates/e5r-format/src/windirs.rs b/crates/e5r-format/src/windirs.rs new file mode 100644 index 0000000..cf633c0 --- /dev/null +++ b/crates/e5r-format/src/windirs.rs @@ -0,0 +1,499 @@ +//! The Windows data directories that say where code is and where addresses are. +//! +//! Three of them, none of which the entry point reaches: +//! +//! - The TLS directory. Its callback array runs before the entry point, once +//! per thread. A packer that does its work there and never returns to the +//! declared entry point is a shape common enough that missing the array is +//! missing the program. +//! - The base relocation table. Every absolute address the image embeds is +//! listed here, which is a map of the image's own pointers whether or not it +//! is ever rebased. +//! - The load config. Its control-flow-guard function table is the linker's own +//! list of every address-taken function, and on 32-bit its SafeSEH table is +//! the list of legal exception handlers. +//! +//! Every pointer these directories hold is an absolute address relative to the +//! image base the file declares, not an RVA, so all of them go through +//! [`Image::addr_of_va`]. + +use e5r_core::{Addr, AddrRange, Caps, Evidence, Provenance}; + +use crate::pe::Image; +use crate::{FunctionHint, Object}; + +/// Pointers accepted in one TLS callback array. The array is NUL-terminated +/// with no count, so something has to stop the walk on a file that never +/// terminates it. +const MAX_TLS_CALLBACKS: u64 = 4096; + +/// `IMAGE_TLS_DIRECTORY`, with the callback array already walked. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TlsDirectory { + /// The template the loader copies into each thread's TLS block. Absent when + /// the declared bounds are not a range inside the image. + pub raw_data: Option, + /// Where the loader writes the TLS slot index. + pub index: Option, + /// Address of the callback array itself. + pub callback_array: Option, + /// Bytes zeroed past the end of the template. + pub zero_fill: u32, + /// Alignment and reserved bits. + pub characteristics: u32, + /// The callbacks, in the order the loader calls them. + pub callbacks: Vec, +} + +/// One base relocation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BaseReloc { + /// The address holding the value to fix up. + pub addr: Addr, + /// `IMAGE_REL_BASED_*` type, as [`reloc_kind`] names it. + pub kind: u8, +} + +/// The name of a base relocation type, or `"unknown"`. +pub fn reloc_kind(kind: u8) -> &'static str { + match kind { + 0 => "absolute", + 1 => "high", + 2 => "low", + 3 => "highlow", + 4 => "highadj", + 5 => "mips jmpaddr / arm mov32 / riscv high20", + 7 => "thumb mov32 / riscv low12i", + 8 => "riscv low12s / loongarch mark la", + 9 => "mips jmpaddr16 / loongarch stack", + 10 => "dir64", + _ => "unknown", + } +} + +/// `IMAGE_LOAD_CONFIG_DIRECTORY`, the fields that name code or a cookie. +/// +/// The directory has grown for twenty years and a given image implements only +/// as much of it as its `Size` says. Everything past that size is absent, not +/// zero, and is reported as absent. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct LoadConfig { + /// The `Size` field, which is how much of the structure exists. + pub size: u32, + /// The stack cookie's storage. + pub security_cookie: Option, + /// SafeSEH's table of legal handlers, 32-bit images only. + pub se_handler_table: Option, + /// Entries the SafeSEH table claims. + pub se_handler_count: u64, + /// The handlers themselves, when the table was readable. + pub se_handlers: Vec, + /// Where the guard check function's pointer lives. + pub guard_cf_check: Option, + /// Where the guard dispatch function's pointer lives. + pub guard_cf_dispatch: Option, + /// The guard function table. + pub guard_cf_function_table: Option, + /// Entries the guard function table claims. + pub guard_cf_function_count: u64, + /// `IMAGE_GUARD_*` flags. The top four bits give the extra bytes each guard + /// table entry carries past its four-byte RVA. + pub guard_flags: u32, + /// Every address-taken function the guard table lists. + pub guard_functions: Vec, +} + +/// Read the TLS directory and walk its callback array. +pub fn read_tls( + img: &Image<'_>, + dir_rva: u32, + dir_size: u32, + caps: &Caps, + obj: &mut Object, +) -> Option { + if dir_rva == 0 { + return None; + } + let ptr = if img.wide() { 8 } else { 4 }; + let want = ptr * 4 + 8; + if (dir_size as u64) < want && dir_size != 0 { + obj.warnings.push(format!( + "TLS directory is {dir_size} bytes, short of the {want} an IMAGE_TLS_DIRECTORY needs" + )); + } + let at = match img.offset_of(dir_rva) { + Some(at) => at, + None => { + obj.warnings + .push("TLS directory RVA is not inside any section".into()); + return None; + } + }; + let mut d = img + .reader() + .slice_at("IMAGE_TLS_DIRECTORY", at, want) + .ok()?; + let start = d.uword("StartAddressOfRawData", img.wide()).ok()?; + let end = d.uword("EndAddressOfRawData", img.wide()).ok()?; + let index = d.uword("AddressOfIndex", img.wide()).ok()?; + let callbacks = d.uword("AddressOfCallBacks", img.wide()).ok()?; + let zero_fill = d.u32("SizeOfZeroFill").ok()?; + let characteristics = d.u32("Characteristics").ok()?; + + let mut tls = TlsDirectory { + raw_data: match (img.addr_of_va(start), img.addr_of_va(end)) { + (Some(s), Some(e)) => AddrRange::new(s, e), + _ => None, + }, + index: img.addr_of_va(index), + callback_array: img.addr_of_va(callbacks), + zero_fill, + characteristics, + callbacks: Vec::new(), + }; + + let Some(array) = tls.callback_array else { + return Some(tls); + }; + let Some(mut off) = img.offset_of_addr(array) else { + obj.warnings.push(format!( + "TLS callback array is at {array}, which no section maps; no callbacks were read" + )); + return Some(tls); + }; + // The array has no count, so the walk is bounded three ways: by the bytes + // left in the section that holds it, by the cap, and by the terminator. + let room = img + .section_bytes_after(off) + .unwrap_or_else(|| img.len().saturating_sub(off)); + let limit = (room / ptr).min(MAX_TLS_CALLBACKS).min(caps.symbols); + for i in 0..limit { + let Ok(mut slot) = img.reader().slice_at("TLS callback", off, ptr) else { + break; + }; + let Ok(va) = slot.uword("callback", img.wide()) else { + break; + }; + off += ptr; + if va == 0 { + break; + } + let Some(addr) = img.addr_of_va(va) else { + obj.warnings.push(format!( + "TLS callback {i} is {va:#x}, which is below the image base" + )); + continue; + }; + if !img.is_code(addr) { + obj.warnings.push(format!( + "TLS callback {i} points at {addr}, which is not executable; not treated as a \ + function" + )); + continue; + } + tls.callbacks.push(addr); + obj.function_hints.push(FunctionHint { + addr, + size: None, + name: Some(format!("tls_callback_{i}")), + provenance: Provenance::new(Evidence::InitArray), + }); + } + if tls.callbacks.len() as u64 == limit && limit != 0 { + obj.warnings.push(format!( + "TLS callback array reached {limit} entries without a terminator; the walk stopped \ + there" + )); + } + Some(tls) +} + +/// Walk the base relocation table. +/// +/// Each block states its own size, so the one thing this must never do is trust +/// it: a block claiming fewer than its own eight header bytes would leave the +/// cursor where it was and the walk would not end. +pub fn read_relocations( + img: &Image<'_>, + dir_rva: u32, + dir_size: u32, + caps: &Caps, + obj: &mut Object, +) -> Vec { + let mut out = Vec::new(); + if dir_rva == 0 || dir_size == 0 { + return out; + } + let Some(start) = img.offset_of(dir_rva) else { + obj.warnings + .push("base relocation directory RVA is not inside any section".into()); + return out; + }; + // The table ends where the directory says, where the file ends, or where + // the section holding it ends, whichever comes first. + let room = img + .section_bytes_after(start) + .unwrap_or_else(|| img.len().saturating_sub(start)); + let end = start.saturating_add((dir_size as u64).min(room)); + let mut at = start; + while at + 8 <= end { + let Ok(mut h) = img.reader().slice_at("base relocation block", at, 8) else { + break; + }; + let (Ok(page), Ok(size)) = (h.u32("PageRVA"), h.u32("BlockSize")) else { + break; + }; + if size == 0 { + break; + } + if (size as u64) < 8 { + obj.warnings.push(format!( + "base relocation block at file offset {at:#x} claims {size} bytes, fewer than its \ + own header; the walk stopped" + )); + break; + } + let body = (size as u64 - 8).min(end - at - 8); + let entries = body / 2; + let mut i = 0u64; + while i < entries { + let Ok(mut e) = img.reader().slice_at("base relocation", at + 8 + i * 2, 2) else { + break; + }; + let Ok(word) = e.u16("entry") else { break }; + i += 1; + let kind = (word >> 12) as u8; + // Type 0 is padding to a four-byte boundary, not a fixup. + if kind == 0 { + continue; + } + // HIGHADJ carries its addend in the next halfword, which is data + // rather than another entry. + if kind == 4 { + i += 1; + } + if out.len() as u64 >= caps.relocations { + obj.warnings.push(format!( + "base relocation table passed the cap of {} entries; the rest was not read", + caps.relocations + )); + return out; + } + out.push(BaseReloc { + addr: img.addr_of(page.wrapping_add((word & 0xfff) as u32)), + kind, + }); + } + // size is at least 8, so the cursor always moves. + at += size as u64; + } + out +} + +// Field offsets, which differ between the two widths because the pointers do. +// Named rather than computed so each one can be checked against winnt.h. +const LC32: LoadConfigLayout = LoadConfigLayout { + ptr: 4, + security_cookie: 0x3c, + se_handler_table: 0x40, + guard_cf_check: 0x48, + guard_cf_function_table: 0x50, + guard_flags: 0x58, +}; +const LC64: LoadConfigLayout = LoadConfigLayout { + ptr: 8, + security_cookie: 0x58, + se_handler_table: 0x60, + guard_cf_check: 0x70, + guard_cf_function_table: 0x80, + guard_flags: 0x90, +}; + +struct LoadConfigLayout { + ptr: u64, + security_cookie: u64, + se_handler_table: u64, + guard_cf_check: u64, + guard_cf_function_table: u64, + guard_flags: u64, +} + +/// Read the load config, the SafeSEH handler table and the guard function +/// table. +/// +/// The addresses these tables hold are function entries, and the linker put +/// them there. They are returned rather than turned into hints: `Evidence` has +/// no variant for "a table the loader validates calls against", and labelling +/// them as something else would make the strength a lie. See the module +/// documentation in `pe.rs` for the one-line change that would fix it. +pub fn read_load_config( + img: &Image<'_>, + dir_rva: u32, + dir_size: u32, + caps: &Caps, + obj: &mut Object, +) -> Option { + if dir_rva == 0 { + return None; + } + let l = if img.wide() { &LC64 } else { &LC32 }; + let at = match img.offset_of(dir_rva) { + Some(at) => at, + None => { + obj.warnings + .push("load config directory RVA is not inside any section".into()); + return None; + } + }; + let mut head = img.reader().slice_at("load config Size", at, 4).ok()?; + let declared = head.u32("Size").ok()?; + // The structure is only as long as it says, and the directory entry is a + // second opinion on that. Take the smaller, and never read past it. + let size = (declared as u64) + .min(if dir_size == 0 { + u64::MAX + } else { + dir_size as u64 + }) + .min(img.len().saturating_sub(at)); + if declared as u64 > size { + obj.warnings.push(format!( + "load config claims {declared} bytes, of which {size} are present" + )); + } + + let field = |off: u64| -> Option { + if off + l.ptr > size { + return None; + } + img.reader() + .slice_at("load config field", at + off, l.ptr) + .ok()? + .uword("field", l.ptr == 8) + .ok() + }; + let mut cfg = LoadConfig { + size: declared, + security_cookie: field(l.security_cookie).and_then(|v| img.addr_of_va(v)), + se_handler_table: field(l.se_handler_table).and_then(|v| img.addr_of_va(v)), + se_handler_count: field(l.se_handler_table + l.ptr).unwrap_or(0), + se_handlers: Vec::new(), + guard_cf_check: field(l.guard_cf_check).and_then(|v| img.addr_of_va(v)), + guard_cf_dispatch: field(l.guard_cf_check + l.ptr).and_then(|v| img.addr_of_va(v)), + guard_cf_function_table: field(l.guard_cf_function_table).and_then(|v| img.addr_of_va(v)), + guard_cf_function_count: field(l.guard_cf_function_table + l.ptr).unwrap_or(0), + guard_flags: if l.guard_flags + 4 <= size { + img.reader() + .slice_at("GuardFlags", at + l.guard_flags, 4) + .ok() + .and_then(|mut r| r.u32("GuardFlags").ok()) + .unwrap_or(0) + } else { + 0 + }, + guard_functions: Vec::new(), + }; + + // SafeSEH: a sorted array of RVAs, 32-bit images only. + if let Some(table) = cfg.se_handler_table { + cfg.se_handlers = read_rva_table( + img, + table, + cfg.se_handler_count, + 4, + "SafeSEH handler table", + caps, + obj, + ); + } + // Control Flow Guard: an RVA plus however many metadata bytes the top of + // GuardFlags says each entry carries. + if let Some(table) = cfg.guard_cf_function_table { + let stride = 4 + ((cfg.guard_flags >> 28) & 0xf) as u64; + cfg.guard_functions = read_rva_table( + img, + table, + cfg.guard_cf_function_count, + stride, + "control flow guard function table", + caps, + obj, + ); + } + // Both tables were built by the linker out of real function entries, and + // the loader enforces them at run time, so an address in one is a function + // whatever else the image says. Anything not executable is a warning, not + // a hint: it would mean the table disagrees with the section flags, which + // is worth saying and not worth believing. + for (what, addrs) in [ + ("safeseh_handler", &cfg.se_handlers), + ("guard_function", &cfg.guard_functions), + ] { + for a in addrs { + if !img.is_code(*a) { + obj.warnings + .push(format!("{what} at {a} is not in executable memory")); + continue; + } + obj.function_hints.push(FunctionHint { + addr: *a, + size: None, + name: None, + provenance: Provenance::new(Evidence::GuardTable), + }); + } + } + Some(cfg) +} + +/// An array of RVAs at an absolute address, bounded by the file before the +/// claimed count is believed. +fn read_rva_table( + img: &Image<'_>, + table: Addr, + count: u64, + stride: u64, + what: &str, + caps: &Caps, + obj: &mut Object, +) -> Vec { + let mut out = Vec::new(); + if count == 0 || stride < 4 { + return out; + } + let Some(at) = img.offset_of_addr(table) else { + obj.warnings + .push(format!("{what} is at {table}, which no section maps")); + return out; + }; + let room = img + .section_bytes_after(at) + .unwrap_or_else(|| img.len().saturating_sub(at)); + let fits = room / stride; + let n = count.min(fits).min(caps.symbols); + if n < count { + obj.warnings.push(format!( + "{what} claims {count} entries, of which {n} fit in the file" + )); + } + for i in 0..n { + let Ok(mut e) = img.reader().slice_at("table entry", at + i * stride, 4) else { + break; + }; + let Ok(rva) = e.u32("function RVA") else { + break; + }; + if rva == 0 { + continue; + } + let addr = img.addr_of(rva); + if img.is_code(addr) { + out.push(addr); + } else { + obj.warnings.push(format!( + "{what} entry {i} points at {addr}, which is not executable" + )); + } + } + out +} diff --git a/crates/e5r-format/tests/archive.rs b/crates/e5r-format/tests/archive.rs new file mode 100644 index 0000000..23d0351 --- /dev/null +++ b/crates/e5r-format/tests/archive.rs @@ -0,0 +1,232 @@ +//! Archive tests, with `ar t` and `nm -s` as the oracle. +//! +//! Skipped when the corpus or the binutils are absent so a fresh checkout +//! still passes; run `scripts/build-fixtures.sh` to enable them. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_core::Arch; +use e5r_format::archive::{self, Flavor}; +use e5r_format::{LoadOptions, load}; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn read(name: &str) -> Option> { + std::fs::read(corpus()?.join(name)).ok() +} + +/// Run a binutil over a fixture, or `None` when it is not installed. +fn tool(cmd: &str, args: &[&str], name: &str) -> Option { + let p = corpus()?.join(name); + let out = Command::new(cmd).args(args).arg(&p).output().ok()?; + out.status + .success() + .then(|| String::from_utf8_lossy(&out.stdout).into_owned()) +} + +#[test] +fn members_match_ar_t() { + let Some(data) = read("libshapes.a") else { + return; + }; + let Some(listing) = tool("ar", &["t"], "libshapes.a") else { + return; + }; + let a = archive::open(&data).expect("libshapes.a did not parse"); + assert_eq!(a.flavor, Flavor::Gnu); + assert!(!a.thin); + assert!(a.warnings.is_empty(), "{:?}", a.warnings); + let want: Vec<&str> = listing + .lines() + .map(str::trim) + .filter(|l| !l.is_empty()) + .collect(); + let got: Vec<&str> = a.objects().map(|(_, m)| m.name.as_str()).collect(); + assert_eq!(got, want); + // The long name only fits in the `//` table, which is the point of it. + assert!( + want.iter().any(|n| n.len() > 16), + "the fixture lost its long member name" + ); +} + +#[test] +fn member_sizes_and_offsets_land_on_real_objects() { + let Some(data) = read("libshapes.a") else { + return; + }; + let a = archive::open(&data).unwrap(); + for (i, m) in a.objects() { + assert_eq!(m.data().len() as u64, m.size); + // The recorded offset and size have to name the same bytes the + // member handed out, or a caller reading the file itself would + // disagree with a caller reading `data()`. + let at = m.offset as usize; + assert_eq!(&data[at..at + m.size as usize], m.data()); + let obj = a.load_member(i, &LoadOptions::default()).expect(&m.name); + assert_eq!(obj.arch, Arch::AArch64); + assert!(obj.section(".text").is_some(), "{} has no .text", m.name); + } +} + +#[test] +fn the_index_matches_nm_s() { + let Some(data) = read("libshapes.a") else { + return; + }; + let Some(listing) = tool("nm", &["-s"], "libshapes.a") else { + return; + }; + let a = archive::open(&data).unwrap(); + // `nm -s` prints "symbol in member" under an "Archive index:" heading. + let mut want: Vec<(String, String)> = listing + .lines() + .filter_map(|l| l.split_once(" in ")) + .map(|(s, m)| (s.trim().to_string(), m.trim().to_string())) + .collect(); + if want.is_empty() { + return; + } + let mut got: Vec<(String, String)> = a + .index + .iter() + .filter_map(|e| { + e.member + .map(|i| (e.name.clone(), a.members[i].name.clone())) + }) + .collect(); + want.sort(); + got.sort(); + assert_eq!(got, want); + // Every index entry resolved, so no offset pointed into open space. + assert!(a.index.iter().all(|e| e.member.is_some())); +} + +#[test] +fn a_symbol_names_the_member_that_defines_it() { + let Some(data) = read("libshapes.a") else { + return; + }; + let Some(listing) = tool("nm", &["-s"], "libshapes.a") else { + return; + }; + let a = archive::open(&data).unwrap(); + let Some((sym, member)) = listing.lines().find_map(|l| l.split_once(" in ")) else { + return; + }; + let (sym, member) = (sym.trim(), member.trim()); + let definers = a.definers(sym); + assert!(!definers.is_empty(), "{sym} is defined by nothing"); + assert!( + definers.iter().any(|&i| a.members[i].name == member), + "{sym} should be in {member}, found {:?}", + definers + .iter() + .map(|&i| &a.members[i].name) + .collect::>() + ); + assert!(a.symbols_of(definers[0]).contains(&sym)); +} + +#[test] +fn a_thin_archive_lists_members_without_their_bytes() { + let Some(data) = read("libshapes-thin.a") else { + return; + }; + let Some(listing) = tool("ar", &["t"], "libshapes-thin.a") else { + return; + }; + let a = archive::open(&data).expect("thin archive did not parse"); + assert!(a.thin); + // `ar t` resolves a thin member against the archive's directory, so the + // basename is the part both sides agree on. + let want: Vec<&str> = listing + .lines() + .map(str::trim) + .filter(|l| !l.is_empty()) + .map(|l| l.rsplit('/').next().unwrap()) + .collect(); + let got: Vec<&str> = a + .objects() + .map(|(_, m)| m.name.rsplit('/').next().unwrap()) + .collect(); + assert_eq!(got, want); + // The bytes are elsewhere, and saying so is better than handing back the + // wrong ones. + let external = a.members.iter().position(|m| m.thin).unwrap(); + assert!(a.load_member(external, &LoadOptions::default()).is_err()); +} + +#[test] +fn load_says_it_is_an_archive() { + let Some(data) = read("libshapes.a") else { + return; + }; + let e = load(&data, &LoadOptions::default()).unwrap_err(); + assert!( + !e.is_not_recognized(), + "an archive should not read as garbage" + ); + let msg = e.to_string(); + assert!(msg.contains("archive"), "{msg}"); + assert!(msg.contains("member"), "{msg}"); +} + +#[test] +fn every_truncation_returns_an_answer() { + let Some(data) = read("libshapes.a") else { + return; + }; + // A step, because a byte at a time over a megabyte is a benchmark. + let step = (data.len() / 512).max(1); + let mut n = 0; + for end in (0..data.len()).step_by(step) { + let a = archive::open(&data[..end]); + if let Ok(a) = a { + // Whatever survived has to be self-consistent. + for m in &a.members { + assert!(m.offset + m.size <= end as u64); + } + } + n += 1; + } + assert!(n > 100, "only {n} truncations tried"); +} + +#[test] +fn corrupt_archives_never_panic_or_hang() { + let Some(data) = read("libshapes.a") else { + return; + }; + let head = &data[..data.len().min(64 * 1024)]; + let mut state = 0x1234_5678_9abc_def1u64; + let mut next = move || { + state ^= state >> 12; + state ^= state << 25; + state ^= state >> 27; + state.wrapping_mul(0x2545_f491_4f6c_dd1d) + }; + let started = std::time::Instant::now(); + for _ in 0..4000 { + let mut v = head.to_vec(); + // Saturate a run, which is how a size or a count becomes enormous. + let at = (next() as usize) % v.len(); + let len = (next() as usize) % 16 + 1; + for b in v.iter_mut().skip(at).take(len) { + *b = if next() & 1 == 0 { 0xff } else { b'9' }; + } + if let Ok(a) = archive::open(&v) { + for i in 0..a.members.len() { + let _ = a.load_member(i, &LoadOptions::default()); + } + } + } + assert!( + started.elapsed() < std::time::Duration::from_secs(30), + "corrupt archives took too long" + ); +} diff --git a/crates/e5r-format/tests/arm_mode.rs b/crates/e5r-format/tests/arm_mode.rs new file mode 100644 index 0000000..85b979d --- /dev/null +++ b/crates/e5r-format/tests/arm_mode.rs @@ -0,0 +1,84 @@ +//! Which instruction set an ARM image holds, which the bytes never say. +//! +//! A32 and T32 share one `e_machine`, so a loader that does not decide reads +//! every Thumb image as A32 and produces plausible nonsense from the first +//! halfword on. Two things decide it, and both are checked here against real +//! images: the low bit of a code address, which is what `bx` reads to switch, +//! and the CPU profile an `.ARM.attributes` section declares. + +use std::path::{Path, PathBuf}; + +use e5r_core::Arch; +use e5r_format::LoadOptions; + +fn build_dir() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn load(name: &str) -> Option { + let data = std::fs::read(build_dir()?.join(name)).ok()?; + e5r_format::load(&data, &LoadOptions::default()).ok() +} + +/// A Thumb object's function symbols carry the Thumb bit, and that is what +/// says the image is Thumb. +#[test] +fn a_thumb_object_is_read_as_thumb() { + for name in [ + "shapes.thumb.O0.o", + "shapes.thumb.O1.o", + "shapes.thumb.O2.o", + ] { + let Some(obj) = load(name) else { + continue; // the corpus has not been built + }; + assert_eq!(obj.arch, Arch::Thumb, "{name}"); + assert_eq!( + obj.metadata.get("arm.mode").map(String::as_str), + Some("thumb") + ); + } +} + +/// And an A32 object is not, which is the half that would pass by accident if +/// the answer were always Thumb. +#[test] +fn an_a32_object_is_read_as_a32() { + for name in ["shapes.arm.O0.o", "shapes.arm.O1.o", "shapes.arm.O2.o"] { + let Some(obj) = load(name) else { + continue; + }; + assert_eq!(obj.arch, Arch::Arm, "{name}"); + assert_eq!( + obj.metadata.get("arm.mode").map(String::as_str), + Some("a32") + ); + } +} + +/// The Thumb bit is not part of the address. +/// +/// Leaving it on puts every function one byte past where it starts, and the +/// decode then runs from the wrong offset for the rest of the file. +#[test] +fn the_thumb_bit_is_not_part_of_a_symbol_address() { + let Some(obj) = load("shapes.thumb.O1.o") else { + return; + }; + let functions: Vec<_> = obj + .symbols + .iter() + .filter(|s| s.kind == e5r_format::SymbolKind::Function) + .collect(); + assert!(!functions.is_empty(), "the fixture names no functions"); + for s in functions { + assert_eq!( + s.addr.get() & 1, + 0, + "{} is at {:#x}, which is not an address", + s.name, + s.addr.get() + ); + } +} diff --git a/crates/e5r-format/tests/dwarf.rs b/crates/e5r-format/tests/dwarf.rs new file mode 100644 index 0000000..1de2593 --- /dev/null +++ b/crates/e5r-format/tests/dwarf.rs @@ -0,0 +1,208 @@ +//! DWARF reading measured against readelf. +//! +//! The oracle is `readelf --debug-dump=info`, which is a different +//! implementation of the same specification. Function names and their entry +//! addresses are what the analysis consumes, so those are what is compared. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_format::LoadOptions; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// Functions readelf says the debug information describes, by address. +fn readelf_functions(path: &Path) -> Option> { + let out = Command::new("readelf") + .arg("--debug-dump=info") + .arg(path) + .output() + .ok()?; + if !out.status.success() { + return None; + } + let text = String::from_utf8_lossy(&out.stdout); + let mut found = BTreeMap::new(); + let mut in_subprogram = false; + let mut name: Option = None; + let mut low: Option = None; + for line in text.lines() { + if line.contains("Abbrev Number:") { + if let (Some(n), Some(a)) = (name.take(), low.take()) { + found.insert(a, n); + } + in_subprogram = line.contains("DW_TAG_subprogram"); + continue; + } + if !in_subprogram { + continue; + } + if let Some(rest) = line.split("DW_AT_name").nth(1) { + // readelf prints the name after a colon, sometimes with an offset + // in parentheses first. + if let Some(v) = rest.rsplit(':').next() { + let v = v.trim(); + if !v.is_empty() { + name = Some(v.to_string()); + } + } + } + if let Some(rest) = line.split("DW_AT_low_pc").nth(1) { + let v = rest.trim().trim_start_matches(':').trim(); + let v = v.trim_start_matches("0x"); + if let Ok(a) = u64::from_str_radix(v, 16) { + low = Some(a); + } + } + } + if let (Some(n), Some(a)) = (name, low) { + found.insert(a, n); + } + Some(found) +} + +fn check(name: &str) { + let Some(dir) = corpus() else { return }; + let path = dir.join(name); + let Ok(data) = std::fs::read(&path) else { + return; + }; + let Ok(obj) = e5r_format::load(&data, &LoadOptions::default()) else { + return; + }; + let Some(debug) = &obj.debug else { + panic!("{name}: no debug information was read at all"); + }; + let Some(expected) = readelf_functions(&path) else { + return; // no readelf here + }; + if expected.is_empty() { + return; + } + + // Names are compared against readelf. Addresses are compared against this + // object's own symbol table, which is the stronger check: a relocatable + // object's debug addresses are written as zero and filled in by + // relocations, so agreeing with the symbol table means the relocations + // were applied correctly. + let ours: BTreeMap = debug + .functions + .values() + .map(|f| (f.name.clone(), f.low_pc.get())) + .collect(); + let symbols: BTreeMap = obj + .symbols + .iter() + .filter(|s| s.kind == e5r_format::SymbolKind::Function) + .map(|s| (s.name.clone(), s.addr.get())) + .collect(); + + let mut missing = Vec::new(); + let mut wrong = Vec::new(); + for expected_name in expected.values() { + let Some(addr) = ours.get(expected_name) else { + missing.push(expected_name.clone()); + continue; + }; + if let Some(from_symbol) = symbols.get(expected_name) { + if from_symbol != addr { + wrong.push(format!( + "{expected_name}: debug says {addr:#x}, the symbol table says {from_symbol:#x}" + )); + } + } + } + assert!( + wrong.is_empty(), + "{name}: {} addresses disagree with the symbol table:\n {}", + wrong.len(), + wrong.join("\n ") + ); + assert!( + missing.is_empty(), + "{name}: {} of {} functions readelf describes were not read: {}", + missing.len(), + expected.len(), + missing.join(", ") + ); + println!( + "{name}: {} functions, {} types, {} line rows", + debug.functions.len(), + debug.types.len(), + debug.lines.len() + ); +} + +#[test] +fn function_names_and_addresses_match_readelf() { + for name in [ + "wide.a64.O0.o", + "wide.a64.O2.o", + "wide.x64.O0.o", + "wide.x64.O2.o", + "shapes.a64.O0.o", + "hello.a64.O0", + "hello.a64.O2", + ] { + check(name); + } +} + +#[test] +fn the_types_of_a_known_function_are_read() { + let Some(dir) = corpus() else { return }; + let Ok(data) = std::fs::read(dir.join("wide.a64.O0.o")) else { + return; + }; + let Ok(obj) = e5r_format::load(&data, &LoadOptions::default()) else { + return; + }; + let Some(debug) = &obj.debug else { return }; + let f = debug + .functions + .values() + .find(|f| f.name == "use_struct") + .expect("use_struct is in the debug information"); + // `i64 use_struct(struct Point *p, int n)`. + assert_eq!(f.signature.parameters.len(), 2); + let (name, ty) = &f.signature.parameters[0]; + assert_eq!(name.as_deref(), Some("p")); + assert_eq!(debug.types.name_of(*ty), "struct Point *"); + let (name, ty) = &f.signature.parameters[1]; + assert_eq!(name.as_deref(), Some("n")); + assert_eq!(debug.types.size_of(*ty), Some(4)); + + // And the structure it points at has the three fields, at their offsets. + let pointer = debug.types.resolve(f.signature.parameters[0].1); + let e5r_types::ctype::Type::Pointer(inner) = debug.types.get(pointer).unwrap() else { + panic!("the first parameter is a pointer"); + }; + let field = debug.types.field_at(*inner, 8).expect("a field at eight"); + assert_eq!(field.0.name, "tag"); +} + +#[test] +fn the_line_table_locates_a_function() { + let Some(dir) = corpus() else { return }; + let Ok(data) = std::fs::read(dir.join("wide.a64.O0.o")) else { + return; + }; + let Ok(obj) = e5r_format::load(&data, &LoadOptions::default()) else { + return; + }; + let Some(debug) = &obj.debug else { return }; + let f = debug + .functions + .values() + .find(|f| f.name == "collatz") + .expect("collatz is in the debug information"); + let row = debug + .line_for(f.low_pc) + .expect("the line table covers the function's entry"); + assert!(row.file.contains("wide.c"), "file was {:?}", row.file); + assert!(row.line > 100, "line was {}", row.line); +} diff --git a/crates/e5r-format/tests/dwarf_frames.rs b/crates/e5r-format/tests/dwarf_frames.rs new file mode 100644 index 0000000..9e188a1 --- /dev/null +++ b/crates/e5r-format/tests/dwarf_frames.rs @@ -0,0 +1,572 @@ +//! Inlined frames, call sites and location lists, measured against readelf. +//! +//! The comparison is entry for entry rather than by count: every +//! `DW_TAG_inlined_subroutine` readelf prints has to appear in the right +//! function with the same call line and the same address ranges, every +//! `DW_TAG_call_site` with the same return address and the same number of +//! described arguments, and every variable whose location is a list has to +//! carry exactly the ranges the list section holds. +//! +//! The oracle is a different implementation of the same specification, which +//! is the only kind of agreement worth anything here. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_format::LoadOptions; +use e5r_format::dwarf::DebugInfo; + +/// The fixtures that carry inlining, call sites and location lists. Two +/// producers: gcc writes the ranges as section offsets, clang reaches them by +/// index through a base, and the two paths share no code. +const FIXTURES: [&str; 9] = [ + "hello.a64.O2", + "wide.a64.O2.o", + "wide.a64.O3.o", + "shapes.a64.O2.o", + "wide.x64.O2.o", + "shapes.x64.O2.o", + // DWARF 4: `.debug_ranges` and `.debug_loc`, and the GNU call site tags. + "hello.a64.dwarf4", + "wide.a64.dwarf4.o", + "shapes.a64.dwarf4.o", +]; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn readelf(path: &Path, what: &str) -> Option { + let out = Command::new("readelf") + .arg(format!("--debug-dump={what}")) + .arg(path) + .output() + .ok()?; + out.status + .success() + .then(|| String::from_utf8_lossy(&out.stdout).into_owned()) +} + +/// One entry of a list section as readelf prints it. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Entry { + Range(u64, u64), + /// A base address, which is an entry of the list and therefore a place a + /// list can start, but not a range. + Base, + End, +} + +/// The list sections, flattened to entries in file order with the offset each +/// one sits at. A list is then every entry from its own offset to the next +/// end marker, which is how both formats delimit one. +/// +/// readelf prints the addresses of a range on a continuation line when the +/// producer emitted GNU location views, so the offset carries over from the +/// last line that started with one. +fn list_entries(text: &str) -> Vec<(u64, Entry)> { + let mut out = Vec::new(); + let mut offset = None; + for line in text.lines() { + let tokens: Vec<&str> = line.split_whitespace().collect(); + let Some(first) = tokens.first() else { + continue; + }; + if first.len() == 8 && first.chars().all(|c| c.is_ascii_hexdigit()) { + offset = u64::from_str_radix(first, 16).ok(); + } + let Some(at) = offset else { continue }; + if line.contains("") { + out.push((at, Entry::End)); + continue; + } + // Exactly sixteen hex digits: a 64-bit address as readelf prints one. + // Anything shorter is an offset or a view number, which is what makes + // this safe to run over every line. + let wide = |t: &&str| t.len() == 16 && t.chars().all(|c| c.is_ascii_hexdigit()); + if line.contains("(base address)") { + out.push((at, Entry::Base)); + continue; + } + if let Some(i) = tokens.windows(2).position(|w| wide(&w[0]) && wide(&w[1])) { + let begin = u64::from_str_radix(tokens[i], 16).unwrap_or(0); + let end = u64::from_str_radix(tokens[i + 1], 16).unwrap_or(0); + out.push((at, Entry::Range(begin, end))); + } + } + out +} + +/// The ranges of the list that starts at `at`, empty ones dropped: an entry +/// whose start equals its end covers no address and is not a range. +fn list_at(entries: &[(u64, Entry)], at: u64) -> Vec<(u64, u64)> { + let Some(start) = entries.iter().position(|(o, _)| *o == at) else { + return Vec::new(); + }; + let mut out = Vec::new(); + for (_, e) in &entries[start..] { + match e { + Entry::End => break, + Entry::Base => {} + Entry::Range(b, n) if b != n => out.push((*b, *n)), + Entry::Range(_, _) => {} + } + } + out +} + +/// One debugging information entry, as readelf printed it. +#[derive(Debug, Clone, Default)] +struct Die { + tag: String, + /// Offset in `.debug_info`, which is how one entry references another. + offset: u64, + parent: Option, + attributes: BTreeMap, +} + +impl Die { + /// The trailing hex number of an attribute readelf printed, which is where + /// it puts the value whether or not the form indirected to get there. + fn hex(&self, name: &str) -> Option { + let v = self.attributes.get(name)?; + let last = v.split_whitespace().next_back()?; + u64::from_str_radix(last.trim_start_matches("0x").trim_end_matches(':'), 16).ok() + } + + /// The leading hex number, for an attribute readelf follows with a note + /// of its own: a location list offset prints as `0x12 (location list)`. + fn hex_first(&self, name: &str) -> Option { + let v = self.attributes.get(name)?; + let first = v.split_whitespace().next()?; + u64::from_str_radix(first.trim_start_matches("0x"), 16).ok() + } + + fn number(&self, name: &str) -> Option { + let v = self.attributes.get(name)?; + v.split_whitespace().next()?.parse().ok() + } + + fn text(&self, name: &str) -> Option<&str> { + Some( + self.attributes + .get(name)? + .rsplit(american_colon) + .next()? + .trim(), + ) + } +} + +/// readelf separates an attribute's decoration from its value with a colon, +/// and a name never contains one. +fn american_colon(c: char) -> bool { + c == ':' +} + +/// Parse `readelf --debug-dump=info` into a tree. +fn dies(text: &str) -> Vec { + let mut out: Vec = Vec::new(); + let mut stack: Vec = Vec::new(); + for line in text.lines() { + if let Some(rest) = line.split("Abbrev Number:").nth(1) { + // ` : Abbrev Number: n (DW_TAG_x)` + let head = line.split_whitespace().next().unwrap_or_default(); + let depth = head + .trim_start_matches('<') + .split('>') + .next() + .and_then(|d| d.parse::().ok()); + let Some(depth) = depth else { continue }; + let offset = head + .rsplit('<') + .next() + .and_then(|o| u64::from_str_radix(o.trim_end_matches(">:"), 16).ok()) + .unwrap_or_default(); + let Some(tag) = rest.split('(').nth(1).and_then(|t| t.split(')').next()) else { + // A zero abbreviation closes a sibling chain and names no tag. + stack.truncate(depth); + continue; + }; + stack.truncate(depth); + let parent = stack.last().copied(); + out.push(Die { + tag: tag.to_string(), + offset, + parent, + attributes: BTreeMap::new(), + }); + stack.push(out.len() - 1); + continue; + } + let Some(at) = line.find("DW_AT_") else { + continue; + }; + let rest = &line[at..]; + let Some((name, value)) = rest.split_once(':') else { + continue; + }; + if let Some(die) = stack.last().and_then(|i| out.get_mut(*i)) { + die.attributes + .insert(name.trim().to_string(), value.trim().to_string()); + } + } + out +} + +/// The name a DIE carries, following the abstract origin an inlined copy has +/// instead of a name of its own. The reader has to do the same, and a name +/// that only the origin carries is the case that matters. +fn name_of(dies: &[Die], at: usize) -> Option { + let by_offset: BTreeMap = dies + .iter() + .enumerate() + .map(|(i, d)| (d.offset, i)) + .collect(); + let mut at = Some(at); + for _ in 0..8 { + let die = dies.get(at?)?; + if let Some(name) = die.text("DW_AT_name") { + return Some(name.to_string()); + } + let next = die + .hex("DW_AT_abstract_origin") + .or_else(|| die.hex("DW_AT_specification"))?; + at = by_offset.get(&next).copied(); + } + None +} + +/// The nearest enclosing subprogram of a DIE, and its entry address. +fn enclosing_function(dies: &[Die], mut at: usize) -> Option { + for _ in 0..64 { + let die = dies.get(at)?; + if die.tag == "DW_TAG_subprogram" { + return die.hex("DW_AT_low_pc"); + } + at = die.parent?; + } + None +} + +/// Address ranges a DIE covers, resolved the way the reader has to resolve +/// them: through the range list section, or from the contiguous pair. +fn die_ranges(die: &Die, ranges: &[(u64, Entry)]) -> Option> { + if let Some(at) = die.hex("DW_AT_ranges") { + return Some(list_at(ranges, at)); + } + let low = die.hex("DW_AT_low_pc")?; + let high = die.hex("DW_AT_high_pc")?; + // DWARF 4 and 5 write the second as a length. + let end = if high > low { high } else { low + high }; + (end > low).then(|| vec![(low, end)]) +} + +/// Where the loader put the image relative to the addresses readelf prints. +/// +/// A relocatable object has no load address of its own, so the loader gives it +/// one and every address the reader reports moves with it. Recovered by +/// agreeing on the functions both sides name, rather than assumed. +fn rebase(dies: &[Die], debug: &DebugInfo) -> u64 { + let mut seen: Vec = Vec::new(); + for die in dies.iter().filter(|d| d.tag == "DW_TAG_subprogram") { + let (Some(low), Some(name)) = (die.hex("DW_AT_low_pc"), die.text("DW_AT_name")) else { + continue; + }; + for (addr, f) in &debug.functions { + if f.name == name { + seen.push(addr.get().wrapping_sub(low)); + } + } + } + seen.sort_unstable(); + seen.dedup(); + match seen.as_slice() { + [one] => *one, + // Nothing to go on, or two answers: zero is then the honest guess and + // the comparison below fails loudly rather than quietly passing. + _ => 0, + } +} + +fn open(dir: &Path, name: &str) -> Option<(PathBuf, DebugInfo)> { + let path = dir.join(name); + let data = std::fs::read(&path).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some((path, obj.debug?)) +} + +#[test] +fn inlined_frames_match_readelf() { + let Some(dir) = corpus() else { return }; + let mut checked = 0usize; + let mut files = 0usize; + for name in FIXTURES { + let Some((path, debug)) = open(&dir, name) else { + continue; + }; + let (Some(info), Some(ranges)) = (readelf(&path, "info"), readelf(&path, "Ranges")) else { + return; // no readelf here + }; + let dies = dies(&info); + let ranges = list_entries(&ranges); + let base = rebase(&dies, &debug); + let mut expected = 0usize; + for (i, die) in dies.iter().enumerate() { + if die.tag != "DW_TAG_inlined_subroutine" { + continue; + } + let Some(function) = enclosing_function(&dies, i) else { + continue; + }; + let Some(want) = die_ranges(die, &ranges) else { + continue; + }; + let want: Vec<(u64, u64)> = want + .into_iter() + .map(|(b, e)| (b.wrapping_add(base), e.wrapping_add(base))) + .collect(); + if want.is_empty() { + continue; + } + expected += 1; + let function = function.wrapping_add(base); + let f = debug + .functions + .get(&e5r_core::Addr(function)) + .unwrap_or_else(|| panic!("{name}: no function at {function:#x}")); + let ours: Vec> = f + .inlines + .iter() + .map(|i| { + i.ranges + .iter() + .map(|r| (r.start().get(), r.end().get())) + .collect() + }) + .collect(); + assert!( + ours.contains(&want), + "{name}: the inlined frame at {want:#x?} in the function at {function:#x} \ + is not among the {} we read: {ours:#x?}", + ours.len() + ); + // The call site, which is the whole point of recovering the frame. + let found = f + .inlines + .iter() + .find(|f| { + f.ranges + .iter() + .map(|r| (r.start().get(), r.end().get())) + .eq(want.iter().copied()) + }) + .unwrap(); + assert_eq!( + found.call_line.map(u64::from), + die.number("DW_AT_call_line"), + "{name}: call line of the frame at {want:#x?}" + ); + assert_eq!( + found.call_column.map(u64::from), + die.number("DW_AT_call_column"), + "{name}: call column of the frame at {want:#x?}" + ); + checked += 1; + } + let ours: usize = debug.functions.values().map(|f| f.inlines.len()).sum(); + assert_eq!( + ours, expected, + "{name}: readelf describes {expected} inlined frames and we read {ours}" + ); + files += 1; + } + assert!(checked > 0, "no fixture carried an inlined frame"); + println!("inlined frames: {checked} compared against readelf over {files} files"); +} + +#[test] +fn call_sites_match_readelf() { + let Some(dir) = corpus() else { return }; + let mut checked = 0usize; + for name in FIXTURES { + let Some((path, debug)) = open(&dir, name) else { + continue; + }; + let Some(info) = readelf(&path, "info") else { + return; + }; + let dies = dies(&info); + let base = rebase(&dies, &debug); + // (function, return pc) -> how many arguments were described. + let mut expected: BTreeMap<(u64, u64), usize> = BTreeMap::new(); + for (i, die) in dies.iter().enumerate() { + let gnu = die.tag == "DW_TAG_GNU_call_site"; + if die.tag != "DW_TAG_call_site" && !gnu { + continue; + } + let Some(function) = enclosing_function(&dies, i) else { + continue; + }; + let return_pc = if gnu { + die.hex("DW_AT_low_pc") + } else { + die.hex("DW_AT_call_return_pc") + }; + let Some(return_pc) = return_pc else { continue }; + let parameters = dies + .iter() + .filter(|c| { + c.parent == Some(i) + && (c.tag == "DW_TAG_call_site_parameter" + || c.tag == "DW_TAG_GNU_call_site_parameter") + }) + .count(); + expected.insert( + (function.wrapping_add(base), return_pc.wrapping_add(base)), + parameters, + ); + } + let mut ours: BTreeMap<(u64, u64), usize> = BTreeMap::new(); + for (addr, f) in &debug.functions { + for c in &f.call_sites { + let Some(pc) = c.return_pc else { continue }; + ours.insert((addr.get(), pc.get()), c.parameters.len()); + } + } + assert_eq!( + ours, expected, + "{name}: the call sites disagree with readelf" + ); + checked += expected.len(); + } + assert!(checked > 0, "no fixture carried a call site"); + println!("call sites: {checked} compared against readelf, argument counts included"); +} + +#[test] +fn location_lists_match_readelf() { + let Some(dir) = corpus() else { return }; + let mut checked = 0usize; + for name in FIXTURES { + let Some((path, debug)) = open(&dir, name) else { + continue; + }; + let (Some(info), Some(locations)) = (readelf(&path, "info"), readelf(&path, "loc")) else { + return; + }; + let dies = dies(&info); + let locations = list_entries(&locations); + let base = rebase(&dies, &debug); + for (i, die) in dies.iter().enumerate() { + if die.tag != "DW_TAG_formal_parameter" && die.tag != "DW_TAG_variable" { + continue; + } + // Only the list form: a single expression is a different path. + let Some(value) = die.attributes.get("DW_AT_location") else { + continue; + }; + if !value.contains("location list") { + continue; + } + let Some(at) = die.hex_first("DW_AT_location") else { + continue; + }; + // Named, and a direct child of something the reader collects + // locals from: a variable inside a lexical block is not read. + let Some(variable) = name_of(&dies, i) else { + continue; + }; + let parent = die.parent.and_then(|p| dies.get(p)); + let direct = parent.is_some_and(|p| { + p.tag == "DW_TAG_subprogram" || p.tag == "DW_TAG_inlined_subroutine" + }); + if !direct { + continue; + } + let Some(function) = enclosing_function(&dies, i) else { + continue; + }; + let Some(f) = debug + .functions + .get(&e5r_core::Addr(function.wrapping_add(base))) + else { + continue; + }; + let want: Vec<(u64, u64)> = list_at(&locations, at) + .into_iter() + .map(|(b, e)| (b.wrapping_add(base), e.wrapping_add(base))) + .collect(); + if want.is_empty() { + continue; + } + let candidates: Vec<&e5r_format::dwarf::DebugLocal> = f + .locals + .iter() + .chain(f.inlines.iter().flat_map(|i| i.locals.iter())) + .filter(|l| l.name == variable) + .collect(); + let ours: Vec> = candidates + .iter() + .map(|l| { + l.locations + .iter() + .map(|e| (e.range.start().get(), e.range.end().get())) + .collect() + }) + .collect(); + assert!( + ours.contains(&want), + "{name}: the location list at {at:#x} for the {} {variable:?} is \ + {want:#x?}, and we read {ours:#x?}", + die.tag, + ); + checked += 1; + } + } + assert!(checked > 0, "no fixture carried a location list"); + println!("location lists: {checked} variables compared against readelf, range for range"); +} + +#[test] +fn a_variable_that_moves_is_reported_where_it_is() { + // The property the whole feature exists for: at -O2 a value lives in a + // register for part of a function and somewhere else for the rest, and a + // single-location reader is wrong for most of it. + let Some(dir) = corpus() else { return }; + let mut moved = 0usize; + for name in FIXTURES { + let Some((_, debug)) = open(&dir, name) else { + continue; + }; + for f in debug.functions.values() { + for local in &f.locals { + let mut places: Vec<&e5r_format::dwarf::Location> = + local.locations.iter().map(|e| &e.location).collect(); + places.dedup(); + if places.len() < 2 { + continue; + } + moved += 1; + // Every range reports the place that range says, and nothing + // reports a place outside its own range. + for entry in &local.locations { + assert_eq!( + local.location_at(entry.range.start()), + Some(&entry.location), + "{name}: {} at {:#x}", + local.name, + entry.range.start() + ); + } + } + } + } + assert!( + moved > 0, + "no fixture had a variable that changes location, so this proves nothing" + ); + println!("{moved} variables change storage inside their function"); +} diff --git a/crates/e5r-format/tests/dwarf_fuzz.rs b/crates/e5r-format/tests/dwarf_fuzz.rs new file mode 100644 index 0000000..e015c15 --- /dev/null +++ b/crates/e5r-format/tests/dwarf_fuzz.rs @@ -0,0 +1,139 @@ +//! Hostile DWARF: the list sections, corrupted on purpose. +//! +//! `fuzz.rs` mutates whole files at random, which reaches the debug sections +//! only by luck. The range and location lists are walked by a length and a +//! kind byte that both come from the file, and a base address that also comes +//! from the file, so they get their own sweep: every byte of every list +//! section set to the two values that break arithmetic, and the whole file +//! truncated at every step. The contract is the one the crate promises +//! everywhere: a value or an error, never a panic, a hang, or an allocation +//! that follows a number the file chose. + +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +use e5r_format::{LoadOptions, load}; + +/// Sections whose contents the new readers walk. +const TARGETS: [&str; 6] = [ + ".debug_info", + ".debug_abbrev", + ".debug_rnglists", + ".debug_loclists", + ".debug_line", + ".debug_str_offsets", +]; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// Where each target section's bytes are in the file, found by loading it once. +fn regions(bytes: &[u8]) -> Vec<(String, u64, u64)> { + let Ok(obj) = load(bytes, &LoadOptions::default()) else { + return Vec::new(); + }; + obj.sections + .iter() + .filter(|s| TARGETS.contains(&s.name.as_str()) && s.file_size > 0) + .map(|s| (s.name.clone(), s.file_offset, s.file_size)) + .collect() +} + +#[test] +fn corrupting_a_debug_section_never_panics_or_hangs() { + let Some(dir) = corpus() else { return }; + let opts = LoadOptions::default(); + let mut cases = 0u64; + let started = Instant::now(); + for name in ["hello.a64.O2", "wide.a64.O2.o", "wide.x64.O2.o"] { + let Ok(full) = std::fs::read(dir.join(name)) else { + continue; + }; + let regions = regions(&full); + assert!( + !regions.is_empty(), + "{name} has no debug sections to corrupt" + ); + for (_, offset, size) in regions { + // Every byte would be a throughput test; every seventh reaches + // each field of every record without being one. + for i in (offset..offset + size).step_by(7) { + for value in [0xffu8, 0x00, 0x80] { + let mut bad = full.clone(); + if let Some(b) = bad.get_mut(i as usize) { + *b = value; + } + let _ = load(&bad, &opts); + cases += 1; + } + } + } + } + assert!(cases > 100, "only {cases} cases, so this proves nothing"); + assert!( + started.elapsed() < Duration::from_secs(60), + "{cases} corrupted debug sections took {:?}", + started.elapsed() + ); + println!("dwarf fuzz: {cases} corrupted debug sections, no panics"); +} + +#[test] +fn a_truncated_debug_section_stops_rather_than_guessing() { + let Some(dir) = corpus() else { return }; + let opts = LoadOptions::default(); + let started = Instant::now(); + let mut cases = 0u64; + for name in ["hello.a64.O2", "wide.a64.O2.o"] { + let Ok(full) = std::fs::read(dir.join(name)) else { + continue; + }; + for cut in (0x40..full.len()).step_by(97) { + let _ = load(&full[..cut], &opts); + cases += 1; + } + } + assert!(cases > 50); + assert!( + started.elapsed() < Duration::from_secs(30), + "{cases} truncations took {:?}", + started.elapsed() + ); +} + +/// A range list whose entries claim to run off the end of the section. +/// +/// Built rather than mutated: the interesting case is a well-formed unit +/// header followed by a list that lies, which a random flip rarely produces. +#[test] +fn a_range_list_that_runs_off_the_end_is_bounded() { + let Some(dir) = corpus() else { return }; + let Ok(full) = std::fs::read(dir.join("hello.a64.O2")) else { + return; + }; + let Some((_, offset, size)) = regions(&full) + .into_iter() + .find(|(name, _, size)| name == ".debug_rnglists" && *size > 16) + else { + return; + }; + let started = Instant::now(); + // Fill the whole list with start-length entries whose length is the + // largest a uleb128 can spell, which is what an unchecked reader adds to + // a base address and then trusts. + let mut bad = full.clone(); + let body = &mut bad[offset as usize..(offset + size) as usize]; + let mut i = 12; // past the section header + while i + 10 <= body.len() { + body[i] = 0x07; // DW_RLE_start_length + for b in &mut body[i + 1..i + 9] { + *b = 0xff; + } + body[i + 9] = 0x7f; // a one-byte uleb128 of 127 + i += 10; + } + let _ = load(&bad, &LoadOptions::default()); + assert!(started.elapsed() < Duration::from_secs(5)); +} diff --git a/crates/e5r-format/tests/elf_relocs_i386.rs b/crates/e5r-format/tests/elf_relocs_i386.rs new file mode 100644 index 0000000..0174ee2 --- /dev/null +++ b/crates/e5r-format/tests/elf_relocs_i386.rs @@ -0,0 +1,1072 @@ +//! i386 relocations, against readelf entry for entry. +//! +//! The oracle is not a count. For every relocation `readelf -r` lists, this +//! recomputes the psABI formula from `readelf -s` and the addresses the loader +//! chose, then reads back the bytes the loader wrote and demands they agree. +//! A count would pass while every displacement was off by the addend, which is +//! precisely the mistake `REL` invites: i386 has no `r_addend` field, so the +//! addend is whatever the assembler already encoded at the place. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, Instant}; + +use e5r_core::{Addr, Arch, Bits}; +use e5r_format::{LoadOptions, Object, load}; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option<(Object, Vec)> { + let p = corpus()?.join(name); + let data = std::fs::read(&p).ok()?; + let obj = load(&data, &LoadOptions::default()) + .unwrap_or_else(|e| panic!("loading {}: {e}", p.display())); + Some((obj, data)) +} + +fn tool(name: &str, fixture: &str, args: &[&str]) -> Option { + let p = corpus()?.join(fixture); + let out = Command::new(name).args(args).arg(&p).output().ok()?; + out.status + .success() + .then(|| String::from_utf8_lossy(&out.stdout).into_owned()) +} + +/// The fixtures this file is about. Both memory models, because the relocation +/// types barely overlap between them. +const FIXTURES: [&str; 4] = [ + "relocs32.nopic.O0.o", + "relocs32.nopic.O2.o", + "relocs32.pic.O0.o", + "relocs32.pic.O2.o", +]; + +/// One row of `readelf -s`: what the symbol table says about a symbol. +struct OracleSym { + /// `st_value`, before any layout is applied. + value: u64, + /// The section index, or `None` for UND, ABS and COMMON. + section: Option, + name: String, +} + +fn oracle_symbols(fixture: &str) -> HashMap { + let mut out = HashMap::new(); + let Some(txt) = tool("readelf", fixture, &["-sW"]) else { + return out; + }; + let mut in_symtab = false; + for line in txt.lines() { + if line.contains("Symbol table") { + in_symtab = line.contains(".symtab"); + continue; + } + if !in_symtab { + continue; + } + // " 12: 00000000 17 FUNC GLOBAL DEFAULT 2 leaf" + let Some((idx, rest)) = line.trim().split_once(':') else { + continue; + }; + let Ok(index) = idx.trim().parse::() else { + continue; + }; + let f: Vec<&str> = rest.split_whitespace().collect(); + if f.len() < 6 { + continue; + } + let Ok(value) = u64::from_str_radix(f[0], 16) else { + continue; + }; + let section = f[5].parse::().ok(); + out.insert( + index, + OracleSym { + value, + section, + name: f.get(6).unwrap_or(&"").to_string(), + }, + ); + } + out +} + +/// One row of `readelf -r`, with the section the entries apply to. +struct OracleRel { + /// The section the relocations patch, as readelf names the table. + applies_to: String, + offset: u64, + kind: u64, + symbol: u64, + type_name: String, +} + +fn oracle_relocations(fixture: &str) -> Vec { + let mut out = Vec::new(); + let Some(txt) = tool("readelf", fixture, &["-rW"]) else { + return out; + }; + let mut applies_to = String::new(); + for line in txt.lines() { + if let Some(rest) = line.strip_prefix("Relocation section '") { + // "Relocation section '.rel.text' at offset 0x664 contains 6 ..." + let table = rest.split('\'').next().unwrap_or(""); + applies_to = table + .trim_start_matches(".rel") + .trim_start_matches('a') + .to_string(); + continue; + } + let f: Vec<&str> = line.split_whitespace().collect(); + if f.len() < 3 || !f[1].chars().all(|c| c.is_ascii_hexdigit()) { + continue; + } + let (Ok(offset), Ok(info)) = (u64::from_str_radix(f[0], 16), u64::from_str_radix(f[1], 16)) + else { + continue; + }; + if !f[2].starts_with("R_386_") { + continue; + } + out.push(OracleRel { + applies_to: applies_to.clone(), + offset, + kind: info & 0xff, + symbol: info >> 8, + type_name: f[2].to_string(), + }); + } + out +} + +/// Where the loader put the section readelf refers to by index. +fn base_of_index(obj: &Object, index: usize) -> Option { + obj.sections + .get(index) + .filter(|s| !s.range.is_empty()) + .map(|s| s.range.start().get()) +} + +fn read_u32(obj: &Object, at: u64) -> Option { + let b = obj.memory.slice(Addr(at), 4)?; + Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) +} + +/// The addend the assembler encoded, read out of the file rather than out of +/// the relocated image. +fn file_addend(obj: &Object, data: &[u8], section: &str, offset: u64) -> Option { + let s = obj.section(section)?; + let at = usize::try_from(s.file_offset + offset).ok()?; + let b = data.get(at..at + 4)?; + Some(i32::from_le_bytes([b[0], b[1], b[2], b[3]])) +} + +#[test] +fn every_relocation_matches_readelf_entry_for_entry() { + let Some(_) = corpus() else { return }; + let mut total = 0usize; + for fixture in FIXTURES { + let Some((obj, data)) = open(fixture) else { + continue; + }; + assert_eq!(obj.arch, Arch::X86, "{fixture}: not an i386 object"); + assert_eq!(obj.bits, Bits::Bits32); + let syms = oracle_symbols(fixture); + let rels = oracle_relocations(fixture); + if syms.is_empty() || rels.is_empty() { + // No readelf here; the gate is skipped rather than faked. + return; + } + let got = obj + .metadata + .get("elf.got_base") + .map(|v| u64::from_str_radix(v.trim_start_matches("0x"), 16).unwrap()); + + let mut checked = 0usize; + for rel in &rels { + // Debug sections are relocated into a private copy for the DWARF + // reader, not into the image, so there is nothing to read back. + if rel.applies_to.starts_with(".debug") || rel.applies_to.is_empty() { + continue; + } + let Some(sec) = obj.section(&rel.applies_to) else { + continue; + }; + if sec.range.is_empty() { + continue; + } + let place = sec.range.start().get() + rel.offset; + let addend = + file_addend(&obj, &data, &rel.applies_to, rel.offset).unwrap_or_else(|| { + panic!( + "{fixture}: no bytes at {}+{:#x}", + rel.applies_to, rel.offset + ) + }) as i64 as u64; + let sym = syms + .get(&rel.symbol) + .unwrap_or_else(|| panic!("{fixture}: readelf lists no symbol {}", rel.symbol)); + let defined = sym.section.and_then(|i| base_of_index(&obj, i)); + let s = defined.map(|b| b + sym.value); + let actual = read_u32(&obj, place) + .unwrap_or_else(|| panic!("{fixture}: {place:#x} is not mapped")); + + let want: u64 = match rel.kind { + // R_386_32: S + A. + 1 => match s { + Some(s) => s.wrapping_add(addend), + // Undefined: the loader must leave what was there, not + // write a confident zero. + None => addend, + }, + // R_386_PC32 and R_386_PLT32: S + A - P. + 2 | 4 => match s { + Some(s) => s.wrapping_add(addend).wrapping_sub(place), + None => addend, + }, + // R_386_GOTOFF: S + A - GOT. + 9 => { + let got = got.expect("a GOTOFF relocation but no GOT base"); + s.expect("GOTOFF against an undefined symbol") + .wrapping_add(addend) + .wrapping_sub(got) + } + // R_386_GOTPC: GOT + A - P. + 10 => got + .expect("a GOTPC relocation but no GOT base") + .wrapping_add(addend) + .wrapping_sub(place), + // R_386_GOT32 and R_386_GOT32X write a slot offset, whose + // value is checked through the slot below rather than here. + 3 | 43 => { + let got = got.expect("a GOT32 relocation but no GOT base"); + let slot = got.wrapping_add(actual as u64).wrapping_sub(addend); + let held = read_u32(&obj, slot) + .unwrap_or_else(|| panic!("{fixture}: GOT slot {slot:#x} is not mapped")); + assert_eq!( + held as u64, + s.unwrap_or(0), + "{fixture}: {} at {:#x} names {}, whose GOT slot holds the wrong address", + rel.type_name, + rel.offset, + sym.name + ); + checked += 1; + continue; + } + other => panic!("{fixture}: unexpected relocation type {other} in a fixture"), + }; + assert_eq!( + actual as u64, want as u32 as u64, + "{fixture}: {} at {}+{:#x} against {} (place {place:#x}, addend {:#x})", + rel.type_name, rel.applies_to, rel.offset, sym.name, addend as u32 + ); + checked += 1; + } + assert!( + checked >= 5, + "{fixture}: only {checked} relocations compared against readelf" + ); + total += checked; + } + assert!(total >= 20, "only {total} relocations compared in total"); +} + +/// The implicit addend is the whole difference between `REL` and `RELA`, so it +/// gets its own assertion: the fixtures really do carry non-zero addends, and +/// ignoring them would change the answer. +#[test] +fn the_implicit_addend_is_not_zero_and_is_used() { + let Some(_) = corpus() else { return }; + let Some((obj, data)) = open("relocs32.nopic.O2.o") else { + return; + }; + let rels = oracle_relocations("relocs32.nopic.O2.o"); + if rels.is_empty() { + return; + } + let mut nonzero = 0; + for rel in rels.iter().filter(|r| r.applies_to == ".text") { + let a = file_addend(&obj, &data, ".text", rel.offset).unwrap(); + if a != 0 { + nonzero += 1; + } + } + assert!( + nonzero > 0, + "no relocation in .text carries an implicit addend; the fixture proves nothing" + ); +} + +/// The call that stays inside the file must land on the function it names. +#[test] +fn an_intra_file_call_reaches_its_target() { + let Some(_) = corpus() else { return }; + for fixture in FIXTURES { + let Some((obj, _)) = open(fixture) else { + continue; + }; + let Some(leaf) = obj.symbols.iter().find(|s| s.name == "leaf") else { + continue; + }; + let Some(text) = obj.section(".text") else { + continue; + }; + // Find the `e8 rel32` whose target is `leaf`. Before relocations were + // applied every one of these pointed at the byte after itself. + let bytes = obj + .memory + .slice(text.range.start(), text.range.len()) + .expect(".text is not mapped"); + let mut hit = false; + for (i, w) in bytes.windows(5).enumerate() { + if w[0] != 0xe8 { + continue; + } + let disp = i32::from_le_bytes([w[1], w[2], w[3], w[4]]) as i64; + let target = text.range.start().get() as i64 + i as i64 + 5 + disp; + if target as u64 == leaf.addr.get() { + hit = true; + } + } + assert!( + hit, + "{fixture}: no call reaches leaf at {:#x}", + leaf.addr.get() + ); + } +} + +/// A call out of the file has no address here. It must stay as the compiler +/// left it rather than be relocated to point at address zero. +#[test] +fn an_external_call_is_left_alone_rather_than_pointed_at_zero() { + let Some(_) = corpus() else { return }; + let Some((obj, data)) = open("relocs32.nopic.O2.o") else { + return; + }; + let syms = oracle_symbols("relocs32.nopic.O2.o"); + let rels = oracle_relocations("relocs32.nopic.O2.o"); + if syms.is_empty() || rels.is_empty() { + return; + } + let mut checked = 0; + for rel in rels.iter().filter(|r| r.applies_to == ".text") { + let Some(sym) = syms.get(&rel.symbol) else { + continue; + }; + if sym.section.is_some() { + continue; + } + let sec = obj.section(".text").unwrap(); + let place = sec.range.start().get() + rel.offset; + let before = file_addend(&obj, &data, ".text", rel.offset).unwrap(); + assert_eq!( + read_u32(&obj, place).unwrap() as i32, + before, + "{} against the undefined {} was patched anyway", + rel.type_name, + sym.name + ); + checked += 1; + } + assert!(checked > 0, "the fixture has no undefined symbol to check"); + assert!( + obj.metadata.contains_key("elf.relocations.unresolved"), + "unresolved relocations were not reported" + ); +} + +/// Nothing may be dropped without being counted. +#[test] +fn what_is_not_applied_is_reported_by_type() { + let Some(_) = corpus() else { return }; + for fixture in FIXTURES { + let Some((obj, _)) = open(fixture) else { + continue; + }; + let applied: u64 = obj + .metadata + .get("elf.relocations.applied") + .expect("no applied count") + .parse() + .unwrap(); + assert!(applied > 0, "{fixture}: nothing was applied"); + // These fixtures use only implemented types, so the unhandled list + // must be absent rather than merely small. + assert!( + !obj.metadata.contains_key("elf.relocations.unhandled"), + "{fixture}: {:?}", + obj.metadata.get("elf.relocations.unhandled") + ); + } +} + +/// Thread-local relocations are the ones deliberately left alone, and leaving +/// them alone has to be visible. +#[test] +fn tls_relocations_are_recorded_as_unhandled() { + let obj = load(&synth::tls_object(), &LoadOptions::default()).expect("load"); + let listed = obj + .metadata + .get("elf.relocations.unhandled") + .expect("a TLS relocation was dropped silently"); + // R_386_TLS_LE is 17, R_386_TLS_IE_32 is 33. + assert!(listed.contains("type 17"), "{listed}"); + assert!(listed.contains("type 33"), "{listed}"); + assert!( + obj.warnings.iter().any(|w| w.contains("not applied")), + "{:?}", + obj.warnings + ); +} + +/// The debug sections are relocated into a private copy for the DWARF reader, +/// and they are `REL` tables too. Without them every function in the unit +/// resolves to the same address. +#[test] +fn debug_information_agrees_with_the_symbol_table() { + let Some(_) = corpus() else { return }; + for fixture in FIXTURES { + let Some((obj, _)) = open(fixture) else { + continue; + }; + let Some(debug) = &obj.debug else { + panic!("{fixture}: no debug information was read at all"); + }; + let mut checked = 0; + for f in debug.functions.values() { + let Some(sym) = obj + .symbols + .iter() + .find(|s| s.name == f.name && s.kind == e5r_format::SymbolKind::Function) + else { + continue; + }; + assert_eq!( + f.low_pc, + sym.addr, + "{fixture}: DWARF puts {} at {:#x}, the symbol table at {:#x}", + f.name, + f.low_pc.get(), + sym.addr.get() + ); + checked += 1; + } + assert!(checked >= 3, "{fixture}: only {checked} functions compared"); + } +} + +// --------------------------------------------------------------------------- +// The PLT, in both of its i386 shapes. +// +// There is no i386 linker on this machine, so the dynamic images are built +// here: a header, one PT_LOAD, a `.dynsym`/`.dynstr` pair, a `.rel.plt` of +// R_386_JMP_SLOT entries, a `.got.plt`, and a `.plt` whose entries jump +// through those slots. Synthesising them is also the only way to get the two +// forms side by side from one toolchain. +// --------------------------------------------------------------------------- + +#[test] +fn a_position_dependent_plt_resolves_to_names() { + let obj = load(&synth::dynamic_plt(false), &LoadOptions::default()).expect("load"); + let names: Vec<&str> = obj + .function_hints + .iter() + .filter_map(|h| h.name.as_deref()) + .filter(|n| n.ends_with("@plt")) + .collect(); + assert_eq!(names, ["puts@plt", "strlen@plt"], "{:?}", obj.warnings); + assert_eq!( + obj.metadata.get("elf.plt.form").map(String::as_str), + Some("decoded"), + "the absolute form went through the index-order fallback" + ); + for imp in &obj.imports { + assert!(imp.thunk.is_some(), "{} has no thunk", imp.name); + } +} + +#[test] +fn a_position_independent_plt_resolves_to_names() { + let obj = load(&synth::dynamic_plt(true), &LoadOptions::default()).expect("load"); + let names: Vec<&str> = obj + .function_hints + .iter() + .filter_map(|h| h.name.as_deref()) + .filter(|n| n.ends_with("@plt")) + .collect(); + assert_eq!(names, ["puts@plt", "strlen@plt"], "{:?}", obj.warnings); + assert_eq!( + obj.metadata.get("elf.plt.form").map(String::as_str), + Some("decoded") + ); +} + +/// The reason to decode rather than count: an entry no relocation names shifts +/// every later slot, and the index-order rule then labels the wrong bytes. +#[test] +fn an_unnamed_plt_entry_does_not_shift_the_names() { + let obj = load(&synth::plt_with_ifunc_gap(), &LoadOptions::default()).expect("load"); + let plt = obj.section(".plt").expect(".plt").range.start().get(); + let named: Vec<(u64, &str)> = obj + .function_hints + .iter() + .filter_map(|h| Some((h.addr.get(), h.name.as_deref()?))) + .filter(|(_, n)| n.ends_with("@plt")) + .collect(); + // Entry 0 is the resolver, entry 1 is the unnamed IFUNC, so the two named + // entries are 2 and 3. Counting would have put them at 1 and 2. + assert_eq!( + named, + [(plt + 0x20, "puts@plt"), (plt + 0x30, "strlen@plt")], + "{:?}", + obj.warnings + ); +} + +// --------------------------------------------------------------------------- +// Hostile input. +// --------------------------------------------------------------------------- + +/// Truncations and targeted corruptions of the relocation tables, in the style +/// of `fuzz.rs`: a loader given any bytes returns a value or an error, and +/// never panics, hangs or allocates without bound. +#[test] +fn corrupted_relocation_tables_neither_panic_nor_hang() { + let Some(_) = corpus() else { return }; + let mut seeds: Vec> = Vec::new(); + for fixture in FIXTURES { + if let Some(p) = corpus().map(|d| d.join(fixture)) { + if let Ok(d) = std::fs::read(p) { + seeds.push(d); + } + } + } + seeds.push(synth::dynamic_plt(true)); + seeds.push(synth::tls_object()); + if seeds.is_empty() { + return; + } + + // The budget stops the sweep; the hang is caught per file. + // + // How long the whole sweep takes is a fact about the build profile and the + // machine -- it is 3s optimized here and ten times that unoptimized on a + // shared runner, with nothing hanging in either -- so the sweep spends a + // budget and stops, and what is asserted is the worst single load. One + // small file taking seconds to refuse is a hang whatever built it. + const BUDGET: Duration = Duration::from_secs(20); + const ONE_FILE: Duration = Duration::from_secs(10); + + let started = Instant::now(); + let mut worst = Duration::ZERO; + let mut slowest = String::new(); + let mut tried = 0u64; + let mut time = |what: String, f: &mut dyn FnMut()| { + let at = Instant::now(); + f(); + let took = at.elapsed(); + tried += 1; + if took > worst { + worst = took; + slowest = what; + } + }; + 'sweep: for (n, seed) in seeds.iter().enumerate() { + // Every truncation, at a stride that still lands inside every table. + for cut in (0..seed.len()).step_by(7) { + time(format!("seed {n} truncated to {cut}"), &mut || { + let _ = load(&seed[..cut], &LoadOptions::default()); + }); + if started.elapsed() > BUDGET { + break 'sweep; + } + } + // Every byte of every section header, saturated. That is how a + // relocation count becomes 7x10^17: `sh_size` is a number from the + // file and nothing else bounds the walk. + for i in 0..seed.len() { + let mut v = seed.clone(); + for b in v[i..(i + 8).min(seed.len())].iter_mut() { + *b = 0xff; + } + time(format!("seed {n} saturated at {i}"), &mut || { + let _ = load(&v, &LoadOptions::default()); + }); + if started.elapsed() > BUDGET { + break 'sweep; + } + } + } + // Not a silent pass: a sweep that tried almost nothing proves nothing. + assert!(tried > 100, "the sweep only loaded {tried} files"); + assert!( + worst < ONE_FILE, + "{slowest} took {worst:?} to load, which is a hang, not slowness \ + ({tried} files in {:?})", + started.elapsed() + ); +} + +/// The specific shape that cost 87 seconds once: a declared count that no file +/// could hold. It must warn and clamp, not allocate and not error. +#[test] +fn an_enormous_declared_relocation_count_is_clamped_and_reported() { + let mut data = synth::tls_object(); + // Section header 2 is `.rel.text`; sh_size is its sixth word. + let shoff = u32::from_le_bytes([data[32], data[33], data[34], data[35]]) as usize; + let off = shoff + 2 * 40; + data[off + 20..off + 24].copy_from_slice(&0xffff_fff0u32.to_le_bytes()); + let started = Instant::now(); + let obj = load(&data, &LoadOptions::default()).expect("a silly size must not fail the load"); + assert!( + started.elapsed() < Duration::from_secs(2), + "took {:?}", + started.elapsed() + ); + assert!( + obj.warnings.iter().any(|w| w.contains("declares")), + "the clamp was silent: {:?}", + obj.warnings + ); +} + +// --------------------------------------------------------------------------- +// Synthesised ELF32 images. +// --------------------------------------------------------------------------- + +mod synth { + const EHDR: usize = 52; + const SHDR: usize = 40; + const PHDR: usize = 32; + + struct Sec { + name: &'static str, + kind: u32, + flags: u32, + addr: u32, + offset: u32, + size: u32, + link: u32, + info: u32, + entsize: u32, + } + + fn put(v: &mut Vec, b: &[u8]) { + v.extend_from_slice(b); + } + + fn build( + e_type: u16, + entry: u32, + secs: &[Sec], + shstrtab: &[u8], + body: &[u8], + body_offset: u32, + load: Option<(u32, u32, u32)>, + ) -> Vec { + let phnum = load.iter().len() as u16; + let shoff = body_offset as usize + body.len(); + let mut v: Vec = Vec::new(); + put(&mut v, &[0x7f, b'E', b'L', b'F', 1, 1, 1, 0]); + put(&mut v, &[0u8; 8]); + put(&mut v, &e_type.to_le_bytes()); + put(&mut v, &3u16.to_le_bytes()); // EM_386 + put(&mut v, &1u32.to_le_bytes()); + put(&mut v, &entry.to_le_bytes()); + put( + &mut v, + &(if phnum > 0 { EHDR as u32 } else { 0 }).to_le_bytes(), + ); + put(&mut v, &(shoff as u32).to_le_bytes()); + put(&mut v, &0u32.to_le_bytes()); // e_flags + put(&mut v, &(EHDR as u16).to_le_bytes()); + put(&mut v, &(PHDR as u16).to_le_bytes()); + put(&mut v, &phnum.to_le_bytes()); + put(&mut v, &(SHDR as u16).to_le_bytes()); + // One null header, one per section, one for the string table itself. + put(&mut v, &(secs.len() as u16 + 2).to_le_bytes()); + put(&mut v, &(secs.len() as u16 + 1).to_le_bytes()); // shstrndx + assert_eq!(v.len(), EHDR); + + if let Some((vaddr, offset, size)) = load { + put(&mut v, &1u32.to_le_bytes()); // PT_LOAD + put(&mut v, &offset.to_le_bytes()); + put(&mut v, &vaddr.to_le_bytes()); + put(&mut v, &vaddr.to_le_bytes()); + put(&mut v, &size.to_le_bytes()); + put(&mut v, &size.to_le_bytes()); + put(&mut v, &7u32.to_le_bytes()); // p_flags last on ELF32 + put(&mut v, &0x1000u32.to_le_bytes()); + assert_eq!(v.len(), EHDR + PHDR); + } + v.resize(body_offset as usize, 0); + put(&mut v, body); + + // Section header 0, then one per section, then the string table's own. + put(&mut v, &[0u8; SHDR]); + let mut name_off = 1u32; + for s in secs { + put(&mut v, &name_off.to_le_bytes()); + put(&mut v, &s.kind.to_le_bytes()); + put(&mut v, &s.flags.to_le_bytes()); + put(&mut v, &s.addr.to_le_bytes()); + put(&mut v, &s.offset.to_le_bytes()); + put(&mut v, &s.size.to_le_bytes()); + put(&mut v, &s.link.to_le_bytes()); + put(&mut v, &s.info.to_le_bytes()); + put(&mut v, &4u32.to_le_bytes()); + put(&mut v, &s.entsize.to_le_bytes()); + name_off += s.name.len() as u32 + 1; + } + // The string table sits after the headers. + let strtab_off = v.len() + SHDR; + put(&mut v, &name_off.to_le_bytes()); + put(&mut v, &3u32.to_le_bytes()); // SHT_STRTAB + put(&mut v, &0u32.to_le_bytes()); + put(&mut v, &0u32.to_le_bytes()); + put(&mut v, &(strtab_off as u32).to_le_bytes()); + put(&mut v, &(shstrtab.len() as u32).to_le_bytes()); + put(&mut v, &0u32.to_le_bytes()); + put(&mut v, &0u32.to_le_bytes()); + put(&mut v, &1u32.to_le_bytes()); + put(&mut v, &0u32.to_le_bytes()); + put(&mut v, shstrtab); + v + } + + fn names(secs: &[Sec]) -> Vec { + let mut t = vec![0u8]; + for s in secs { + t.extend_from_slice(s.name.as_bytes()); + t.push(0); + } + t.push(0); + t + } + + /// A `.dynsym`/`.dynstr` pair naming `puts` and `strlen` as undefined. + fn dynsym(strtab: &mut Vec) -> Vec { + let mut syms = vec![0u8; 16]; + for name in ["puts", "strlen"] { + let off = strtab.len() as u32; + strtab.extend_from_slice(name.as_bytes()); + strtab.push(0); + syms.extend_from_slice(&off.to_le_bytes()); + syms.extend_from_slice(&0u32.to_le_bytes()); // st_value + syms.extend_from_slice(&0u32.to_le_bytes()); // st_size + syms.push(0x12); // GLOBAL FUNC + syms.push(0); + syms.extend_from_slice(&0u16.to_le_bytes()); // SHN_UNDEF + } + syms + } + + /// One i386 PLT entry: the indirect jump, the relocation index push and + /// the branch back to the resolver, padded to sixteen bytes. + fn plt_entry(pic: bool, operand: u32, index: u32) -> Vec { + let mut e = Vec::new(); + e.extend_from_slice(if pic { &[0xff, 0xa3] } else { &[0xff, 0x25] }); + e.extend_from_slice(&operand.to_le_bytes()); + e.push(0x68); // push imm32 + e.extend_from_slice(&index.to_le_bytes()); + e.push(0xe9); // jmp rel32 + e.extend_from_slice(&0u32.to_le_bytes()); + e.resize(16, 0x90); + e + } + + /// A dynamic executable with a two-entry PLT, in whichever form. + pub fn dynamic_plt(pic: bool) -> Vec { + image(pic, false) + } + + /// The same, with an extra entry that no relocation names sitting in front + /// of the two that do. + pub fn plt_with_ifunc_gap() -> Vec { + image(false, true) + } + + fn image(pic: bool, gap: bool) -> Vec { + const BASE: u32 = 0x0804_8000; + let body_offset: u32 = 0x1000; + + let mut dynstr = vec![0u8]; + let dynsyms = dynsym(&mut dynstr); + + // Layout inside the loaded image, all in one PT_LOAD. + let dynsym_addr = BASE + body_offset; + let dynstr_addr = dynsym_addr + dynsyms.len() as u32; + let relplt_addr = (dynstr_addr + dynstr.len() as u32).next_multiple_of(4); + let relplt_len = 2 * 8; + let gotplt_addr = relplt_addr + relplt_len; + // Three reserved slots, then one per PLT entry. + let slots = if gap { 4 } else { 2 }; + let gotplt_len = (3 + slots) * 4; + let plt_addr = (gotplt_addr + gotplt_len).next_multiple_of(16); + let entries = if gap { 4 } else { 3 }; + let plt_len = entries * 16; + + // `.rel.plt`: one R_386_JMP_SLOT per named entry, pointing at the + // GOT slot that entry jumps through. + let named_slot = |i: u32| gotplt_addr + (3 + if gap { i + 1 } else { i }) * 4; + let mut relplt = Vec::new(); + for i in 0..2u32 { + relplt.extend_from_slice(&named_slot(i).to_le_bytes()); + // r_info: symbol index i+1, type 7 (R_386_JMP_SLOT). + relplt.extend_from_slice(&(((i + 1) << 8) | 7).to_le_bytes()); + } + + let gotplt = vec![0u8; gotplt_len as usize]; + + let mut plt = Vec::new(); + // Entry 0 is the resolver stub, which names no symbol. + plt.extend_from_slice(&plt_entry(pic, 4, 0)); + if gap { + // An IFUNC entry: a real jump through a slot no relocation names. + let slot = gotplt_addr + 3 * 4; + plt.extend_from_slice(&plt_entry( + pic, + if pic { slot - gotplt_addr } else { slot }, + 0, + )); + } + for i in 0..2u32 { + let slot = named_slot(i); + plt.extend_from_slice(&plt_entry( + pic, + if pic { slot - gotplt_addr } else { slot }, + i * 8, + )); + } + assert_eq!(plt.len() as u32, plt_len); + + // `.dynamic`, for DT_PLTGOT: the GOT base the `%ebx` form measures + // from. Without it the position-independent entries name nothing. + let dynamic_addr = plt_addr + plt_len; + let mut dynamic = Vec::new(); + dynamic.extend_from_slice(&3u32.to_le_bytes()); // DT_PLTGOT + dynamic.extend_from_slice(&gotplt_addr.to_le_bytes()); + dynamic.extend_from_slice(&0u32.to_le_bytes()); // DT_NULL + dynamic.extend_from_slice(&0u32.to_le_bytes()); + + let end = dynamic_addr + dynamic.len() as u32; + let mut body = vec![0u8; (end - dynsym_addr) as usize]; + let mut place = |addr: u32, bytes: &[u8]| { + let at = (addr - dynsym_addr) as usize; + body[at..at + bytes.len()].copy_from_slice(bytes); + }; + place(dynsym_addr, &dynsyms); + place(dynstr_addr, &dynstr); + place(relplt_addr, &relplt); + place(gotplt_addr, &gotplt); + place(plt_addr, &plt); + place(dynamic_addr, &dynamic); + + let off = |addr: u32| body_offset + (addr - dynsym_addr); + let secs = vec![ + Sec { + name: ".dynsym", + kind: 11, + flags: 2, + addr: dynsym_addr, + offset: off(dynsym_addr), + size: dynsyms.len() as u32, + link: 2, + info: 1, + entsize: 16, + }, + Sec { + name: ".dynstr", + kind: 3, + flags: 2, + addr: dynstr_addr, + offset: off(dynstr_addr), + size: dynstr.len() as u32, + link: 0, + info: 0, + entsize: 0, + }, + Sec { + name: ".rel.plt", + kind: 9, + flags: 2, + addr: relplt_addr, + offset: off(relplt_addr), + size: relplt_len, + link: 1, + info: 4, + entsize: 8, + }, + Sec { + name: ".got.plt", + kind: 1, + flags: 3, + addr: gotplt_addr, + offset: off(gotplt_addr), + size: gotplt_len, + link: 0, + info: 0, + entsize: 4, + }, + Sec { + name: ".plt", + kind: 1, + flags: 6, + addr: plt_addr, + offset: off(plt_addr), + size: plt_len, + link: 0, + info: 0, + entsize: 16, + }, + Sec { + name: ".dynamic", + kind: 6, + flags: 3, + addr: dynamic_addr, + offset: off(dynamic_addr), + size: dynamic.len() as u32, + link: 2, + info: 0, + entsize: 8, + }, + ]; + // `.dynsym` links to section index 2, which is `.dynstr`. + let shstr = names(&secs); + build( + 2, + plt_addr, + &secs, + &shstr, + &body, + body_offset, + Some((dynsym_addr, body_offset, body.len() as u32)), + ) + } + + /// A relocatable object whose `.rel.text` holds only thread-local + /// relocations, which is what "recorded rather than applied" is about. + pub fn tls_object() -> Vec { + let body_offset: u32 = 0x100; + // Two instructions' worth of bytes to patch, and a symbol table. + let text = vec![0x90u8; 16]; + let mut strtab = vec![0u8]; + let mut symtab = vec![0u8; 16]; + for (name, shndx) in [("tls_a", 1u16), ("tls_b", 1u16)] { + let off = strtab.len() as u32; + strtab.extend_from_slice(name.as_bytes()); + strtab.push(0); + symtab.extend_from_slice(&off.to_le_bytes()); + symtab.extend_from_slice(&0u32.to_le_bytes()); + symtab.extend_from_slice(&4u32.to_le_bytes()); + symtab.push(0x16); // GLOBAL TLS + symtab.push(0); + symtab.extend_from_slice(&shndx.to_le_bytes()); + } + // R_386_TLS_LE (17) and R_386_TLS_IE_32 (33). + let mut rel = Vec::new(); + rel.extend_from_slice(&0u32.to_le_bytes()); + rel.extend_from_slice(&((1u32 << 8) | 17).to_le_bytes()); + rel.extend_from_slice(&8u32.to_le_bytes()); + rel.extend_from_slice(&((2u32 << 8) | 33).to_le_bytes()); + + let mut body = Vec::new(); + let text_off = body_offset; + body.extend_from_slice(&text); + let rel_off = text_off + body.len() as u32; + body.extend_from_slice(&rel); + let symtab_off = text_off + body.len() as u32; + body.extend_from_slice(&symtab); + let strtab_off = text_off + body.len() as u32; + body.extend_from_slice(&strtab); + + let secs = vec![ + Sec { + name: ".text", + kind: 1, + flags: 6, + addr: 0, + offset: text_off, + size: text.len() as u32, + link: 0, + info: 0, + entsize: 0, + }, + Sec { + name: ".rel.text", + kind: 9, + flags: 0, + addr: 0, + offset: rel_off, + size: rel.len() as u32, + link: 3, + info: 1, + entsize: 8, + }, + Sec { + name: ".symtab", + kind: 2, + flags: 0, + addr: 0, + offset: symtab_off, + size: symtab.len() as u32, + link: 4, + info: 1, + entsize: 16, + }, + Sec { + name: ".strtab", + kind: 3, + flags: 0, + addr: 0, + offset: strtab_off, + size: strtab.len() as u32, + link: 0, + info: 0, + entsize: 0, + }, + ]; + let shstr = names(&secs); + build(1, 0, &secs, &shstr, &body, body_offset, None) + } +} + +/// A section that claims more than the image holds must not become a loop. +/// +/// Found by the sweep above, which named the file it choked on: eight bytes +/// saturated at 4488 of a 4,642-byte dynamic object lands in a section header, +/// and `.plt` then claimed a size the file could not hold. The PLT readers +/// divided that size by an entry width and walked a quarter of a billion +/// slots, every one of them outside the image -- 309ms optimized here, and the +/// eighteen seconds a dev build took on a runner. +/// +/// Pinned by name rather than left to the sweep, which spends a budget and may +/// stop before reaching this input again. The bound is loose on purpose: the +/// gap between a bounded read and an unbounded one here is four orders of +/// magnitude, so nothing turns on where inside that gap the line sits. +#[test] +fn a_section_larger_than_the_image_is_not_a_loop() { + let mut v = synth::dynamic_plt(true); + let end = (4488 + 8).min(v.len()); + assert!( + end > 4488, + "the synthetic object is too short to corrupt there" + ); + for b in v[4488..end].iter_mut() { + *b = 0xff; + } + let started = Instant::now(); + let _ = load(&v, &LoadOptions::default()); + let took = started.elapsed(); + assert!( + took < Duration::from_secs(2), + "loading {} bytes took {took:?}: a size from the file became a walk", + v.len() + ); +} diff --git a/crates/e5r-format/tests/fixtures.rs b/crates/e5r-format/tests/fixtures.rs new file mode 100644 index 0000000..8d25c96 --- /dev/null +++ b/crates/e5r-format/tests/fixtures.rs @@ -0,0 +1,359 @@ +//! Loader tests against the built fixture corpus, with readelf as the oracle. +//! +//! Skipped when the corpus is absent so a fresh checkout still passes; run +//! `scripts/build-fixtures.sh` to enable them. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_core::{Addr, Arch, Bits, Evidence, Strength}; +use e5r_format::{LoadOptions, Object, SymbolKind, load}; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option<(Object, Vec)> { + let p = corpus()?.join(name); + let data = std::fs::read(&p).ok()?; + let obj = load(&data, &LoadOptions::default()) + .unwrap_or_else(|e| panic!("loading {}: {e}", p.display())); + Some((obj, data)) +} + +/// Ask readelf, so the expectations are not our own parser's opinion. +fn readelf(name: &str, args: &[&str]) -> Option { + let p = corpus()?.join(name); + let out = Command::new("readelf").args(args).arg(&p).output().ok()?; + out.status + .success() + .then(|| String::from_utf8_lossy(&out.stdout).into_owned()) +} + +#[test] +fn aarch64_executable_loads() { + let Some((obj, _)) = open("hello.a64.O0") else { + return; + }; + assert_eq!(obj.arch, Arch::AArch64); + assert_eq!(obj.bits, Bits::Bits64); + assert!(obj.entry.is_some()); + assert!(!obj.memory.is_empty()); + assert!(obj.section(".text").is_some(), "no .text"); + assert!(obj.memory.is_executable(obj.entry.unwrap())); +} + +#[test] +fn entry_point_matches_readelf() { + let Some((obj, _)) = open("hello.a64.O0") else { + return; + }; + let Some(txt) = readelf("hello.a64.O0", &["-h"]) else { + return; + }; + let want = txt + .lines() + .find(|l| l.contains("Entry point address")) + .and_then(|l| l.split("0x").nth(1)) + .and_then(|h| u64::from_str_radix(h.trim(), 16).ok()) + .expect("readelf printed no entry point"); + assert_eq!(obj.entry, Some(Addr(want))); +} + +#[test] +fn section_addresses_match_readelf() { + let Some((obj, _)) = open("hello.a64.O0") else { + return; + }; + let Some(txt) = readelf("hello.a64.O0", &["-S", "-W"]) else { + return; + }; + let mut checked = 0; + for line in txt.lines() { + // " [ 14] .text PROGBITS 0000000000000640 000640 000174 00 AX 0 0 64" + let Some(rest) = line.split(']').nth(1) else { + continue; + }; + let f: Vec<&str> = rest.split_whitespace().collect(); + if f.len() < 4 || !f[0].starts_with('.') { + continue; + } + let Ok(addr) = u64::from_str_radix(f[2], 16) else { + continue; + }; + if addr == 0 { + continue; + } + let ours = obj + .section(f[0]) + .unwrap_or_else(|| panic!("missing {}", f[0])); + assert_eq!(ours.range.start(), Addr(addr), "{}", f[0]); + checked += 1; + } + assert!(checked > 5, "only checked {checked} sections"); +} + +#[test] +fn every_readelf_function_symbol_is_found() { + let Some((obj, _)) = open("hello.a64.O0") else { + return; + }; + let Some(txt) = readelf("hello.a64.O0", &["-sW"]) else { + return; + }; + let mut want = 0; + let mut found = 0; + for line in txt.lines() { + let f: Vec<&str> = line.split_whitespace().collect(); + // " 57: 0000000000000754 112 FUNC GLOBAL DEFAULT 14 main" + if f.len() < 8 || f[3] != "FUNC" || f[6] == "UND" { + continue; + } + let Ok(addr) = u64::from_str_radix(f[1], 16) else { + continue; + }; + if addr == 0 { + continue; + } + want += 1; + let name = f[7].split('@').next().unwrap(); + if obj + .symbols + .iter() + .any(|s| s.name == name && s.addr == Addr(addr) && s.kind == SymbolKind::Function) + { + found += 1; + } + } + assert!(want > 0, "readelf listed no defined functions"); + assert_eq!( + found, + want, + "missed {} of {want} function symbols", + want - found + ); +} + +#[test] +fn eh_frame_agrees_with_the_symbol_table() { + // Both are proven evidence for the same functions, so on a binary with + // both, .eh_frame should not be inventing entries the symbols deny. + let Some((obj, _)) = open("hello.a64.O0") else { + return; + }; + let eh: Vec = obj + .function_hints + .iter() + .filter(|h| { + h.provenance.best == Evidence::EhFrame + || h.provenance.corroborating.contains(&Evidence::EhFrame) + }) + .map(|h| h.addr) + .collect(); + assert!(!eh.is_empty(), "no .eh_frame hints at all"); + for a in &eh { + assert!(obj.memory.is_executable(*a), "{a} is not executable"); + } + // The symbol table's functions should nearly all be corroborated by an FDE. + let syms: Vec = obj + .symbols + .iter() + .filter(|s| s.is_defined_function() && !s.dynamic) + .map(|s| s.addr) + .collect(); + let overlap = syms.iter().filter(|a| eh.contains(a)).count(); + assert!( + overlap * 2 >= syms.len(), + "only {overlap} of {} symbol functions have an FDE", + syms.len() + ); +} + +#[test] +fn hints_are_sorted_by_evidence_strength() { + let Some((obj, _)) = open("hello.a64.O0") else { + return; + }; + let mut last = Strength::Asserted; + for h in &obj.function_hints { + assert!(h.provenance.strength() <= last, "hints are out of order"); + last = h.provenance.strength(); + } +} + +#[test] +fn relocatable_objects_get_synthetic_addresses() { + let Some((obj, _)) = open("shapes.a64.O2.o") else { + return; + }; + assert_eq!(obj.arch, Arch::AArch64); + // Every section sits at zero in the file; the loader must lay them out. + let text = obj.section(".text").expect("no .text"); + assert!(text.range.start() != Addr::ZERO, "text was not relocated"); + assert!(!text.range.is_empty()); + // Function symbols must follow their section. + for want in ["sum_to", "branchy", "dense_switch", "recursive"] { + let s = obj + .symbols + .iter() + .find(|s| s.name == want) + .unwrap_or_else(|| panic!("no symbol {want}")); + assert!( + obj.memory.is_executable(s.addr), + "{want} at {} is not in executable memory", + s.addr + ); + } +} + +#[test] +fn x86_64_objects_load_on_an_aarch64_host() { + let Some((obj, _)) = open("shapes.x64.O2.o") else { + return; + }; + assert_eq!(obj.arch, Arch::X86_64); + assert_eq!(obj.bits, Bits::Bits64); + assert!(obj.symbols.iter().any(|s| s.name == "dense_switch")); +} + +#[test] +fn stripped_binaries_still_yield_functions() { + let Some((stripped, _)) = open("hello.a64.O2.stripped") else { + return; + }; + let Some((full, _)) = open("hello.a64.O2") else { + return; + }; + assert!( + stripped.symbols.len() < full.symbols.len(), + "stripping removed nothing" + ); + // .eh_frame survives stripping, which is the point of preferring it. + assert!( + !stripped.function_hints.is_empty(), + "no function hints left after stripping" + ); +} + +#[test] +fn loading_is_deterministic() { + let Some((a, data)) = open("hello.a64.O0") else { + return; + }; + let b = load(&data, &LoadOptions::default()).unwrap(); + assert_eq!(a.symbols, b.symbols); + assert_eq!( + a.function_hints.iter().map(|h| h.addr).collect::>(), + b.function_hints.iter().map(|h| h.addr).collect::>() + ); + assert_eq!(a.metadata, b.metadata); +} + +#[test] +fn truncation_at_every_length_never_panics() { + let Some((_, data)) = open("hello.a64.O0") else { + return; + }; + // Every prefix of a real file is a plausible truncated download and a + // plausible fuzz case. + let mut n = 1; + while n < data.len() { + let _ = load(&data[..n], &LoadOptions::default()); + n = (n * 2).max(n + 97); + } +} + +#[test] +fn single_byte_corruption_never_panics() { + let Some((_, data)) = open("hello.a64.O0") else { + return; + }; + let mut buf = data.clone(); + // Headers are where a hostile file does its damage, so hit them hardest. + for i in (0..0x400.min(buf.len())).step_by(3) { + let old = buf[i]; + for v in [0xff, 0x00, 0x80] { + buf[i] = v; + let _ = load(&buf, &LoadOptions::default()); + } + buf[i] = old; + } +} + +#[test] +fn plt_thunks_are_named_where_objdump_names_them() { + // objdump prints `` at each PLT entry, which is the oracle for + // whether the layout arithmetic is right. An off-by-one here names every + // indirect call after the wrong import. + // Whatever this machine has: the oracle is objdump, not a recorded list, + // so any dynamically linked system binary will do and the architecture + // does not have to be this one. + let mut compared_somewhere = false; + for path in [ + "/bin/bash", + "/usr/lib/aarch64-linux-gnu/libc.so.6", + "/usr/lib/x86_64-linux-gnu/libc.so.6", + ] { + let p = Path::new(path); + if !p.is_file() { + continue; + } + let data = std::fs::read(p).unwrap(); + let Ok(obj) = load(&data, &LoadOptions::default()) else { + continue; + }; + // `.plt.sec` as well: with indirect-branch tracking on, that is where + // the entry a call reaches lives, and it is the one objdump labels. + let Ok(out) = Command::new("objdump") + .args(["-d", "--section=.plt", "--section=.plt.sec"]) + .arg(p) + .output() + else { + continue; + }; + let text = String::from_utf8_lossy(&out.stdout); + let mut checked = 0; + for line in text.lines() { + // "0000000000032b80 :" + let Some((addr, rest)) = line.split_once(" <") else { + continue; + }; + let Some(name) = rest.strip_suffix(">:") else { + continue; + }; + if !name.ends_with("@plt") { + continue; + } + // objdump synthesizes `*ABS*+0x...@plt` for an IFUNC entry whose + // relocation names no symbol. That is its own invention, not a + // name in the file, so there is nothing to match. + if name.starts_with('*') { + continue; + } + let Ok(addr) = u64::from_str_radix(addr.trim(), 16) else { + continue; + }; + let ours = obj + .function_hints + .iter() + .find(|h| h.addr == Addr(addr) && h.name.as_deref() == Some(name)); + assert!( + ours.is_some(), + "{path}: objdump names {name} at {addr:#x}, we do not" + ); + checked += 1; + } + // Not per path: a machine need not have all three, and one that names + // none of them is a machine where this gate did not run, which is + // worth failing over rather than passing quietly. + if checked > 10 { + compared_somewhere = true; + } + } + assert!( + compared_somewhere, + "no system binary here offered a PLT objdump would name; this gate \ + compared nothing" + ); +} diff --git a/crates/e5r-format/tests/fuzz.rs b/crates/e5r-format/tests/fuzz.rs new file mode 100644 index 0000000..fb14a66 --- /dev/null +++ b/crates/e5r-format/tests/fuzz.rs @@ -0,0 +1,188 @@ +//! Mutation fuzzing of the loaders, on stable and in the ordinary test run. +//! +//! `cargo-fuzz` needs nightly and a separate invocation, so it finds nothing +//! on a machine where nobody runs it. This runs every time, seeded from the +//! real corpus, and keeps a fixed budget so it stays a test rather than a +//! session. The contract it checks is the one the error model promises: a +//! loader given any bytes returns a value or an error, and never panics, hangs +//! or allocates without bound. + +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +use e5r_format::{LoadOptions, load}; + +/// How long each target gets. Enough to find a systematic fault, short enough +/// that nobody is tempted to skip the suite. +const BUDGET: Duration = Duration::from_millis(1500); + +/// A small deterministic generator, so a failure is reproducible from its seed. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + // xorshift64*, chosen for being four lines and reproducible. + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545_f491_4f6c_dd1d) + } + + fn below(&mut self, n: usize) -> usize { + if n == 0 { + 0 + } else { + (self.next() % n as u64) as usize + } + } +} + +fn corpus() -> Vec> { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + let mut out = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&dir) { + for e in entries.flatten() { + let p: PathBuf = e.path(); + if !p.is_file() { + continue; + } + if let Ok(d) = std::fs::read(&p) { + // Prefixes only: a whole libc per iteration would make this a + // throughput test rather than a fault hunt. + out.push(d[..d.len().min(64 * 1024)].to_vec()); + } + } + } + out +} + +/// Corrupt a buffer in one of the ways that actually break parsers. +fn mutate(seed: &[u8], rng: &mut Rng) -> Vec { + let mut v = seed.to_vec(); + if v.is_empty() { + return v; + } + match rng.below(6) { + // Truncate: the most common real-world corruption. + 0 => v.truncate(rng.below(v.len())), + // Flip a bit. + 1 => { + let i = rng.below(v.len()); + v[i] ^= 1 << rng.below(8); + } + // Write a saturating value, which is how a count becomes enormous. + 2 => { + let i = rng.below(v.len()); + let end = (i + 8).min(v.len()); + for b in &mut v[i..end] { + *b = 0xff; + } + } + // Zero a field. + 3 => { + let i = rng.below(v.len()); + let end = (i + 4).min(v.len()); + for b in &mut v[i..end] { + *b = 0; + } + } + // Splice: move a chunk somewhere else, which breaks every offset. + 4 => { + let from = rng.below(v.len()); + let to = rng.below(v.len()); + let len = rng.below(64).min(v.len() - from.max(to)); + let chunk: Vec = v[from..from + len].to_vec(); + v[to..to + len].copy_from_slice(&chunk); + } + // Scramble the headers, which is where a parser does its arithmetic. + _ => { + let end = 0x200.min(v.len()); + for b in &mut v[..end] { + if rng.below(8) == 0 { + *b = (rng.next() & 0xff) as u8; + } + } + } + } + v +} + +#[test] +fn loaders_survive_mutated_real_files() { + let seeds = corpus(); + if seeds.is_empty() { + return; + } + let opts = LoadOptions::default(); + let mut rng = Rng(0x5eed_1234_abcd_0001); + let start = Instant::now(); + let mut n = 0u64; + while start.elapsed() < BUDGET { + let seed = &seeds[rng.below(seeds.len())]; + let case = mutate(seed, &mut rng); + // The contract: a value or an error, never anything else. + let _ = load(&case, &opts); + n += 1; + } + assert!( + n > 100, + "only {n} cases in {BUDGET:?}; something is very slow" + ); + println!("loader fuzz: {n} mutated cases, no panics"); +} + +#[test] +fn loaders_survive_arbitrary_bytes() { + // Not mutations of anything: bytes that only look like a header by + // accident are a different shape of input. + let opts = LoadOptions::default(); + let mut rng = Rng(0x5eed_dead_beef_0002); + let start = Instant::now(); + let mut n = 0u64; + // Every magic, so the probe actually reaches each loader. + let magics: [&[u8]; 5] = [ + b"\x7fELF", + b"MZ\x90\x00", + b"\xcf\xfa\xed\xfe", + b"\xca\xfe\xba\xbe", + b"\x64\x86\x04\x00", + ]; + while start.elapsed() < BUDGET { + let len = 16 + rng.below(4096); + let mut v = Vec::with_capacity(len); + let magic = magics[rng.below(magics.len())]; + v.extend_from_slice(magic); + while v.len() < len { + v.extend_from_slice(&rng.next().to_le_bytes()); + } + v.truncate(len); + let _ = load(&v, &opts); + n += 1; + } + assert!(n > 100, "only {n} cases in {BUDGET:?}"); + println!("loader fuzz: {n} random cases, no panics"); +} + +#[test] +fn a_lying_count_does_not_exhaust_memory() { + // A header that claims four billion sections must produce an error rather + // than an allocation. Measured by the fact that this returns at all. + let mut elf = vec![0u8; 0x1000]; + elf[..4].copy_from_slice(b"\x7fELF"); + elf[4] = 2; // 64-bit + elf[5] = 1; // little-endian + elf[16..18].copy_from_slice(&2u16.to_le_bytes()); // ET_EXEC + elf[18..20].copy_from_slice(&183u16.to_le_bytes()); // AArch64 + // e_shoff, e_shentsize, e_shnum: a plausible offset and an absurd count. + elf[0x28..0x30].copy_from_slice(&0x40u64.to_le_bytes()); + elf[0x3a..0x3c].copy_from_slice(&64u16.to_le_bytes()); + elf[0x3c..0x3e].copy_from_slice(&0xffffu16.to_le_bytes()); + let started = Instant::now(); + let _ = load(&elf, &LoadOptions::default()); + assert!( + started.elapsed() < Duration::from_secs(2), + "a lying section count took too long" + ); +} diff --git a/crates/e5r-format/tests/macho.rs b/crates/e5r-format/tests/macho.rs new file mode 100644 index 0000000..a96945d --- /dev/null +++ b/crates/e5r-format/tests/macho.rs @@ -0,0 +1,195 @@ +//! Mach-O loading. +//! +//! There is no macOS linker here, so the objects come from clang, which +//! cross-compiles them without a sysroot, and the image path is exercised by a +//! synthesized file. `llvm-objdump` is the cross-check for the objects. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_core::{Addr, Arch, Bits, Evidence}; +use e5r_format::{Format, LoadOptions, load, macho}; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option<(e5r_format::Object, Vec)> { + let p = corpus()?.join(name); + let data = std::fs::read(&p).ok()?; + let obj = load(&data, &LoadOptions::default()) + .unwrap_or_else(|e| panic!("loading {}: {e}", p.display())); + Some((obj, data)) +} + +#[test] +fn an_arm64_object_loads() { + let Some((obj, _)) = open("wide.macho.a64.o") else { + return; + }; + assert_eq!(obj.format, Format::MachO); + assert_eq!(obj.arch, Arch::AArch64); + assert_eq!(obj.bits, Bits::Bits64); + assert_eq!( + obj.metadata.get("macho.type").map(String::as_str), + Some("object") + ); + let text = obj + .section("__TEXT,__text") + .expect("no __TEXT,__text section"); + assert!(text.exec); + assert!(obj.memory.is_executable(text.range.start())); +} + +#[test] +fn an_x86_64_object_loads_on_an_aarch64_host() { + let Some((obj, _)) = open("wide.macho.x64.o") else { + return; + }; + assert_eq!(obj.arch, Arch::X86_64); + assert!(obj.symbols.iter().any(|s| s.name == "rotate")); +} + +#[test] +fn the_underscore_prefix_is_removed() { + // Mach-O prefixes C symbols with an underscore; a listing that shows + // `_rotate` is showing an implementation detail of the format. + let Some((obj, _)) = open("wide.macho.a64.o") else { + return; + }; + assert!(obj.symbols.iter().any(|s| s.name == "rotate")); + assert!( + !obj.symbols.iter().any(|s| s.name == "_rotate"), + "the underscore was not removed" + ); +} + +#[test] +fn assembler_temporaries_do_not_name_functions() { + // clang emits `ltmp0` at the same address as the first real function. + // Letting it win hides the name anyone actually wants. + let Some((obj, _)) = open("wide.macho.a64.o") else { + return; + }; + for h in &obj.function_hints { + if let Some(n) = &h.name { + assert!( + !n.starts_with("ltmp") && !n.starts_with("l_"), + "{n} was used as a function name" + ); + } + } + // And the real name is still there. + assert!( + obj.function_hints + .iter() + .any(|h| h.name.as_deref() == Some("arith8")), + "the real name at that address was lost" + ); +} + +#[test] +fn every_symbol_llvm_objdump_lists_is_found() { + let Some((obj, _)) = open("wide.macho.a64.o") else { + return; + }; + let p = corpus().unwrap().join("wide.macho.a64.o"); + let Ok(out) = Command::new("llvm-objdump-18") + .args(["-t"]) + .arg(&p) + .output() + else { + return; + }; + if !out.status.success() { + return; + } + let text = String::from_utf8_lossy(&out.stdout); + let mut checked = 0; + for line in text.lines() { + // "0000000000000000 g F __TEXT,__text _arith8" + let Some(name) = line.split_whitespace().last() else { + continue; + }; + let Some(name) = name.strip_prefix('_') else { + continue; + }; + if !line.contains("__TEXT,__text") || name.is_empty() { + continue; + } + assert!( + obj.symbols.iter().any(|s| s.name == name), + "llvm-objdump lists {name}, we do not" + ); + checked += 1; + } + assert!(checked > 5, "only {checked} symbols compared"); +} + +#[test] +fn a_truncated_macho_never_panics() { + let Some((_, data)) = open("wide.macho.a64.o") else { + return; + }; + let mut n = 1; + while n < data.len() { + let _ = load(&data[..n], &LoadOptions::default()); + n = (n * 2).max(n + 31); + } +} + +#[test] +fn corrupted_headers_never_panic() { + let Some((_, mut data)) = open("wide.macho.a64.o") else { + return; + }; + for i in (0..0x400.min(data.len())).step_by(3) { + let old = data[i]; + for v in [0xff, 0x00, 0x80] { + data[i] = v; + let _ = load(&data, &LoadOptions::default()); + } + data[i] = old; + } +} + +#[test] +fn a_fat_header_with_a_lying_count_is_an_error_not_a_panic() { + // Built by hand: the fat index is big-endian, and a count it cannot back + // up is the obvious way to make a reader walk off the end. + let mut data = Vec::new(); + data.extend_from_slice(&0xcafe_babeu32.to_be_bytes()); + data.extend_from_slice(&0xffff_ffffu32.to_be_bytes()); + assert!(load(&data, &LoadOptions::default()).is_err()); + + // A count of zero has no slice to load either. + let mut empty = Vec::new(); + empty.extend_from_slice(&0xcafe_babeu32.to_be_bytes()); + empty.extend_from_slice(&0u32.to_be_bytes()); + assert!(load(&empty, &LoadOptions::default()).is_err()); +} + +#[test] +fn is_macho_does_not_claim_an_elf_or_a_pe() { + assert!(!macho::is_macho(b"\x7fELF\x02\x01\x01\x00")); + assert!(!macho::is_macho(b"MZ\x90\x00\x03\x00\x00\x00")); + assert!(!macho::is_macho(b"\xfe\xed")); +} + +#[test] +fn function_starts_are_used_when_present() { + // Objects carry no LC_FUNCTION_STARTS, so this only checks that the + // evidence kind exists and is never invented where it is absent. + let Some((obj, _)) = open("wide.macho.a64.o") else { + return; + }; + assert!( + !obj.function_hints + .iter() + .any(|h| h.provenance.best == Evidence::MachFunctionStarts), + "claimed LC_FUNCTION_STARTS evidence in an object file that has none" + ); + assert!(!obj.function_hints.is_empty()); + assert!(obj.function_hints.iter().all(|h| h.addr != Addr::ZERO)); +} diff --git a/crates/e5r-format/tests/metadata.rs b/crates/e5r-format/tests/metadata.rs new file mode 100644 index 0000000..cf5d33c --- /dev/null +++ b/crates/e5r-format/tests/metadata.rs @@ -0,0 +1,631 @@ +//! Runtime metadata readers, measured against the binaries that carry them. +//! +//! Go is a real oracle: the same program is built twice, once stripped, and +//! every name the `pclntab` reader recovers from the stripped copy is compared +//! against the symbol table of the one that kept it. Rust is a real oracle +//! too: the fixture is compiled from a source file whose name and line numbers +//! the test knows. Objective-C has neither, because there is no macOS linker +//! here, so the image it reads is built by hand. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +use e5r_core::Addr; +use e5r_format::metadata; +use e5r_format::{LoadOptions, Object, load}; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let p = corpus()?.join(name); + let data = std::fs::read(&p).ok()?; + Some(load(&data, &LoadOptions::default()).unwrap_or_else(|e| panic!("loading {name}: {e}"))) +} + +#[test] +fn the_pclntab_names_what_stripping_removed() { + let (Some(plain), Some(stripped)) = (open("hello.go"), open("hello.go.stripped")) else { + return; + }; + // Go's linker appends an ABI suffix to the ELF symbol of a wrapper; the + // pclntab spells the same function without it. + let want: BTreeMap = plain + .symbols + .iter() + .filter(|s| s.is_defined_function()) + // `runtime.text` and its siblings are zero-size markers the linker + // puts at the ends of the text section, not functions. + .filter(|s| s.size != 0) + .map(|s| { + let n = s + .name + .trim_end_matches(".abi0") + .trim_end_matches(".abiinternal"); + (s.addr.get(), n.to_string()) + }) + .collect(); + assert!(want.len() > 500, "a Go binary has more functions than that"); + assert!( + !stripped.symbols.iter().any(|s| s.is_defined_function()), + "the stripped copy still has symbols, so this proves nothing" + ); + + let t = metadata::go_pclntab(&stripped).expect("no pclntab in a stripped Go binary"); + assert!(t.version.as_str().starts_with("go1.")); + let got: BTreeMap = t + .functions + .iter() + .map(|f| (f.addr.get(), f.name.clone())) + .collect(); + + let mut agreed = 0usize; + let mut disagreed = Vec::new(); + for (addr, name) in &want { + match got.get(addr) { + Some(g) if g == name => agreed += 1, + Some(g) => disagreed.push(format!("{addr:#x}: symtab {name}, pclntab {g}")), + None => {} + } + } + assert!(disagreed.is_empty(), "{disagreed:#?}"); + assert!( + agreed * 100 >= want.len() * 90, + "recovered {agreed} of {} names", + want.len() + ); + assert_eq!(got.get(&0), None); +} + +#[test] +fn the_pclntab_gives_every_function_an_end() { + let Some(obj) = open("hello.go.stripped") else { + return; + }; + let t = metadata::go_pclntab(&obj).expect("no pclntab"); + for f in &t.functions { + assert!(f.end > f.addr, "{} has no end", f.name); + } + for h in t.hints() { + assert!(h.size.is_some_and(|n| n > 0)); + assert!(h.name.is_some()); + } + // The table is in address order, which is what lets the end of one + // function be the start of the next. + assert!(t.functions.windows(2).all(|w| w[0].addr <= w[1].addr)); +} + +#[test] +fn the_build_info_says_which_go_built_it() { + let Some(obj) = open("hello.go.stripped") else { + return; + }; + let t = metadata::go_pclntab(&obj).expect("no pclntab"); + let v = t.build_version.expect("no build version"); + assert!(v.starts_with("go1."), "{v}"); + assert!( + t.module_info.is_some_and(|m| m.contains("e5rfixture")), + "the module line should name the fixture module" + ); +} + +#[test] +fn panic_locations_name_the_file_and_line_they_are_in() { + let Some(obj) = open("panicky") else { + return; + }; + let sites = metadata::rust_panic_sites(&obj); + assert!(!sites.is_empty(), "no panic locations at all"); + for s in &sites { + assert!(s.file.ends_with(".rs"), "{}", s.file); + assert!(s.line >= 1 && s.column >= 1); + } + // The fixture is compiled from inside the fixture directory, so the name + // the compiler recorded is the bare one. + let ours: Vec<_> = sites.iter().filter(|s| s.file == "panicky.rs").collect(); + assert!(!ours.is_empty(), "found {} sites, none ours", sites.len()); + let lines: Vec = ours.iter().map(|s| s.line).collect(); + assert!( + lines.contains(&2), + "the indexing panic is on line 2: {lines:?}" + ); + // And the standard library's own panics come with it. + assert!( + sites.iter().any(|s| s.file.contains("core/src")), + "no library locations, so the scan found only one object file's worth" + ); +} + +#[test] +fn a_file_with_no_metadata_yields_none_of_it() { + let Some(obj) = open("hello.a64.O2") else { + return; + }; + assert!(metadata::go_pclntab(&obj).is_none()); + assert!(metadata::objc_classes(&obj).is_empty()); + // A C binary has no Location records; anything found here would be a + // false positive of the scan. + assert!(metadata::rust_panic_sites(&obj).is_empty()); +} + +/// A `pclntab` built byte by byte. +/// +/// The only Go on this machine emits one header shape, so the other three are +/// pinned the way `pe.rs` pins a PE: by writing the layout out and asserting +/// the reader recovers exactly what went in. +fn synth_pclntab(magic: u32, ptr: u64, text: u64, funcs: &[(u64, &str)], end: u64) -> Vec { + let v118 = matches!(magic, 0xffff_fff0 | 0xffff_fff1); + let v12 = magic == 0xffff_fffb; + let field = if v118 { 4 } else { ptr }; + let n = funcs.len() as u64; + + let mut names = Vec::new(); + let mut name_off = Vec::new(); + for (_, name) in funcs { + name_off.push(names.len() as u64); + names.extend_from_slice(name.as_bytes()); + names.push(0); + } + let align = |v: u64| v.next_multiple_of(8); + + // Before 1.16 the function table sits right behind the count, so the name + // blob has to follow it; after, the header says where everything is. + let (header, names_at, functab_at) = if v12 { + let functab_at = 8 + ptr; + let names_at = align(functab_at + (n * 2 + 1) * ptr + 4); + (8 + ptr, names_at, functab_at) + } else if v118 { + let names_at = align(8 + 8 * ptr); + (8 + 8 * ptr, names_at, align(names_at + names.len() as u64)) + } else { + let names_at = align(8 + 7 * ptr); + (8 + 7 * ptr, names_at, align(names_at + names.len() as u64)) + }; + // Whichever of the two tables ends last, the `_func` records go behind it. + let funcs_at = align((names_at + names.len() as u64).max(functab_at + (n * 2 + 1) * field + 4)); + let mut buf = vec![0u8; (funcs_at + n * 16) as usize]; + + let put = |buf: &mut Vec, at: u64, b: &[u8]| { + buf[at as usize..at as usize + b.len()].copy_from_slice(b); + }; + let word = |buf: &mut Vec, at: u64, v: u64, size: u64| { + if size == 4 { + put(buf, at, &(v as u32).to_le_bytes()); + } else { + put(buf, at, &v.to_le_bytes()); + } + }; + + put(&mut buf, 0, &magic.to_le_bytes()); + put(&mut buf, 6, &[1, ptr as u8]); + word(&mut buf, 8, n, ptr); + if !v12 { + word(&mut buf, 8 + ptr, 0, ptr); + let base = if v118 { + word(&mut buf, 8 + 2 * ptr, text, ptr); + 3 + } else { + 2 + }; + word(&mut buf, 8 + base * ptr, names_at, ptr); + word(&mut buf, header - ptr, functab_at, ptr); + } + put(&mut buf, names_at, &names); + + for (i, (addr, _)) in funcs.iter().enumerate() { + let i = i as u64; + let entry = if v118 { addr - text } else { *addr }; + let func_at = funcs_at + i * 16; + word(&mut buf, functab_at + 2 * i * field, entry, field); + word( + &mut buf, + functab_at + (2 * i + 1) * field, + if v12 { func_at } else { func_at - functab_at }, + field, + ); + // _func: the entry PC again, then the offset of the name. + word(&mut buf, func_at, entry, field); + let name_base = if v12 { names_at } else { 0 }; + word( + &mut buf, + func_at + field, + name_base + name_off[i as usize], + 4, + ); + } + let last = if v118 { end - text } else { end }; + word(&mut buf, functab_at + 2 * n * field, last, field); + buf +} + +const MAGICS: [u32; 4] = [0xffff_fffb, 0xffff_fffa, 0xffff_fff0, 0xffff_fff1]; + +fn sample() -> ([(u64, &'static str); 3], u64) { + ( + [ + (0x1100, "main.main"), + (0x1180, "runtime.gcBgMarkWorker"), + (0x1200, "internal/abi.Kind.String"), + ], + 0x1280, + ) +} + +#[test] +fn every_header_shape_round_trips() { + let (funcs, end) = sample(); + for magic in MAGICS { + for ptr in [4u64, 8] { + // The 1.18 layout is 64-bit only in practice, but its entries are + // 32-bit either way, so both widths have to parse. + let bytes = synth_pclntab(magic, ptr, 0x1000, &funcs, end); + let t = metadata::parse_pclntab(&bytes, Some(Addr(0x1000))) + .unwrap_or_else(|e| panic!("{magic:#x}/{ptr}: {e}")); + assert_eq!(t.ptr_size, ptr); + assert_eq!(t.functions.len(), funcs.len(), "{magic:#x}/{ptr}"); + for (i, (addr, name)) in funcs.iter().enumerate() { + assert_eq!(t.functions[i].addr, Addr(*addr), "{magic:#x}/{ptr}"); + assert_eq!(&t.functions[i].name, name, "{magic:#x}/{ptr}"); + } + assert_eq!(t.functions[0].end, Addr(funcs[1].0)); + assert_eq!(t.functions[2].end, Addr(end)); + assert!(t.warnings.is_empty(), "{:?}", t.warnings); + } + } +} + +#[test] +fn a_big_endian_table_reads_the_same_way() { + let (funcs, end) = sample(); + let mut bytes = synth_pclntab(0xffff_fff1, 8, 0x1000, &funcs, end); + // Only the magic distinguishes the two orders, so flipping it is enough + // to prove the rest of the reader follows the magic and not the host. + bytes[..4].copy_from_slice(&0xffff_fff1u32.to_be_bytes()); + let t = metadata::parse_pclntab(&bytes, Some(Addr(0x1000))).unwrap(); + // The body is still little-endian, so nothing should agree, and the + // reader must say so rather than invent functions. + assert!(t.functions.len() < funcs.len() || t.functions[0].addr != Addr(funcs[0].0)); +} + +#[test] +fn a_count_larger_than_the_table_is_clamped_and_reported() { + let (funcs, end) = sample(); + for magic in MAGICS { + let mut bytes = synth_pclntab(magic, 8, 0x1000, &funcs, end); + bytes[8..16].copy_from_slice(&u64::MAX.to_le_bytes()); + let start = Instant::now(); + let t = metadata::parse_pclntab(&bytes, Some(Addr(0x1000))) + .unwrap_or_else(|e| panic!("{magic:#x}: {e}")); + assert!(start.elapsed() < Duration::from_secs(1)); + assert!( + t.functions.len() < 64, + "{magic:#x} produced {} functions from {} bytes", + t.functions.len(), + bytes.len() + ); + assert!( + t.warnings.iter().any(|w| w.contains("room for")), + "{magic:#x}: no warning about the count" + ); + for (addr, name) in &funcs { + assert!( + t.functions + .iter() + .any(|f| f.addr == Addr(*addr) && &f.name == name), + "{magic:#x}: lost a real function while clamping" + ); + } + } +} + +#[test] +fn a_truncated_or_corrupt_table_returns_and_never_hangs() { + let (funcs, end) = sample(); + let real: Vec<&str> = funcs.iter().map(|(_, n)| *n).collect(); + let start = Instant::now(); + for magic in MAGICS { + let full = synth_pclntab(magic, 8, 0x1000, &funcs, end); + for cut in 0..full.len() { + if let Ok(t) = metadata::parse_pclntab(&full[..cut], Some(Addr(0x1000))) { + // A short table may name fewer functions, never other ones. + for f in &t.functions { + assert!(real.contains(&f.name.as_str()), "invented {}", f.name); + } + } + } + for i in 0..full.len() { + for bit in [0u32, 3, 7] { + let mut bad = full.clone(); + bad[i] ^= 1 << bit; + let _ = metadata::parse_pclntab(&bad, Some(Addr(0x1000))); + } + } + } + assert!(start.elapsed() < Duration::from_secs(30)); +} + +const MACHO_BASE: u64 = 0x1_0000_0000; +const TEXT: u64 = MACHO_BASE; +const DATA: u64 = MACHO_BASE + 0x1000; + +/// A Mach-O executable carrying two Objective-C classes. +/// +/// There is no macOS linker here and a relocatable object has nothing but +/// zeros where its class pointers belong, so the only honest way to test the +/// reader against resolved metadata is to write the resolved metadata out. +/// The section names and structure layouts are the ones clang emits, which the +/// relocatable fixture is checked against separately. +fn synth_objc() -> Vec { + let mut f = vec![0u8; 0x2000]; + let put = |f: &mut Vec, at: u64, b: &[u8]| { + f[at as usize..at as usize + b.len()].copy_from_slice(b); + }; + let u32at = |f: &mut Vec, at: u64, v: u32| put(f, at, &v.to_le_bytes()); + let u64at = |f: &mut Vec, at: u64, v: u64| put(f, at, &v.to_le_bytes()); + let name16 = |s: &str| { + let mut b = [0u8; 16]; + b[..s.len()].copy_from_slice(s.as_bytes()); + b + }; + + // mach_header_64: 64-bit little-endian arm64 executable. + u32at(&mut f, 0, 0xfeed_facf); + u32at(&mut f, 4, 0x0100_000c); + u32at(&mut f, 12, 2); + u32at(&mut f, 16, 2); + u32at(&mut f, 20, 2 * (72 + 4 * 80)); + + let seg = |f: &mut Vec, + at: u64, + name: &str, + vmaddr: u64, + fileoff: u64, + prot: u32, + sects: &[(&str, u64, u64, u32)]| { + u32at(f, at, 0x19); + u32at(f, at + 4, 72 + 80 * sects.len() as u32); + put(f, at + 8, &name16(name)); + u64at(f, at + 24, vmaddr); + u64at(f, at + 32, 0x1000); + u64at(f, at + 40, fileoff); + u64at(f, at + 48, 0x1000); + u32at(f, at + 56, 7); + u32at(f, at + 60, prot); + u32at(f, at + 64, sects.len() as u32); + for (i, (sn, addr, size, flags)) in sects.iter().enumerate() { + let s = at + 72 + 80 * i as u64; + put(f, s, &name16(sn)); + put(f, s + 16, &name16(name)); + u64at(f, s + 32, *addr); + u64at(f, s + 40, *size); + u32at(f, s + 48, (addr - MACHO_BASE) as u32); + u32at(f, s + 64, *flags); + } + }; + seg( + &mut f, + 32, + "__TEXT", + TEXT, + 0, + 5, + &[ + ("__text", TEXT + 0x400, 0x100, 0x8000_0400), + ("__objc_methname", TEXT + 0x600, 0x40, 2), + ("__objc_classname", TEXT + 0x680, 0x20, 2), + ("__objc_methtype", TEXT + 0x6c0, 0x20, 2), + ], + ); + seg( + &mut f, + 32 + 72 + 4 * 80, + "__DATA", + DATA, + 0x1000, + 3, + &[ + ("__objc_classlist", DATA, 0x10, 0), + ("__objc_data", DATA + 0x40, 0x100, 0), + ("__objc_const", DATA + 0x200, 0x400, 0), + ("__objc_selrefs", DATA + 0x600, 8, 0), + ], + ); + + put(&mut f, 0x600, b"count\0length\0make\0tiny\0"); + put(&mut f, 0x680, b"Greeter\0Small\0"); + put(&mut f, 0x6c0, b"i16@0:8\0"); + let sel = |off: u64| TEXT + 0x600 + off; + let types = TEXT + 0x6c0; + + // classlist, then the four class objects it reaches. + u64at(&mut f, 0x1000, DATA + 0x40); + u64at(&mut f, 0x1008, DATA + 0x90); + for (class, meta, ro_at) in [(0x40u64, 0x68u64, 0x200u64), (0x90, 0xb8, 0x300)] { + u64at(&mut f, 0x1000 + class, DATA + meta); + u64at(&mut f, 0x1000 + class + 32, DATA + ro_at); + u64at(&mut f, 0x1000 + meta + 32, DATA + ro_at + 0x80); + } + // class_ro_t: flags, then the name and method list behind the fixed head. + for (ro, meta_flag, name, methods) in [ + (0x200u64, 0u32, 0u64, Some(0x400u64)), + (0x280, 1, 0, Some(0x480)), + (0x300, 0, 8, Some(0x500)), + (0x380, 1, 8, None), + ] { + u32at(&mut f, 0x1000 + ro, meta_flag); + u64at(&mut f, 0x1000 + ro + 24, TEXT + 0x680 + name); + if let Some(m) = methods { + u64at(&mut f, 0x1000 + ro + 32, DATA + m); + } + } + // Two pointer-sized method lists. + for (list, entries) in [ + (0x400u64, vec![(0u64, 0x400u64), (6, 0x420)]), + (0x480, vec![(13, 0x440)]), + ] { + u32at(&mut f, 0x1000 + list, 24); + u32at(&mut f, 0x1000 + list + 4, entries.len() as u32); + for (i, (name, imp)) in entries.iter().enumerate() { + let e = 0x1000 + list + 8 + 24 * i as u64; + u64at(&mut f, e, sel(*name)); + u64at(&mut f, e + 8, types); + u64at(&mut f, e + 16, TEXT + imp); + } + } + // One small method list, whose fields are displacements from themselves + // and whose name field reaches a selector reference rather than the text. + u32at(&mut f, 0x1500, 12 | 0x8000_0000); + u32at(&mut f, 0x1504, 1); + u64at(&mut f, 0x1600, sel(18)); + let entry = DATA + 0x508; + u32at(&mut f, 0x1508, (DATA + 0x600).wrapping_sub(entry) as u32); + u32at(&mut f, 0x150c, types.wrapping_sub(entry + 4) as u32); + u32at( + &mut f, + 0x1510, + (TEXT + 0x460).wrapping_sub(entry + 8) as u32, + ); + f +} + +#[test] +fn objc_class_lists_name_every_implementation() { + let bytes = synth_objc(); + let obj = load(&bytes, &LoadOptions::default()).expect("the synthesized image does not load"); + let classes = metadata::objc_classes(&obj); + assert_eq!(classes.len(), 2); + assert_eq!(classes[0].name, "Greeter"); + assert_eq!(classes[1].name, "Small"); + + let hints: BTreeMap = classes + .iter() + .flat_map(|c| c.hints()) + .map(|h| (h.addr.get(), h.name.unwrap())) + .collect(); + let want = [ + (TEXT + 0x400, "-[Greeter count]"), + (TEXT + 0x420, "-[Greeter length]"), + (TEXT + 0x440, "+[Greeter make]"), + (TEXT + 0x460, "-[Small tiny]"), + ]; + assert_eq!(hints.len(), want.len(), "{hints:#?}"); + for (addr, name) in want { + assert_eq!(hints.get(&addr).map(String::as_str), Some(name)); + } + assert!( + classes[0].methods.iter().all(|m| m.types == "i16@0:8"), + "the type encodings did not come through" + ); + assert!( + metadata::read(&obj) + .notes + .get("objc.methods") + .is_some_and(|n| n == "4"), + "the summary disagrees with the class list" + ); +} + +#[test] +fn a_relocatable_objc_object_yields_nothing_rather_than_zeros() { + let Some(obj) = open("greeter.macho.a64.o") else { + return; + }; + assert!( + obj.sections + .iter() + .any(|s| s.name.ends_with("__objc_classlist")), + "the fixture has no ObjC metadata to be careful about" + ); + // Every pointer in a relocatable object is still a relocation, so the + // class list is all zeros and the honest answer is no classes at all. + assert!(metadata::objc_classes(&obj).is_empty()); +} + +#[test] +fn a_corrupt_objc_image_never_hangs() { + let full = synth_objc(); + let start = Instant::now(); + for i in (0x1000..full.len()).step_by(7) { + let mut bad = full.clone(); + bad[i] = 0xff; + bad[i - 1] = 0xff; + if let Ok(obj) = load(&bad, &LoadOptions::default()) { + let _ = metadata::read(&obj); + } + } + assert!(start.elapsed() < Duration::from_secs(30)); +} + +#[test] +fn mutated_real_files_are_read_or_refused_but_never_hang() { + // The loader-level fuzz gate cannot reach these readers until a loader + // calls them, so the same contract is checked here: any bytes in, a value + // or nothing out, inside a budget. + let mut seed = 0x2545_f491_4f6c_dd1du64; + let mut rng = move || { + seed ^= seed >> 12; + seed ^= seed << 25; + seed ^= seed >> 27; + seed.wrapping_mul(0x2545_f491_4f6c_dd1d) + }; + let start = Instant::now(); + for name in ["hello.go.stripped", "panicky", "greeter.macho.a64.o"] { + let Some(path) = corpus().map(|d| d.join(name)) else { + return; + }; + let Ok(data) = std::fs::read(&path) else { + continue; + }; + // Debug information is off: parsing a megabyte of DWARF on every + // iteration would make this a test of `dwarf.rs` instead. + let opts = LoadOptions { + debug_info: false, + ..LoadOptions::default() + }; + for _ in 0..32 { + let mut bad = data.clone(); + for _ in 0..16 { + let i = (rng() % bad.len() as u64) as usize; + bad[i] = rng() as u8; + } + if let Ok(obj) = load(&bad, &opts) { + let found = metadata::read(&obj); + for p in &found.panics { + assert!(p.file.ends_with(".rs")); + } + } + assert!( + start.elapsed() < Duration::from_secs(60), + "{name} is too slow" + ); + } + } +} + +#[test] +fn a_binary_with_no_section_headers_still_gives_up_its_table() { + let Some(path) = corpus().map(|d| d.join("hello.go.stripped")) else { + return; + }; + let Ok(mut data) = std::fs::read(&path) else { + return; + }; + // Remove the section headers the way a packer does: the segments still + // map, so a scan is the only way left to find anything. + data[0x28..0x30].copy_from_slice(&0u64.to_le_bytes()); + data[0x3c..0x3e].copy_from_slice(&0u16.to_le_bytes()); + data[0x3e..0x40].copy_from_slice(&0u16.to_le_bytes()); + let obj = load(&data, &LoadOptions::default()).expect("headerless ELF does not load"); + assert!( + obj.sections.is_empty(), + "the section headers are still there" + ); + + let t = metadata::go_pclntab(&obj).expect("the scan missed the table"); + assert!(t.functions.len() > 500); + assert!(t.functions.iter().any(|f| f.name == "main.main")); + assert!(t.build_version.is_some_and(|v| v.starts_with("go1."))); +} diff --git a/crates/e5r-format/tests/overlay.rs b/crates/e5r-format/tests/overlay.rs new file mode 100644 index 0000000..2576c86 --- /dev/null +++ b/crates/e5r-format/tests/overlay.rs @@ -0,0 +1,261 @@ +//! Overlay, entropy and packer-evidence tests. +//! +//! The one that matters most is the false-positive check: an ordinary compiled +//! binary must produce no finding an analyst would act on. A detector that +//! fires on everything is the same as no detector. +//! +//! Skipped when the corpus is absent; run `scripts/build-fixtures.sh`. + +use std::path::{Path, PathBuf}; + +use e5r_core::Strength; +use e5r_format::overlay::{self, Content, Kind}; +use e5r_format::{LoadOptions, Object, load}; + +/// Fixtures that are whole images rather than relocatable objects, one per +/// container the overlay walk knows how to measure. +const IMAGES: &[&str] = &[ + "hello.a64.O0", + "hello.a64.O2", + "hello.a64.O0.stripped", + "driver.x64.O2", + "driver.a64.O2", + "hello.go", + "panicky", +]; + +/// Relocatable objects, whose symbol, string and relocation tables sit past +/// the last section and are the easiest way to invent an overlay that is not +/// there. +const OBJECTS: &[&str] = &[ + "shapes.a64.O0.o", + "shapes.x64.O2.o", + "shapes.coff.o", + "wide.coff.o", + "shapes.macho.a64.o", + "shapes.macho.x64.o", + "greeter.macho.a64.o", +]; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option<(Object, Vec)> { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = load(&data, &LoadOptions::default()).ok()?; + Some((obj, data)) +} + +/// A deterministic byte stream, so a failure reproduces. +fn pseudorandom(n: usize) -> Vec { + let mut s = 0x243f_6a88_85a3_08d3u64; + (0..n) + .map(|_| { + s ^= s >> 12; + s ^= s << 25; + s ^= s >> 27; + (s.wrapping_mul(0x2545_f491_4f6c_dd1d) >> 33) as u8 + }) + .collect() +} + +#[test] +fn a_clean_binary_has_no_overlay() { + let mut checked = 0; + for name in IMAGES.iter().chain(OBJECTS) { + let Some((obj, data)) = open(name) else { + continue; + }; + let r = overlay::analyze(&obj, &data); + assert!( + r.overlay.is_none(), + "{name}: {} bytes of overlay at {:#x}, but nothing was appended", + r.overlay.as_ref().map(|o| o.size).unwrap_or(0), + r.described_end + ); + checked += 1; + } + assert!(checked > 0, "no images in the corpus"); +} + +#[test] +fn appended_bytes_are_found_at_the_right_offset() { + let Some((_, data)) = open("hello.a64.O0") else { + return; + }; + let tail = pseudorandom(9000); + let mut grown = data.clone(); + grown.extend_from_slice(&tail); + let obj = load(&grown, &LoadOptions::default()).unwrap(); + let r = overlay::analyze(&obj, &grown); + let o = r.overlay.expect("appended bytes were not reported"); + assert_eq!(o.offset, data.len() as u64); + assert_eq!(o.size, tail.len() as u64); + assert!(o.entropy > 7.5, "{}", o.entropy); + assert_eq!(o.content, Content::HighEntropy); + assert!(r.findings.iter().any(|f| f.kind == Kind::OverlayPresent)); +} + +#[test] +fn an_appended_container_is_named() { + let Some((_, data)) = open("hello.a64.O0") else { + return; + }; + for (magic, want) in [ + (b"PK\x03\x04".as_slice(), Content::Zip), + (b"MSCF".as_slice(), Content::Cabinet), + (b"\x7fELF".as_slice(), Content::Elf), + ] { + let mut grown = data.clone(); + grown.extend_from_slice(magic); + grown.extend_from_slice(&pseudorandom(4096)); + let obj = load(&grown, &LoadOptions::default()).unwrap(); + let o = overlay::analyze(&obj, &grown).overlay.expect("no overlay"); + assert_eq!(o.content, want); + assert_eq!(o.offset, data.len() as u64); + } +} + +#[test] +fn a_run_of_zeros_scores_nothing_and_random_bytes_score_eight() { + let Some((_, data)) = open("hello.a64.O0") else { + return; + }; + let mut zeros = data.clone(); + zeros.extend_from_slice(&[0u8; 16384]); + let obj = load(&zeros, &LoadOptions::default()).unwrap(); + let o = overlay::analyze(&obj, &zeros).overlay.unwrap(); + assert!(o.entropy < 0.01, "zeros scored {}", o.entropy); + + let mut noise = data.clone(); + noise.extend_from_slice(&pseudorandom(1 << 18)); + let obj = load(&noise, &LoadOptions::default()).unwrap(); + let o = overlay::analyze(&obj, &noise).overlay.unwrap(); + assert!(o.entropy > 7.9, "noise scored {}", o.entropy); +} + +#[test] +fn the_entropy_map_localizes_a_packed_window() { + let Some((obj, data)) = open("hello.a64.O0") else { + return; + }; + let Some(text) = obj.section(".text") else { + return; + }; + // Replace one four-kilobyte window of .text with noise. The section + // average moves a little; the window it happened in moves to the top. + if text.file_size < 3 * overlay::WINDOW { + return; + } + let at = (text.file_offset + overlay::WINDOW) as usize; + let mut packed = data.clone(); + packed[at..at + overlay::WINDOW as usize] + .copy_from_slice(&pseudorandom(overlay::WINDOW as usize)); + let obj = load(&packed, &LoadOptions::default()).unwrap(); + let r = overlay::analyze(&obj, &packed); + let se = r + .sections + .iter() + .find(|s| s.name == ".text") + .expect("no .text in the map"); + let peak = se.peak().expect("no windows measured"); + assert_eq!(peak.offset, at as u64, "the noisy window was not the peak"); + assert!(peak.entropy > 7.5, "{}", peak.entropy); + assert!( + peak.entropy > se.entropy, + "a window should stand out from its section average" + ); +} + +#[test] +fn section_entropy_covers_every_section_and_clips_to_the_file() { + let Some((obj, data)) = open("hello.a64.O0") else { + return; + }; + let r = overlay::analyze(&obj, &data); + assert_eq!(r.sections.len(), obj.sections.len()); + for s in &r.sections { + assert!(s.file_offset + s.size <= data.len() as u64); + assert!((0.0..=8.0).contains(&s.entropy)); + let measured: u64 = s.windows.iter().map(|w| w.size).sum(); + assert!(measured == s.size || s.windows.is_empty()); + } + assert!(r.warnings.is_empty(), "{:?}", r.warnings); +} + +#[test] +fn a_normal_binary_produces_no_strong_evidence() { + let mut checked = 0; + for name in IMAGES.iter().chain(OBJECTS) { + let Some((obj, data)) = open(name) else { + continue; + }; + let r = overlay::analyze(&obj, &data); + let strong: Vec = r + .at_least(Strength::Inferred) + .map(|f| format!("{}: {}", f.kind, f.detail)) + .collect(); + assert!(strong.is_empty(), "{name} produced {strong:#?}"); + checked += 1; + } + assert!(checked > 0, "no images in the corpus"); +} + +#[test] +fn a_packer_name_is_only_claimed_when_it_is_a_signature() { + let Some((mut obj, data)) = open("hello.a64.O0") else { + return; + }; + // Rename a section to a signature the module knows, which is the only + // case where naming a product is a fact rather than a guess. + obj.sections[1].name = "UPX1".into(); + let f = overlay::evidence(&obj, &data, &[], None); + let named: Vec<&overlay::Finding> = f + .iter() + .filter(|f| f.kind == Kind::PackerSectionName) + .collect(); + assert_eq!(named.len(), 1); + assert!(named[0].detail.contains("UPX")); + // And nothing anywhere says the file is packed. + assert!( + !f.iter() + .any(|f| f.detail.to_lowercase().contains("is packed")), + "a verdict leaked into the evidence" + ); +} + +#[test] +fn evidence_is_ordered_strongest_first_and_is_deterministic() { + let Some((obj, data)) = open("hello.a64.O0") else { + return; + }; + let (map, _) = overlay::section_entropy(&obj, &data); + let a = overlay::evidence(&obj, &data, &map, None); + let b = overlay::evidence(&obj, &data, &map, None); + assert_eq!(a, b); + assert!(a.windows(2).all(|w| w[0].strength >= w[1].strength)); +} + +#[test] +fn analysis_of_hostile_bytes_returns() { + let Some((obj, data)) = open("hello.a64.O0") else { + return; + }; + // A section table that claims the world, over bytes that are not there. + let mut hostile = obj.clone(); + for s in &mut hostile.sections { + s.file_offset = u64::MAX - 1024; + s.file_size = u64::MAX / 2; + } + let started = std::time::Instant::now(); + let r = overlay::analyze(&hostile, &data); + assert!(r.sections.iter().all(|s| s.size == 0)); + assert!( + r.findings + .iter() + .any(|f| f.kind == Kind::SectionOutsideFile) + ); + assert!(started.elapsed() < std::time::Duration::from_secs(2)); +} diff --git a/crates/e5r-format/tests/pdb.rs b/crates/e5r-format/tests/pdb.rs new file mode 100644 index 0000000..6b010d3 --- /dev/null +++ b/crates/e5r-format/tests/pdb.rs @@ -0,0 +1,1415 @@ +//! Reading a Windows program database. +//! +//! Two gates, and they measure different things. +//! +//! The first builds a database byte by byte the way the PE tests build an +//! image. A synthesized database pins exactly which bytes the reader is +//! supposed to have read, so an assertion fails when the layout is misread +//! rather than when a tool is missing, and it is the only gate a checkout with +//! no corpus can run. +//! +//! The second reads the databases `scripts/build-fixtures.sh` produces with +//! clang and rust-lld, and compares them against `llvm-pdbutil`, record by +//! record rather than by counting. An earlier note here said this machine +//! could not produce a database; that was wrong. It has no Windows linker and +//! no CRT, but `-gcodeview` and lld's `/debug` need neither. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, Instant}; + +use e5r_core::{Addr, AddrRange, Evidence, Strength}; +use e5r_format::dwarf::Location; +use e5r_format::{Section, pdb}; + +/// A little-endian writer, so the builder reads like the layout it produces. +#[derive(Default, Clone)] +struct Buf(Vec); + +impl Buf { + fn u8(&mut self, v: u8) -> &mut Self { + self.0.push(v); + self + } + fn u16(&mut self, v: u16) -> &mut Self { + self.0.extend_from_slice(&v.to_le_bytes()); + self + } + fn u32(&mut self, v: u32) -> &mut Self { + self.0.extend_from_slice(&v.to_le_bytes()); + self + } + fn i32(&mut self, v: i32) -> &mut Self { + self.0.extend_from_slice(&v.to_le_bytes()); + self + } + fn bytes(&mut self, v: &[u8]) -> &mut Self { + self.0.extend_from_slice(v); + self + } + fn cstr(&mut self, v: &str) -> &mut Self { + self.0.extend_from_slice(v.as_bytes()); + self.0.push(0); + self + } + /// Pad to a four byte boundary with the padding leaves a type record uses, + /// which say how far to step rather than being skipped by alignment. + fn pad_leaf(&mut self) -> &mut Self { + let n = (4 - self.0.len() % 4) % 4; + for i in (1..=n).rev() { + self.0.push(0xf0 + i as u8); + } + self + } + fn pad_zero(&mut self) -> &mut Self { + while self.0.len() % 4 != 0 { + self.0.push(0); + } + self + } +} + +const IMAGE_BASE: u64 = 0x1_4000_0000; +const TEXT_RVA: u64 = 0x1000; +const DATA_RVA: u64 = 0x2000; +const BLOCK: usize = 1024; +const GUID: [u8; 16] = [ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, +]; + +// Stream numbers this builder uses. The first five are fixed by the format. +const GLOBALS: u16 = 5; +const MODULE: u16 = 6; +const NAMES: u16 = 7; +const HEADERS: u16 = 8; + +// Type indices, in the order the records are written. +const T_INT4: u32 = 0x0074; +const T_UQUAD: u32 = 0x0023; +const T_REAL32: u32 = 0x0040; +const POINT_FIELDS: u32 = 0x1000; +const POINT: u32 = 0x1001; +const POINT_PTR: u32 = 0x1002; +const ARGS: u32 = 0x1003; +const ADD_PROC: u32 = 0x1004; +const COLOUR_FIELDS: u32 = 0x1005; +const COLOUR: u32 = 0x1006; +const VALUE_FIELDS: u32 = 0x1007; +const VALUE: u32 = 0x1008; +const TABLE: u32 = 0x1009; +const KONST: u32 = 0x100a; +const POINT_FORWARD: u32 = 0x100b; + +/// One record: the length, the kind, and the payload the kind describes. +fn record(kind: u16, payload: &[u8], leaf_padding: bool) -> Vec { + let mut body = Buf::default(); + body.u16(kind).bytes(payload); + if leaf_padding { + body.pad_leaf(); + } else { + body.pad_zero(); + } + let mut out = Buf::default(); + out.u16(body.0.len() as u16).bytes(&body.0); + out.0 +} + +fn type_record(kind: u16, payload: &[u8]) -> Vec { + record(kind, payload, true) +} + +fn symbol(kind: u16, payload: &[u8]) -> Vec { + record(kind, payload, false) +} + +/// One entry of a field list, padded the way the next entry expects to find. +fn entry(kind: u16, payload: &[u8]) -> Vec { + let mut out = Buf::default(); + out.u16(kind).bytes(payload).pad_leaf(); + out.0 +} + +/// The type stream: enough of a graph to exercise every leaf the reader knows. +fn tpi() -> Vec { + let mut records = Buf::default(); + let member = |ty: u32, offset: u16, name: &str| { + let mut b = Buf::default(); + b.u16(3).u32(ty).u16(offset).cstr(name); + entry(0x150d, &b.0) + }; + let enumerate = |value: u16, name: &str| { + let mut b = Buf::default(); + b.u16(3).u16(value).cstr(name); + entry(0x1502, &b.0) + }; + + // The members of Point, then Point itself. + let mut fields = Buf::default(); + fields + .bytes(&member(T_INT4, 0, "x")) + .bytes(&member(T_INT4, 4, "y")); + records.bytes(&type_record(0x1203, &fields.0)); + let mut point = Buf::default(); + point + .u16(2) + .u16(0) + .u32(POINT_FIELDS) + .u32(0) + .u32(0) + .u16(8) + .cstr("Point"); + records.bytes(&type_record(0x1505, &point.0)); + + // A pointer to it, an argument list holding that, and the procedure. + let mut pointer = Buf::default(); + pointer.u32(POINT).u32(0x0004_000c); + records.bytes(&type_record(0x1002, &pointer.0)); + let mut args = Buf::default(); + args.u32(1).u32(POINT_PTR); + records.bytes(&type_record(0x1201, &args.0)); + let mut procedure = Buf::default(); + procedure.u32(T_INT4).u8(0).u8(0).u16(1).u32(ARGS); + records.bytes(&type_record(0x1008, &procedure.0)); + + // An enumeration. + let mut values = Buf::default(); + values + .bytes(&enumerate(0, "RED")) + .bytes(&enumerate(1, "GREEN")); + records.bytes(&type_record(0x1203, &values.0)); + let mut colour = Buf::default(); + colour + .u16(2) + .u16(0) + .u32(T_INT4) + .u32(COLOUR_FIELDS) + .cstr("Colour"); + records.bytes(&type_record(0x1507, &colour.0)); + + // A union, whose members both start at zero. + let mut both = Buf::default(); + both.bytes(&member(T_INT4, 0, "i")) + .bytes(&member(T_REAL32, 0, "f")); + records.bytes(&type_record(0x1203, &both.0)); + let mut union = Buf::default(); + union.u16(2).u16(0).u32(VALUE_FIELDS).u16(4).cstr("Value"); + records.bytes(&type_record(0x1506, &union.0)); + + // An array, which declares a size in bytes where C declares a count. + let mut array = Buf::default(); + array.u32(T_INT4).u32(T_UQUAD).u16(40).cstr(""); + records.bytes(&type_record(0x1503, &array.0)); + + // A qualifier, which changes nothing about the layout. + let mut modifier = Buf::default(); + modifier.u32(T_INT4).u16(1); + records.bytes(&type_record(0x1001, &modifier.0)); + + // A forward reference to Point, which names it and nothing else. + let mut forward = Buf::default(); + forward + .u16(0) + .u16(0x80) + .u32(0) + .u32(0) + .u32(0) + .u16(0) + .cstr("Point"); + records.bytes(&type_record(0x1505, &forward.0)); + + let mut out = Buf::default(); + out.u32(20040203) + .u32(56) + .u32(POINT_FIELDS) + .u32(POINT_FORWARD + 1) + .u32(records.0.len() as u32) + .u16(0xffff) + .u16(0xffff) + .u32(4) + .u32(0x3ffff) + .u32(0) + .u32(0) + .u32(0) + .u32(0) + .u32(0) + .u32(0); + assert_eq!(out.0.len(), 56, "type stream header"); + out.bytes(&records.0); + out.0 +} + +/// The item stream, where a modern compiler puts the name and type of a +/// procedure the symbol record only points at. +fn ipi() -> Vec { + let mut identity = Buf::default(); + identity.u32(0).u32(ADD_PROC).cstr("add"); + let records = type_record(0x1601, &identity.0); + let mut out = Buf::default(); + out.u32(20040203) + .u32(56) + .u32(0x1000) + .u32(0x1001) + .u32(records.len() as u32) + .u16(0xffff) + .u16(0xffff) + .u32(4) + .u32(0x3ffff) + .u32(0) + .u32(0) + .u32(0) + .u32(0) + .u32(0) + .u32(0); + out.bytes(&records); + out.0 +} + +/// The module's symbols: one procedure with a local, then its globals. +fn module_symbols() -> Vec { + let mut out = Buf::default(); + out.u32(4); // C13 line information follows the symbols + let mut procedure = Buf::default(); + procedure + .u32(0) + .u32(0) + .u32(0) + .u32(0x20) + .u32(0) + .u32(0x20) + .u32(0x1000) // the item stream's identity record + .u32(0) + .u16(1) + .u8(0) + .cstr("add"); + out.bytes(&symbol(0x1147, &procedure.0)); + let mut local = Buf::default(); + local.i32(16).u32(POINT_PTR).u16(335).cstr("p"); + out.bytes(&symbol(0x1111, &local.0)); + out.bytes(&symbol(0x0006, &[])); + + let mut data = |ty: u32, offset: u32, name: &str| { + let mut b = Buf::default(); + b.u32(ty).u32(offset).u16(2).cstr(name); + out.bytes(&symbol(0x110d, &b.0)); + }; + data(COLOUR, 0x00, "colour"); + data(TABLE, 0x40, "table"); + data(VALUE, 0x80, "value"); + data(KONST, 0xc0, "konst"); + data(POINT_FORWARD, 0x100, "shape"); + out.0 +} + +/// The line information: which file, and which line each address came from. +fn module_lines() -> Vec { + let mut out = Buf::default(); + let mut checksum = Buf::default(); + checksum.u32(1).u8(0).u8(0).pad_zero(); + out.u32(0xf4) + .u32(checksum.0.len() as u32) + .bytes(&checksum.0); + + let mut block = Buf::default(); + block + .u32(0) // offset into the section + .u16(1) // the section + .u16(0) // no columns + .u32(0x20) // bytes of code covered + .u32(0) // the checksum entry naming the file + .u32(2) // rows + .u32(12 + 2 * 8) + .u32(0) + .u32(42 | 0x8000_0000) + .u32(8) + .u32(43 | 0x8000_0000); + out.u32(0xf2).u32(block.0.len() as u32).bytes(&block.0); + out.0 +} + +/// The stream the debug directory's GUID is matched against. +fn info() -> Vec { + let mut out = Buf::default(); + out.u32(20000404).u32(0x5f5e100).u32(1).bytes(&GUID); + let mut names = Buf::default(); + names.cstr("/names"); + out.u32(names.0.len() as u32).bytes(&names.0); + out.u32(1) // entries in use + .u32(1) // capacity + .u32(1) // one word of the present vector + .u32(1) + .u32(0) // no deleted words + .u32(0) // the name at offset zero + .u32(NAMES as u32); + out.0 +} + +/// The DBI stream: where the symbols are, and which modules there were. +fn dbi(symbol_bytes: usize, line_bytes: usize) -> Vec { + let mut module = Buf::default(); + module + .u32(0) + .u16(1) + .u16(0) + .i32(0) + .i32(0x20) + .u32(0x6000_0020) + .u16(0) + .u16(0) + .u32(0) + .u32(0) + .u16(0) // flags + .u16(MODULE) + .u32(symbol_bytes as u32) + .u32(0) + .u32(line_bytes as u32) + .u16(1) + .u16(0) + .u32(0) + .u32(0) + .u32(0) + .cstr("main.obj") + .cstr("main.obj") + .pad_zero(); + + let mut optional = Buf::default(); + for n in 0..11u16 { + optional.u16(if n == 5 { HEADERS } else { 0xffff }); + } + + let mut out = Buf::default(); + out.i32(-1) + .u32(19990903) + .u32(1) + .u16(0xffff) // globals + .u16(0) + .u16(0xffff) // publics + .u16(0) + .u16(GLOBALS) + .u16(0) + .i32(module.0.len() as i32) + .i32(0) // section contributions + .i32(0) // section map + .i32(0) // source files + .i32(0) // type servers + .u32(0) + .i32(optional.0.len() as i32) + .i32(0) // edit and continue + .u16(0) + .u16(0x8664) + .u32(0); + assert_eq!(out.0.len(), 64, "DBI header"); + out.bytes(&module.0).bytes(&optional.0); + out.0 +} + +/// The global records, which name addresses and say nothing about types. +fn globals() -> Vec { + let mut out = Buf::default(); + let mut public = Buf::default(); + public.u32(2).u32(0x30).u16(1).cstr("?add@@YAHPEAUPoint@@Z"); + out.bytes(&symbol(0x110e, &public.0)); + let mut data = Buf::default(); + data.u32(0).u32(0x200).u16(2).cstr("g_counter"); + out.bytes(&symbol(0x110e, &data.0)); + out.0 +} + +/// The linker's copy of the image's section headers. +fn section_headers() -> Vec { + let mut out = Buf::default(); + for (name, rva) in [(b".text\0\0\0", TEXT_RVA), (b".data\0\0\0", DATA_RVA)] { + out.bytes(name) + .u32(0x1000) + .u32(rva as u32) + .u32(0x1000) + .u32(0x400) + .u32(0) + .u32(0) + .u16(0) + .u16(0) + .u32(0x6000_0020); + } + out.0 +} + +/// The string table the line information indexes into. +fn names() -> Vec { + let mut buffer = Buf::default(); + buffer.u8(0).cstr("c:\\src\\main.c"); + let mut out = Buf::default(); + out.u32(0xeffe_effe) + .u32(1) + .u32(buffer.0.len() as u32) + .bytes(&buffer.0); + out.0 +} + +/// A whole container: the superblock, the blocks, and the directory that says +/// which of them each stream is scattered across. +fn synth_pdb() -> Vec { + let symbols = module_symbols(); + let lines = module_lines(); + let mut module = symbols.clone(); + module.extend_from_slice(&lines); + let streams = vec![ + Vec::new(), // the previous directory + info(), + tpi(), + dbi(symbols.len(), lines.len()), + ipi(), + globals(), + module, + names(), + section_headers(), + ]; + + // Blocks zero to two are the superblock and the two free block maps. + let mut next = 3usize; + let mut blocks: Vec> = Vec::new(); + for s in &streams { + let n = s.len().div_ceil(BLOCK); + blocks.push((0..n).map(|i| (next + i) as u32).collect()); + next += n; + } + let mut directory = Buf::default(); + directory.u32(streams.len() as u32); + for s in &streams { + directory.u32(s.len() as u32); + } + for b in &blocks { + for n in b { + directory.u32(*n); + } + } + let map: Vec = (0..directory.0.len().div_ceil(BLOCK)) + .map(|i| (next + i) as u32) + .collect(); + next += map.len(); + let map_block = next; + next += 1; + + let mut file = vec![0u8; next * BLOCK]; + let mut superblock = Buf::default(); + superblock + .bytes(b"Microsoft C/C++ MSF 7.00\r\n\x1aDS\0\0\0") + .u32(BLOCK as u32) + .u32(1) + .u32(next as u32) + .u32(directory.0.len() as u32) + .u32(0) + .u32(map_block as u32); + assert_eq!(superblock.0.len(), 56, "superblock"); + file[..superblock.0.len()].copy_from_slice(&superblock.0); + for (s, b) in streams.iter().zip(&blocks) { + write_blocks(&mut file, b, s); + } + write_blocks(&mut file, &map, &directory.0); + let mut list = Buf::default(); + for n in &map { + list.u32(*n); + } + let at = map_block * BLOCK; + file[at..at + list.0.len()].copy_from_slice(&list.0); + file +} + +fn write_blocks(file: &mut [u8], blocks: &[u32], data: &[u8]) { + for (n, chunk) in blocks.iter().zip(data.chunks(BLOCK)) { + let at = *n as usize * BLOCK; + file[at..at + chunk.len()].copy_from_slice(chunk); + } +} + +fn section(name: &str, rva: u64) -> Section { + Section { + name: name.to_string(), + range: AddrRange::sized(Addr(IMAGE_BASE + rva), 0x1000).unwrap(), + file_offset: 0x400, + file_size: 0x1000, + exec: name == ".text", + write: name == ".data", + kind: 0, + } +} + +fn image_sections() -> Vec
{ + vec![section(".text", TEXT_RVA), section(".data", DATA_RVA)] +} + +#[test] +fn the_identity_is_the_guid_and_the_age_the_image_asked_for() { + let data = synth_pdb(); + let id = pdb::identity(&data).expect("a database has an identity"); + assert_eq!(id.guid, GUID); + assert_eq!(id.age, 1); + assert!(id.matches(&GUID, 1)); + // A rebuild keeps the GUID and bumps the age, and is not the same file. + assert!(!id.matches(&GUID, 2)); + assert_eq!(id.key(), "443322116655887799AABBCCDDEEFF001"); +} + +#[test] +fn the_container_hands_back_the_streams_it_was_given() { + let data = synth_pdb(); + let msf = pdb::Msf::open(&data).expect("opens"); + assert_eq!(msf.count(), 9); + assert_eq!(msf.stream(NAMES as usize).unwrap(), names()); + assert_eq!(msf.stream(HEADERS as usize).unwrap(), section_headers()); + // A stream the directory does not name is not a stream. + assert!(msf.stream(9).is_none()); +} + +#[test] +fn a_procedure_comes_back_with_its_address_size_and_signature() { + let data = synth_pdb(); + let info = pdb::parse(&data, &image_sections()).expect("parses"); + let at = Addr(IMAGE_BASE + TEXT_RVA); + let f = info.functions.get(&at).expect("the procedure record"); + assert_eq!(f.name, "add"); + assert_eq!(f.size, Some(0x20)); + // The signature came from the item stream by way of the identity record. + let returns = f.signature.returns.expect("a return type"); + assert_eq!(info.types.name_of(returns), "int32_t"); + assert_eq!(f.signature.parameters.len(), 1); + let (_, parameter) = f.signature.parameters[0]; + assert_eq!(info.types.declare(parameter, "p"), "struct Point *p"); + // And the frame-relative local the scope held. + assert_eq!(f.locals.len(), 1); + assert_eq!(f.locals[0].name, "p"); + assert_eq!(f.locals[0].frame_offset, Some(16)); + assert_eq!(f.locals[0].ty, parameter); +} + +#[test] +fn the_type_graph_is_the_one_the_records_described() { + let data = synth_pdb(); + let info = pdb::parse(&data, &image_sections()).expect("parses"); + let variable = |name: &str| { + info.variables + .values() + .find(|v| v.name == name) + .unwrap_or_else(|| panic!("no variable {name}")) + }; + + let point = variable("shape"); + // A forward reference names a type; the members are on the record that + // defines it, and the reader has to have followed that. + assert_eq!( + info.types.definition(point.ty).unwrap(), + "struct Point { int32_t x; int32_t y; };" + ); + assert_eq!( + info.types.definition(variable("colour").ty).unwrap(), + "enum Colour { RED = 0, GREEN = 1 };" + ); + assert_eq!( + info.types.definition(variable("value").ty).unwrap(), + "union Value { int32_t i; float f; };" + ); + // The array record gave forty bytes of four byte elements. + assert_eq!( + info.types.declare(variable("table").ty, "table"), + "int32_t table[10]" + ); + // A qualifier does not change the layout, so it is not carried. + assert_eq!(info.types.name_of(variable("konst").ty), "int32_t"); +} + +#[test] +fn public_symbols_name_addresses_no_procedure_record_claimed() { + let data = synth_pdb(); + let info = pdb::parse(&data, &image_sections()).expect("parses"); + let public = info + .functions + .get(&Addr(IMAGE_BASE + TEXT_RVA + 0x30)) + .expect("the public"); + assert_eq!(public.name, "?add@@YAHPEAUPoint@@Z"); + assert_eq!(public.size, None); + // The procedure record says more about its address than the public does, + // so it is the one that survives. + assert_eq!(info.functions[&Addr(IMAGE_BASE + TEXT_RVA)].name, "add"); + let counter = info + .variables + .get(&Addr(IMAGE_BASE + DATA_RVA + 0x200)) + .expect("the public data symbol"); + assert_eq!(counter.name, "g_counter"); +} + +#[test] +fn line_rows_say_where_each_address_came_from() { + let data = synth_pdb(); + let info = pdb::parse(&data, &image_sections()).expect("parses"); + let at = Addr(IMAGE_BASE + TEXT_RVA); + let first = info.line_for(at).expect("a row at the entry"); + assert_eq!(first.file, "c:\\src\\main.c"); + assert_eq!(first.line, 42); + assert!(first.statement); + assert_eq!(info.line_for(Addr(at.get() + 8)).unwrap().line, 43); + assert_eq!(info.line_for(Addr(at.get() + 12)).unwrap().line, 43); + // Past the run the subsection covered there is no row, only its end. + assert!(info.line_for(Addr(at.get() + 0x20)).is_none()); + // The declaration site is in the line table rather than on the record. + let f = &info.functions[&at]; + assert_eq!(f.decl_file.as_deref(), Some("c:\\src\\main.c")); + assert_eq!(f.decl_line, Some(42)); +} + +#[test] +fn without_the_image_the_linkers_own_section_headers_are_used() { + let data = synth_pdb(); + let info = pdb::parse(&data, &[]).expect("parses"); + // The database knows only relative addresses, so that is what comes back. + let f = info.functions.get(&Addr(TEXT_RVA)).expect("the procedure"); + assert_eq!(f.name, "add"); + assert!(info.variables.contains_key(&Addr(DATA_RVA + 0x200))); + assert_eq!(info.units, 1); +} + +#[test] +fn truncation_at_every_length_is_refused_rather_than_survived() { + let data = synth_pdb(); + let started = Instant::now(); + for n in 0..data.len() { + // The contract is the loaders': a value or nothing, never a panic and + // never a loop that does not end. + let _ = pdb::parse(&data[..n], &image_sections()); + let _ = pdb::identity(&data[..n]); + } + // A prefix is never the whole file, so none of them can have parsed into + // the symbols the whole file has. + assert!( + pdb::parse(&data[..data.len() - 1], &[]) + .is_none_or(|i| i.functions.is_empty() || i.lines.is_empty()) + ); + assert!( + started.elapsed() < Duration::from_secs(30), + "truncation sweep took {:?}", + started.elapsed() + ); +} + +#[test] +fn corrupted_fields_are_refused_rather_than_believed() { + let data = synth_pdb(); + let started = Instant::now(); + // Every byte, set to the values that break parsers: a saturating count, a + // zero length, and a high bit that turns a length negative. + for n in 0..data.len() { + for value in [0xff, 0x00, 0x80] { + let mut case = data.clone(); + case[n] = value; + let _ = pdb::parse(&case, &image_sections()); + let _ = pdb::identity(&case); + } + } + assert!( + started.elapsed() < Duration::from_secs(60), + "corruption sweep took {:?}", + started.elapsed() + ); +} + +#[test] +fn a_lying_count_does_not_exhaust_memory() { + let data = synth_pdb(); + // The directory says four billion streams, and the superblock says the + // directory is four billion bytes long. Both are bounded by the size of + // the file, so both have to be refused rather than allocated for. + let mut case = data.clone(); + case[44..48].copy_from_slice(&u32::MAX.to_le_bytes()); + let started = Instant::now(); + assert!(pdb::Msf::open(&case).is_none()); + let mut case = data.clone(); + let map = u32::from_le_bytes([case[52], case[53], case[54], case[55]]) as usize; + let directory = u32::from_le_bytes([case[48 - 4], case[49 - 4], case[50 - 4], case[51 - 4]]); + assert!(directory > 0, "the directory has bytes"); + // The first word of the directory is the stream count. + let at = map * BLOCK; + let block = u32::from_le_bytes([case[at], case[at + 1], case[at + 2], case[at + 3]]) as usize; + case[block * BLOCK..block * BLOCK + 4].copy_from_slice(&u32::MAX.to_le_bytes()); + assert!(pdb::Msf::open(&case).is_none()); + assert!( + started.elapsed() < Duration::from_secs(2), + "a lying count took too long" + ); +} + +#[test] +fn arbitrary_bytes_are_not_a_database() { + assert!(pdb::parse(&[], &[]).is_none()); + assert!(pdb::parse(b"not a program database at all", &[]).is_none()); + let mut rng = 0x5eed_1234_abcd_0001u64; + let mut next = move || { + rng ^= rng >> 12; + rng ^= rng << 25; + rng ^= rng >> 27; + rng.wrapping_mul(0x2545_f491_4f6c_dd1d) + }; + let started = Instant::now(); + for _ in 0..2000 { + // The magic, so the probe actually reaches the container. + let mut case = b"Microsoft C/C++ MSF 7.00\r\n\x1aDS\0\0\0".to_vec(); + while case.len() < 2048 { + case.extend_from_slice(&next().to_le_bytes()); + } + let _ = pdb::parse(&case, &image_sections()); + } + assert!( + started.elapsed() < Duration::from_secs(20), + "random input took {:?}", + started.elapsed() + ); +} + +#[test] +fn a_real_database_reads_when_one_is_present() { + // There is no Windows toolchain here, so this is a fixture only when + // somebody points at one. Absent, the synthesized container is the gate. + let Ok(path) = std::env::var("E5R_PDB") else { + return; + }; + let data = std::fs::read(&path).expect("the named database"); + let id = pdb::identity(&data).expect("an identity"); + let info = pdb::parse(&data, &[]).expect("parses"); + println!( + "{path}: {} age {}, {} functions, {} variables, {} rows, {} types", + id.key(), + id.age, + info.functions.len(), + info.variables.len(), + info.lines.len(), + info.types.len() + ); + assert!(!info.is_empty(), "a real database describes something"); +} + +// The corpus gate. Everything below reads databases a real producer wrote and +// compares them against llvm-pdbutil, which is the only external tool on this +// machine that reads the format. + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// Every fixture database, or an empty list when the corpus is not built. +fn fixtures() -> Vec<(String, Vec)> { + let Some(dir) = corpus() else { + return Vec::new(); + }; + let mut out = Vec::new(); + for arch in ["x64", "a64"] { + for opt in ["O0", "O2"] { + let name = format!("pdb.{arch}.{opt}.pdb"); + if let Ok(data) = std::fs::read(dir.join(&name)) { + out.push((name, data)); + } + } + } + out +} + +/// Ask llvm-pdbutil, so the expectations are not our own reader's opinion. +/// `None` when it is not installed, which downgrades the gate to the +/// synthesized database rather than failing. +fn pdbutil(name: &str, args: &[&str]) -> Option { + let p = corpus()?.join(name); + for tool in [ + "llvm-pdbutil", + "llvm-pdbutil-20", + "llvm-pdbutil-19", + "llvm-pdbutil-18", + "llvm-pdbutil-17", + "llvm-pdbutil-16", + "llvm-pdbutil-15", + ] { + let Ok(out) = Command::new(tool).arg("dump").args(args).arg(&p).output() else { + continue; + }; + if out.status.success() { + return Some(String::from_utf8_lossy(&out.stdout).into_owned()); + } + } + None +} + +/// Where each section starts, from the oracle rather than from us, so a +/// `0001:0010` in its output can be compared with an address in ours. +fn oracle_sections(name: &str) -> Option> { + let text = pdbutil(name, &["--section-headers"])?; + let mut out = Vec::new(); + for line in text.lines() { + let line = line.trim(); + if let Some(hex) = line.strip_suffix(" virtual address") { + out.push(u64::from_str_radix(hex.trim(), 16).ok()?); + } + } + (!out.is_empty()).then_some(out) +} + +/// `0001:0010` as a relative address. +fn oracle_addr(sections: &[u64], spec: &str) -> Option { + let (section, offset) = spec.split_once(':')?; + let section: usize = section.trim().parse().ok()?; + let offset: u64 = offset.trim().parse().ok()?; + Some(Addr(sections.get(section.checked_sub(1)?)? + offset)) +} + +/// The value of `key = ` in one of the oracle's detail lines. +fn field<'a>(line: &'a str, key: &str) -> Option<&'a str> { + let at = line.find(&format!("{key} = "))? + key.len() + 3; + let rest = &line[at..]; + Some(rest.split(',').next().unwrap_or(rest).trim()) +} + +#[test] +fn procedures_match_the_oracle_name_address_and_size() { + let mut checked = 0; + for (name, data) in fixtures() { + let Some(text) = pdbutil(&name, &["--symbols"]) else { + continue; + }; + let Some(sections) = oracle_sections(&name) else { + continue; + }; + // Every procedure record the oracle saw, as name, entry and length. + let mut want: Vec<(String, Addr, u64)> = Vec::new(); + let mut lines = text.lines().peekable(); + while let Some(line) = lines.next() { + let is_proc = line.contains("S_GPROC32 [") || line.contains("S_LPROC32 ["); + if !is_proc { + continue; + } + let symbol = line.rsplit('`').nth(1).expect("a quoted name").to_string(); + let detail = lines.next().expect("a procedure has a detail line"); + let addr = oracle_addr(§ions, field(detail, "addr").expect("addr")); + let size: u64 = field(detail, "code size") + .expect("code size") + .parse() + .expect("a number"); + want.push((symbol, addr.expect("a section the headers list"), size)); + } + assert!(!want.is_empty(), "{name}: the oracle saw no procedures"); + + let db = pdb::read(&data, &[]).expect("a fixture database parses"); + // Record by record, not by count: a reader that found the right number + // of the wrong functions would pass a count. + for (symbol, addr, size) in &want { + let found = db + .debug + .functions + .get(addr) + .unwrap_or_else(|| panic!("{name}: nothing at {addr:?} for {symbol}")); + assert_eq!(&found.name, symbol, "{name}: name at {addr:?}"); + assert_eq!(found.size, Some(*size), "{name}: size of {symbol}"); + checked += 1; + } + // And nothing invented: every function we report is one the oracle saw + // as a procedure or as a public. + let publics = pdbutil(&name, &["--publics"]).unwrap_or_default(); + for (addr, f) in &db.debug.functions { + let known = want.iter().any(|(n, a, _)| n == &f.name && a == addr) + || publics.contains(&format!("`{}`", f.name)); + assert!(known, "{name}: {} at {addr:?} is ours alone", f.name); + } + } + assert!(checked > 0, "no corpus or no llvm-pdbutil: gate not run"); + println!("procedures checked against llvm-pdbutil: {checked}"); +} + +#[test] +fn public_function_symbols_match_the_oracle() { + let mut checked = 0; + for (name, data) in fixtures() { + let Some(text) = pdbutil(&name, &["--publics"]) else { + continue; + }; + let Some(sections) = oracle_sections(&name) else { + continue; + }; + let db = pdb::read(&data, &[]).expect("parses"); + let mut lines = text.lines().peekable(); + while let Some(line) = lines.next() { + if !line.contains("S_PUB32 [") { + continue; + } + let symbol = line.rsplit('`').nth(1).expect("a quoted name").to_string(); + let detail = lines.next().expect("a public has a detail line"); + let addr = oracle_addr(§ions, field(detail, "addr").expect("addr")) + .expect("a section the headers list"); + let code = field(detail, "flags").is_some_and(|f| f.contains("function")); + if code { + let found = db + .debug + .functions + .get(&addr) + .unwrap_or_else(|| panic!("{name}: no function at {addr:?} for {symbol}")); + assert_eq!(found.name, symbol, "{name}: public at {addr:?}"); + } else { + let found = db + .debug + .variables + .get(&addr) + .unwrap_or_else(|| panic!("{name}: no variable at {addr:?} for {symbol}")); + assert_eq!(found.name, symbol, "{name}: public data at {addr:?}"); + } + checked += 1; + } + } + assert!(checked > 0, "no corpus or no llvm-pdbutil: gate not run"); + println!("public symbols checked against llvm-pdbutil: {checked}"); +} + +#[test] +fn frame_relative_locals_match_the_oracle_offsets() { + let mut checked = 0; + for (name, data) in fixtures() { + let Some(text) = pdbutil(&name, &["--symbols"]) else { + continue; + }; + let Some(sections) = oracle_sections(&name) else { + continue; + }; + // (procedure entry, local name) -> frame offset, as the oracle read it. + let mut want: BTreeMap<(Addr, String), i64> = BTreeMap::new(); + let mut procedure = None; + let mut local = None; + let mut lines = text.lines().peekable(); + while let Some(line) = lines.next() { + if line.contains("S_GPROC32 [") || line.contains("S_LPROC32 [") { + let detail = lines.next().expect("a detail line"); + procedure = oracle_addr(§ions, field(detail, "addr").expect("addr")); + local = None; + } else if line.contains("S_LOCAL [") { + local = line.rsplit('`').nth(1).map(str::to_string); + } else if line.contains("S_DEFRANGE_FRAMEPOINTER_REL [") + || line.contains("S_DEFRANGE_REGISTER_REL [") + { + let detail = lines.next().expect("a detail line"); + // The register relative form only names a frame slot when its + // register is the one the frame record calls the local base. + if line.contains("S_DEFRANGE_REGISTER_REL [") + && !field(detail, "register").is_some_and(|r| r.ends_with("SP")) + { + continue; + } + let offset: i64 = field(detail, "offset") + .expect("offset") + .parse() + .expect("a number"); + if let (Some(p), Some(l)) = (procedure, local.clone()) { + want.insert((p, l), offset); + } + } else if line.contains("S_INLINESITE [") { + // Locals inside an inlined call belong to the frame, not to + // the procedure, and are compared by the inline site gate. + local = None; + while let Some(next) = lines.peek() { + if next.contains("S_INLINESITE_END [") { + break; + } + lines.next(); + } + } + } + // At -O2 a target can keep every local in a register, and then there + // is nothing here to compare; the register gate covers those. + let db = pdb::read(&data, &[]).expect("parses"); + for ((at, local), offset) in &want { + let f = db + .debug + .functions + .get(at) + .unwrap_or_else(|| panic!("{name}: no function at {at:?}")); + let found = f + .locals + .iter() + .find(|l| &l.name == local) + .unwrap_or_else(|| panic!("{name}: {} has no local {local}", f.name)); + assert_eq!( + found.frame_offset, + Some(*offset), + "{name}: {}::{local}", + f.name + ); + checked += 1; + } + } + assert!(checked > 0, "no corpus or no llvm-pdbutil: gate not run"); + println!("frame locals checked against llvm-pdbutil: {checked}"); +} + +#[test] +fn register_locations_name_the_register_the_oracle_named() { + // CodeView and DWARF number registers differently and for unrelated + // reasons, so the translation is worth nothing unless it is checked + // against something that prints the name. Only the registers these + // fixtures use are covered here; the rest of the table is unverified and + // the scorecard must not say otherwise. + let names: BTreeMap<&str, u16> = BTreeMap::from([ + ("RAX", 0), + ("EAX", 0), + ("RDX", 1), + ("EDX", 1), + ("RCX", 2), + ("ECX", 2), + ("RSP", 7), + ("ARM64_X0", 0), + ("ARM64_W0", 0), + ("ARM64_X1", 1), + ("ARM64_W1", 1), + ("ARM64_X8", 8), + ("ARM64_W8", 8), + ("ARM64_X9", 9), + ("ARM64_W9", 9), + ("ARM64_X10", 10), + ("ARM64_W10", 10), + ("ARM64_SP", 31), + ]); + let mut checked = 0; + for (name, data) in fixtures() { + let Some(text) = pdbutil(&name, &["--symbols"]) else { + continue; + }; + let Some(sections) = oracle_sections(&name) else { + continue; + }; + // Each range the oracle printed, with the register it named over it. + let mut want: Vec<(AddrRange, u16, &str)> = Vec::new(); + let mut lines = text.lines().peekable(); + while let Some(line) = lines.next() { + let Some(reg) = field(line, "register") else { + continue; + }; + let Some(number) = names.get(reg) else { + continue; + }; + // The plain register form prints its range on the same line; the + // register relative form prints it on the next one. + let range = match field(line, "range start") { + Some(spec) => { + let len: u64 = field(line, "length").expect("length").parse().expect("n"); + AddrRange::sized(oracle_addr(§ions, spec).expect("a section"), len) + } + None => lines.next().and_then(|next| { + let at = next.find("range = [")? + 9; + let (spec, rest) = next[at..].split_once(",+")?; + let len: u64 = rest.split(')').next()?.parse().ok()?; + AddrRange::sized(oracle_addr(§ions, spec)?, len) + }), + }; + want.push((range.expect("a range the oracle printed"), *number, reg)); + } + if want.is_empty() { + continue; + } + + let db = pdb::read(&data, &[]).expect("parses"); + // Every register location we report, keyed by the range it covers. + let mut ours: Vec<(AddrRange, u16)> = Vec::new(); + for f in db.debug.functions.values() { + let inner = f.inlines.iter().flat_map(|i| i.locals.iter()); + for l in f.locals.iter().chain(inner) { + for r in &l.locations { + match r.location { + Location::Register(n) | Location::RegisterOffset(n, _) => { + ours.push((r.range, n)); + } + _ => {} + } + } + } + } + for (range, number, reg) in &want { + // Our ranges are the oracle's with the record's gaps punched out, + // so they sit inside it and together reach both of its ends. + let mine: Vec = ours + .iter() + .filter(|(r, n)| n == number && range.contains_range(*r)) + .map(|(r, _)| *r) + .collect(); + assert!( + mine.iter().any(|r| r.start() == range.start()) + && mine.iter().any(|r| r.end() == range.end()), + "{name}: the oracle names {reg} over {range:?}, we report {mine:?} for {number}" + ); + checked += 1; + } + } + assert!(checked > 0, "no corpus or no llvm-pdbutil: gate not run"); + println!("register locations checked against llvm-pdbutil: {checked}"); +} + +#[test] +fn inlined_frames_match_the_oracle_inlinee_and_extent() { + let mut checked = 0; + let mut files = 0; + for (name, data) in fixtures() { + let Some(text) = pdbutil(&name, &["--symbols"]) else { + continue; + }; + let Some(sections) = oracle_sections(&name) else { + continue; + }; + // The oracle prints each site's accumulated code offset and the end it + // computes, both relative to the procedure the site sits in. + let mut want: Vec<(Addr, String, u64, u64)> = Vec::new(); + let mut procedure = Addr::ZERO; + let mut lines = text.lines().peekable(); + while let Some(line) = lines.next() { + if line.contains("S_GPROC32 [") || line.contains("S_LPROC32 [") { + let detail = lines.next().expect("a detail line"); + procedure = oracle_addr(§ions, field(detail, "addr").expect("addr")) + .expect("a section the headers list"); + } else if line.contains("S_INLINESITE [") || line.contains("S_INLINESITE2 [") { + let detail = lines.next().expect("a detail line"); + let inlinee = detail + .rsplit_once('(') + .and_then(|(_, r)| r.split(')').next()) + .expect("an inlinee name") + .to_string(); + // The annotation lines follow until the next record, and a + // site can describe more than one range. + let mut start = None; + while let Some(next) = lines.peek() { + if next.contains('|') { + break; + } + let hex = |s: &str| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok(); + if let Some(at) = next.find("code end 0x") { + let end = hex(next[at + 9..].split_whitespace().next().unwrap_or("")); + if let (Some(from), Some(end)) = (start.take(), end) { + want.push((procedure, inlinee.clone(), from, end)); + } + } else if let Some(at) = next.find("code 0x") { + start = hex(next[at + 5..].split_whitespace().next().unwrap_or("")); + } + lines.next(); + } + } + } + if want.is_empty() { + continue; + } + files += 1; + + let db = pdb::read(&data, &[]).expect("parses"); + for (at, inlinee, start, end) in &want { + let f = db + .debug + .functions + .get(at) + .unwrap_or_else(|| panic!("{name}: no function at {at:?}")); + let range = AddrRange::new(Addr(at.get() + start), Addr(at.get() + end)) + .expect("a range the oracle printed"); + let found = f.inlines.iter().find(|i| i.ranges.contains(&range)); + let found = found.unwrap_or_else(|| { + panic!("{name}: {} has no inlined frame over {range:?}", f.name) + }); + assert_eq!(&found.name, inlinee, "{name}: inlinee over {range:?}"); + checked += 1; + } + // Nothing invented the other way either. + let mine: usize = db.debug.functions.values().map(|f| f.inlines.len()).sum(); + assert_eq!(mine, want.len(), "{name}: inlined frame count"); + } + assert!(files > 0, "no corpus or no llvm-pdbutil: gate not run"); + println!("inlined frames checked against llvm-pdbutil: {checked}"); +} + +#[test] +fn section_contributions_match_the_oracle() { + let mut checked = 0; + for (name, data) in fixtures() { + let Some(text) = pdbutil(&name, &["--section-contribs"]) else { + continue; + }; + let Some(sections) = oracle_sections(&name) else { + continue; + }; + let mut want: Vec<(Addr, u64, u16)> = Vec::new(); + for line in text.lines() { + let Some((_, rest)) = line.split_once("| mod = ") else { + continue; + }; + let mut parts = rest.split(',').map(str::trim); + let module: u16 = parts.next().expect("a module").parse().expect("a number"); + let addr = + oracle_addr(§ions, parts.next().expect("an address")).expect("a section"); + let size: u64 = parts + .next() + .and_then(|s| s.strip_prefix("size = ")) + .expect("a size") + .parse() + .expect("a number"); + want.push((addr, size, module)); + } + assert!(!want.is_empty(), "{name}: the oracle saw no contributions"); + + let db = pdb::read(&data, &[]).expect("parses"); + assert_eq!(db.contributions.len(), want.len(), "{name}: count"); + for ((addr, size, module), got) in want.iter().zip(&db.contributions) { + assert_eq!(got.range.start(), *addr, "{name}: contribution start"); + assert_eq!(got.range.len(), *size, "{name}: contribution size"); + assert_eq!(got.module, *module, "{name}: contribution module"); + assert!( + (got.module as usize) < db.modules.len(), + "{name}: a contribution names a module that is not listed" + ); + checked += 1; + } + } + assert!(checked > 0, "no corpus or no llvm-pdbutil: gate not run"); + println!("section contributions checked against llvm-pdbutil: {checked}"); +} + +#[test] +fn the_identity_is_the_one_the_image_asks_for() { + let Some(dir) = corpus() else { return }; + let mut checked = 0; + for arch in ["x64", "a64"] { + for opt in ["O0", "O2"] { + let Ok(image) = std::fs::read(dir.join(format!("pdb.{arch}.{opt}.exe"))) else { + continue; + }; + let Ok(data) = std::fs::read(dir.join(format!("pdb.{arch}.{opt}.pdb"))) else { + continue; + }; + // The debug directory's CodeView record: the signature, then the + // GUID and the age the linker stamped into both files. + let at = image + .windows(4) + .position(|w| w == b"RSDS") + .expect("the image carries a CodeView record"); + let guid: [u8; 16] = image[at + 4..at + 20].try_into().expect("sixteen bytes"); + let age = u32::from_le_bytes(image[at + 20..at + 24].try_into().expect("four")); + let id = pdb::identity(&data).expect("an identity"); + assert!( + id.matches(&guid, age), + "pdb.{arch}.{opt}: the database is not the one the image names" + ); + assert_eq!(id.age, age); + checked += 1; + } + } + assert!(checked > 0, "no corpus: gate not run"); +} + +#[test] +fn hints_are_proven_and_name_what_the_records_named() { + let mut checked = 0; + for (name, data) in fixtures() { + let db = pdb::read(&data, &[]).expect("parses"); + let hints = db.hints(); + assert!(!hints.is_empty(), "{name}: a database proves some entries"); + for h in &hints { + assert_eq!(h.provenance.best, Evidence::Pdb); + assert_eq!(h.provenance.strength(), Strength::Proven); + let f = db.debug.functions.get(&h.addr).expect("a function"); + assert_eq!(h.name.as_deref(), Some(f.name.as_str())); + assert!(!f.name.is_empty(), "{name}: a hint with no name"); + checked += 1; + } + } + assert!(checked > 0, "no corpus: gate not run"); + println!("function hints from real databases: {checked}"); +} + +#[test] +fn a_real_database_fills_the_same_shape_dwarf_does() { + let mut seen = 0; + for (name, data) in fixtures() { + let db = pdb::read(&data, &[]).expect("parses"); + assert!(!db.debug.is_empty(), "{name}: describes nothing"); + assert!(db.warnings.is_empty(), "{name}: {:?}", db.warnings); + assert_eq!(db.debug.units, db.modules.len()); + // The producer string is the one fact that says which compiler wrote + // it, and both modules of a fixture have one. + assert!( + db.modules.iter().any(|m| m.producer.is_some()), + "{name}: no module named its producer" + ); + // Types, line rows and locals: a reader that found the functions and + // nothing else would still pass the boundary gates. + assert!(db.debug.types.len() > 8, "{name}: no type graph"); + assert!(!db.debug.lines.is_empty(), "{name}: no line rows"); + let locals: usize = db.debug.functions.values().map(|f| f.locals.len()).sum(); + assert!(locals >= 7, "{name}: {locals} locals"); + // Every function the line table covers resolves back to itself. + for (addr, f) in &db.debug.functions { + assert_eq!(db.debug.function_at(*addr).map(|f| &f.name), Some(&f.name)); + for frame in &f.inlines { + for r in &frame.ranges { + assert!(f.covers(r.start()), "{name}: {} inline outside", f.name); + } + } + } + seen += 1; + } + assert!(seen > 0, "no corpus: gate not run"); +} + +#[test] +fn hostile_databases_are_survived_in_bounded_time() { + // The page directory, the stream directory and the symbol records are the + // three places a count comes out of the file and decides an allocation. + let Some((name, base)) = fixtures().into_iter().next() else { + return; + }; + let started = Instant::now(); + let mut cases = 0; + + // Truncation at every block boundary, and at a handful of byte offsets + // inside the first block where the superblock's own fields live. + for n in (0..base.len()).step_by(4096).chain(0..64) { + let _ = pdb::read(&base[..n], &[]); + let _ = pdb::identity(&base[..n]); + cases += 1; + } + + // Saturate every little endian word in the superblock and in the first two + // blocks, which is where the page count, the directory size and the + // directory's own block list are. + for at in (0..8192.min(base.len() - 4)).step_by(2) { + for word in [u32::MAX, u32::MAX / 2, 0, 1] { + let mut case = base.clone(); + case[at..at + 4].copy_from_slice(&word.to_le_bytes()); + let _ = pdb::read(&case, &[]); + cases += 1; + } + } + + // And the record streams: a length field that claims more than the stream + // holds, or claims nothing at all, in every stream the file has. + let msf = pdb::Msf::open(&base).expect("the fixture opens"); + for n in 0..msf.count() { + let Some(stream) = msf.stream(n) else { + continue; + }; + if stream.is_empty() { + continue; + } + // Where the stream's bytes sit in the file, so its record lengths can + // be rewritten in place. + let window = stream.len().min(256); + let Some(at) = base.windows(window).position(|w| w == &stream[..window]) else { + continue; + }; + for word in [0u16, 1, 2, u16::MAX, 0x8000] { + let mut case = base.clone(); + for step in (0..window - 2).step_by(4) { + case[at + step..at + step + 2].copy_from_slice(&word.to_le_bytes()); + } + let _ = pdb::read(&case, &[]); + cases += 1; + } + } + + let took = started.elapsed(); + assert!( + took < Duration::from_secs(20), + "{name}: {cases} hostile cases took {took:?}" + ); + println!("{cases} hostile cases in {took:?}"); +} diff --git a/crates/e5r-format/tests/pe.rs b/crates/e5r-format/tests/pe.rs new file mode 100644 index 0000000..886b717 --- /dev/null +++ b/crates/e5r-format/tests/pe.rs @@ -0,0 +1,389 @@ +//! PE and COFF loading. +//! +//! There is no Windows linker on this machine, so the image tests build a PE +//! byte by byte. That is not a weakness: a synthesized image pins exactly what +//! the parser is supposed to read, and the fields that matter (the directories) +//! are the ones a real file would differ from only in scale. COFF objects come +//! from clang, which cross-compiles them without a sysroot. + +use std::path::{Path, PathBuf}; + +use e5r_core::{Addr, Arch, Bits, Evidence}; +use e5r_format::{Format, LoadOptions, load, pe}; + +/// A little-endian writer, so the builder reads like the layout it produces. +#[derive(Default)] +struct Buf(Vec); + +impl Buf { + fn u8(&mut self, v: u8) -> &mut Self { + self.0.push(v); + self + } + fn u16(&mut self, v: u16) -> &mut Self { + self.0.extend_from_slice(&v.to_le_bytes()); + self + } + fn u32(&mut self, v: u32) -> &mut Self { + self.0.extend_from_slice(&v.to_le_bytes()); + self + } + fn u64(&mut self, v: u64) -> &mut Self { + self.0.extend_from_slice(&v.to_le_bytes()); + self + } + fn bytes(&mut self, v: &[u8]) -> &mut Self { + self.0.extend_from_slice(v); + self + } + fn pad_to(&mut self, n: usize) -> &mut Self { + while self.0.len() < n { + self.0.push(0); + } + self + } + fn at(&mut self, off: usize, v: &[u8]) { + self.0[off..off + v.len()].copy_from_slice(v); + } +} + +const IMAGE_BASE: u64 = 0x1_4000_0000; +const TEXT_RVA: u32 = 0x1000; +const RDATA_RVA: u32 = 0x2000; +const HEADERS: usize = 0x400; +const TEXT_OFF: usize = 0x400; +const RDATA_OFF: usize = 0x600; + +/// A PE32+ executable with two sections, one import, one export and one +/// runtime function. +fn synth_pe() -> Vec { + let mut b = Buf::default(); + // DOS header: the magic, then e_lfanew at 0x3c. + b.bytes(b"MZ").pad_to(0x3c).u32(0x80).pad_to(0x80); + b.bytes(b"PE\0\0"); + + // COFF header. + b.u16(0x8664) // AMD64 + .u16(2) // sections + .u32(0) // timestamp + .u32(0) // symbol table + .u32(0) // symbols + .u16(240) // optional header size + .u16(0x0022); // executable, large address aware + + let opt_start = b.0.len(); + b.u16(0x20b) // PE32+ + .u8(14) + .u8(0) // linker version + .u32(0x200) // code size + .u32(0x200) // initialized data + .u32(0) // uninitialized + .u32(TEXT_RVA) // entry point + .u32(TEXT_RVA) // base of code + .u64(IMAGE_BASE) + .u32(0x1000) // section alignment + .u32(0x200) // file alignment + .u16(6) + .u16(0) + .u16(0) + .u16(0) + .u16(6) + .u16(0) // versions + .u32(0) // win32 version + .u32(0x3000) // size of image + .u32(HEADERS as u32) + .u32(0) // checksum + .u16(3) // console subsystem + .u16(0x0160) // dynamic base, NX, guard CF + .u64(0x100000) + .u64(0x1000) + .u64(0x100000) + .u64(0x1000) + .u32(0) // loader flags + .u32(16); // directory count + let dirs_at = b.0.len(); + for _ in 0..16 { + b.u32(0).u32(0); + } + assert_eq!(b.0.len() - opt_start, 240, "optional header size"); + + // Section headers. + let sec = |b: &mut Buf, name: &[u8; 8], rva: u32, size: u32, off: u32, ch: u32| { + b.bytes(name) + .u32(size) + .u32(rva) + .u32(size.next_multiple_of(0x200)) + .u32(off) + .u32(0) + .u32(0) + .u16(0) + .u16(0) + .u32(ch); + }; + sec( + &mut b, + b".text\0\0\0", + TEXT_RVA, + 0x20, + TEXT_OFF as u32, + 0x6000_0020, + ); + sec( + &mut b, + b".rdata\0\0", + RDATA_RVA, + 0x140, + RDATA_OFF as u32, + 0x4000_0040, + ); + + b.pad_to(TEXT_OFF); + // Enough bytes that every referenced address is executable. + b.bytes(&[0xc3; 0x200]); + + // .rdata holds the import, export and exception tables. + b.pad_to(RDATA_OFF); + let rdata = |off: usize| (RDATA_RVA as usize + (off - RDATA_OFF)) as u32; + + // Import descriptor, then a terminator. + let import_dir = b.0.len(); + let thunks_rva_slot = import_dir; // filled in below + b.u32(0).u32(0).u32(0).u32(0).u32(0); // descriptor, patched later + b.u32(0).u32(0).u32(0).u32(0).u32(0); // terminator + + // Import name table: one thunk pointing at a hint/name entry, then zero. + let int_at = b.0.len(); + b.u64(0).u64(0); + // Import address table, same shape. + let iat_at = b.0.len(); + b.u64(0).u64(0); + // The hint/name entry and the DLL name. + let hint_at = b.0.len(); + b.u16(0).bytes(b"CreateFileW\0"); + let dll_at = b.0.len(); + b.bytes(b"KERNEL32.dll\0"); + + // Export directory. + while b.0.len() % 4 != 0 { + b.u8(0); + } + let export_dir = b.0.len(); + let export_names = export_dir + 40; + let export_ordinals = export_names + 4; + let export_functions = export_ordinals + 2 + 2; + let export_name_str = export_functions + 4; + b.u32(0).u32(0).u16(0).u16(0); // flags, stamp, versions + b.u32(0); // module name RVA, patched + b.u32(1); // ordinal base + b.u32(1) // functions + .u32(1) // names + .u32(0) // address table RVA, patched + .u32(0) // name pointer RVA, patched + .u32(0); // ordinal table RVA, patched + assert_eq!(b.0.len(), export_names); + b.u32(0); // name pointer, patched + b.u16(0); // ordinal + b.u16(0); // padding + assert_eq!(b.0.len(), export_functions); + b.u32(TEXT_RVA + 8); // exported function RVA + assert_eq!(b.0.len(), export_name_str); + b.bytes(b"my.dll\0"); + let exported_name_at = b.0.len(); + b.bytes(b"DoThing\0"); + + // Exception directory: two runtime functions. + while b.0.len() % 4 != 0 { + b.u8(0); + } + let exception_dir = b.0.len(); + b.u32(TEXT_RVA).u32(TEXT_RVA + 8).u32(0); + b.u32(TEXT_RVA + 8).u32(TEXT_RVA + 0x20).u32(0); + let exception_size = (b.0.len() - exception_dir) as u32; + + b.pad_to(RDATA_OFF + 0x200); + + // Patch the directory entries and the RVAs that were not known in order. + let mut d = Buf::default(); + d.u32(rdata(export_dir)).u32(40 + 4 + 4 + 4 + 7 + 8); + d.u32(rdata(import_dir)).u32(40); + let import_bytes = d.0.clone(); + b.at(dirs_at, &import_bytes[..8]); // export + b.at(dirs_at + 8, &import_bytes[8..]); // import + let mut ex = Buf::default(); + ex.u32(rdata(exception_dir)).u32(exception_size); + b.at(dirs_at + 24, &ex.0); // exception + + let mut desc = Buf::default(); + desc.u32(rdata(int_at)) // OriginalFirstThunk + .u32(0) + .u32(0) + .u32(rdata(dll_at)) // Name + .u32(rdata(iat_at)); // FirstThunk + b.at(thunks_rva_slot, &desc.0); + + let mut thunk = Buf::default(); + thunk.u64(rdata(hint_at) as u64); + b.at(int_at, &thunk.0); + b.at(iat_at, &thunk.0); + + let mut ename = Buf::default(); + ename.u32(rdata(export_name_str)); + b.at(export_dir + 12, &ename.0); + let mut tables = Buf::default(); + tables + .u32(rdata(export_functions)) + .u32(rdata(export_names)) + .u32(rdata(export_ordinals)); + b.at(export_dir + 28, &tables.0); + let mut np = Buf::default(); + np.u32(rdata(exported_name_at)); + b.at(export_names, &np.0); + + b.0 +} + +#[test] +fn a_pe_image_loads() { + let data = synth_pe(); + let obj = load(&data, &LoadOptions::default()).expect("load"); + assert_eq!(obj.format, Format::Pe); + assert_eq!(obj.arch, Arch::X86_64); + assert_eq!(obj.bits, Bits::Bits64); + assert_eq!(obj.image_base, Addr(IMAGE_BASE)); + assert_eq!(obj.entry, Some(Addr(IMAGE_BASE + TEXT_RVA as u64))); + assert!(obj.section(".text").is_some()); + assert!(obj.section(".rdata").is_some()); + assert!(obj.memory.is_executable(obj.entry.unwrap())); + assert_eq!( + obj.metadata.get("pe.subsystem").map(String::as_str), + Some("windows console") + ); + assert_eq!(obj.metadata.get("pe.nx").map(String::as_str), Some("true")); + assert_eq!( + obj.metadata.get("pe.aslr").map(String::as_str), + Some("true") + ); +} + +#[test] +fn imports_carry_their_library_and_their_slot() { + let obj = load(&synth_pe(), &LoadOptions::default()).unwrap(); + let imp = obj + .imports + .iter() + .find(|i| i.name == "CreateFileW") + .expect("import not found"); + assert_eq!(imp.library.as_deref(), Some("KERNEL32.dll")); + // The slot is what an indirect call goes through, so it has to be there. + assert!(imp.thunk.is_some(), "no IAT slot"); +} + +#[test] +fn exports_are_named_and_become_function_hints() { + let obj = load(&synth_pe(), &LoadOptions::default()).unwrap(); + let exp = obj + .exports + .iter() + .find(|e| e.name == "DoThing") + .expect("export not found"); + assert_eq!(exp.addr, Addr(IMAGE_BASE + TEXT_RVA as u64 + 8)); + assert_eq!(exp.ordinal, Some(1)); + assert!( + obj.function_hints + .iter() + .any(|h| h.addr == exp.addr && h.provenance.best == Evidence::Export), + "the export did not become a hint" + ); +} + +#[test] +fn the_exception_directory_gives_exact_boundaries() { + let obj = load(&synth_pe(), &LoadOptions::default()).unwrap(); + let unwind: Vec<_> = obj + .function_hints + .iter() + .filter(|h| { + h.provenance.best == Evidence::PeUnwind + || h.provenance.corroborating.contains(&Evidence::PeUnwind) + }) + .collect(); + assert_eq!(unwind.len(), 2, "expected two runtime functions"); + assert!( + unwind.iter().any(|h| h.size == Some(8)), + "sizes were not recovered" + ); +} + +#[test] +fn a_truncated_pe_never_panics() { + let data = synth_pe(); + let mut n = 1; + while n < data.len() { + let _ = load(&data[..n], &LoadOptions::default()); + n = (n * 2).max(n + 37); + } +} + +#[test] +fn corrupted_headers_never_panic() { + let mut data = synth_pe(); + for i in (0..0x200).step_by(3) { + let old = data[i]; + for v in [0xff, 0x00, 0x80] { + data[i] = v; + let _ = load(&data, &LoadOptions::default()); + } + data[i] = old; + } +} + +#[test] +fn e_lfanew_pointing_nowhere_is_an_error_not_a_panic() { + let mut data = synth_pe(); + data[0x3c..0x40].copy_from_slice(&0xffff_fff0u32.to_le_bytes()); + assert!(load(&data, &LoadOptions::default()).is_err()); +} + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +#[test] +fn coff_objects_load_and_get_laid_out() { + let Some(dir) = corpus() else { return }; + let p = dir.join("wide.coff.o"); + let Ok(data) = std::fs::read(&p) else { return }; + let obj = load(&data, &LoadOptions::default()).expect("coff"); + assert_eq!(obj.arch, Arch::X86_64); + assert_eq!( + obj.metadata.get("pe.kind").map(String::as_str), + Some("object") + ); + // Sections in an object all claim RVA zero, so the loader has to place + // them; otherwise every symbol lands on top of every other. + let text = obj.section(".text").expect("no .text"); + assert!(text.range.start() != Addr::ZERO); + assert!(obj.memory.is_executable(text.range.start())); + // Function symbols must follow their section. + let named: Vec<_> = obj + .symbols + .iter() + .filter(|s| s.is_defined_function()) + .collect(); + assert!(named.len() > 5, "only {} function symbols", named.len()); + for s in named { + assert!( + obj.memory.is_executable(s.addr), + "{} at {} is not executable", + s.name, + s.addr + ); + } +} + +#[test] +fn is_pe_does_not_claim_an_elf() { + let elf = b"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\xb7\x00"; + assert!(!pe::is_pe(elf)); +} diff --git a/crates/e5r-format/tests/swift.rs b/crates/e5r-format/tests/swift.rs new file mode 100644 index 0000000..516f431 --- /dev/null +++ b/crates/e5r-format/tests/swift.rs @@ -0,0 +1,555 @@ +//! Swift reflection metadata, against hand-built images. +//! +//! There is no Swift toolchain on this machine, so there is no compiler output +//! to compare against and no `swift demangle` to check a name with. What these +//! tests measure is a fixture written from the published ABI: the section +//! names Swift's IRGen emits, the field descriptor record layout, the context +//! descriptor layout, and the conformance record, with every relative pointer +//! computed by hand from the address of the field that holds it. That is a +//! weaker claim than the Go and DWARF gates, which are measured against other +//! implementations, and the scorecard should say so. +//! +//! What the fixture does check that a compiler could not check better: a +//! displacement measured from the wrong place lands somewhere deliberately +//! nearby, so each expected value here is one an off-by-a-field reader gets +//! wrong. The section walk landing exactly on the section's end is the second +//! check, and it is the one that catches a record size read wrong. + +use std::time::{Duration, Instant}; + +use e5r_core::Addr; +use e5r_format::swift::{ContextKind, FieldDescriptorKind, TypeReference}; +use e5r_format::{LoadOptions, load, metadata, swift}; + +const MACHO_BASE: u64 = 0x1_0000_0000; + +// __TEXT. Every Swift reflection section lives here on Mach-O. +const TEXT_CODE: u64 = 0x400; +const CONST: u64 = 0x500; +const MODULE: u64 = 0x500; +const POINT: u64 = 0x520; +const COLOR: u64 = 0x540; +const CONFORMANCE: u64 = 0x560; +const PROTOCOL: u64 = 0x580; +const WITNESS: u64 = 0x5a0; +const TYPEREF: u64 = 0x700; +const SYMBOLIC: u64 = 0x730; +const REFLSTR: u64 = 0x780; +const FIELDMD: u64 = 0x800; +const FIELDMD_SIZE: u64 = 0x50; +const TYPES: u64 = 0x900; +const PROTO: u64 = 0x910; +const PROTOS: u64 = 0x920; +// __DATA: one slot, so the indirect form of a relative pointer has somewhere +// to indirect through the way a linker's GOT does. +const GOT: u64 = 0x1000; + +fn at(offset: u64) -> Addr { + Addr(MACHO_BASE + offset) +} + +/// A Mach-O image carrying Swift reflection metadata. +/// +/// Written out resolved, for the same reason the Objective-C fixture is: a +/// relocatable object has zero where each of these pointers belongs. +fn synth_swift() -> Vec { + let mut f = vec![0u8; 0x2000]; + let put = |f: &mut Vec, at: u64, b: &[u8]| { + f[at as usize..at as usize + b.len()].copy_from_slice(b); + }; + let u16at = |f: &mut Vec, at: u64, v: u16| put(f, at, &v.to_le_bytes()); + let u32at = |f: &mut Vec, at: u64, v: u32| put(f, at, &v.to_le_bytes()); + let u64at = |f: &mut Vec, at: u64, v: u64| put(f, at, &v.to_le_bytes()); + // A relative pointer: the displacement from the field's own address, which + // is the whole thing this format gets wrong when it is got wrong. + let rel = |f: &mut Vec, field: u64, target: u64| { + u32at(f, field, (target as i64 - field as i64) as i32 as u32); + }; + let name16 = |s: &str| { + let mut b = [0u8; 16]; + b[..s.len()].copy_from_slice(s.as_bytes()); + b + }; + + let text_sections: [(&str, u64, u64, u32); 8] = [ + ("__text", TEXT_CODE, 0x100, 0x8000_0400), + ("__const", CONST, 0x200, 0), + ("__swift5_typeref", TYPEREF, 0x80, 2), + ("__swift5_reflstr", REFLSTR, 0x80, 2), + ("__swift5_fieldmd", FIELDMD, FIELDMD_SIZE, 0), + ("__swift5_types", TYPES, 8, 0), + ("__swift5_proto", PROTO, 4, 0), + ("__swift5_protos", PROTOS, 4, 0), + ]; + let data_sections: [(&str, u64, u64, u32); 1] = [("__got", GOT, 0x40, 6)]; + + // mach_header_64: 64-bit little-endian arm64 executable. + u32at(&mut f, 0, 0xfeed_facf); + u32at(&mut f, 4, 0x0100_000c); + u32at(&mut f, 12, 2); + u32at(&mut f, 16, 2); + let text_cmd = 72 + 80 * text_sections.len() as u32; + let data_cmd = 72 + 80 * data_sections.len() as u32; + u32at(&mut f, 20, text_cmd + data_cmd); + + let seg = |f: &mut Vec, + cmd: u64, + name: &str, + vmaddr: u64, + fileoff: u64, + prot: u32, + sects: &[(&str, u64, u64, u32)]| { + u32at(f, cmd, 0x19); + u32at(f, cmd + 4, 72 + 80 * sects.len() as u32); + put(f, cmd + 8, &name16(name)); + u64at(f, cmd + 24, vmaddr); + u64at(f, cmd + 32, 0x1000); + u64at(f, cmd + 40, fileoff); + u64at(f, cmd + 48, 0x1000); + u32at(f, cmd + 56, 7); + u32at(f, cmd + 60, prot); + u32at(f, cmd + 64, sects.len() as u32); + for (i, (sn, offset, size, flags)) in sects.iter().enumerate() { + let s = cmd + 72 + 80 * i as u64; + put(f, s, &name16(sn)); + put(f, s + 16, &name16(name)); + u64at(f, s + 32, MACHO_BASE + offset); + u64at(f, s + 40, *size); + // __TEXT starts at file offset zero and __DATA at 0x1000, which is + // where each section's own address puts it. + u32at(f, s + 48, *offset as u32); + u32at(f, s + 64, *flags); + } + }; + seg(&mut f, 32, "__TEXT", MACHO_BASE, 0, 5, &text_sections); + seg( + &mut f, + 32 + text_cmd as u64, + "__DATA", + MACHO_BASE + GOT, + GOT, + 3, + &data_sections, + ); + + // Reflection strings: field and type names. + put( + &mut f, + REFLSTR, + b"x\0y\0red\0green\0Point\0Color\0Demo\0Drawable\0", + ); + let x = REFLSTR; + let y = REFLSTR + 2; + let red = REFLSTR + 4; + let green = REFLSTR + 8; + let point = REFLSTR + 14; + let color = REFLSTR + 20; + let module = REFLSTR + 26; + let drawable = REFLSTR + 31; + + // Mangled type names. The third is symbolic: a control byte, then a + // four-byte displacement whose bytes include three zeros, which is what + // breaks a reader that stops the name at the first zero byte. + put(&mut f, TYPEREF, b"$s4Demo5PointV\0"); + put(&mut f, TYPEREF + 16, b"Si\0"); + put(&mut f, TYPEREF + 20, b"$s4Demo5ColorO\0"); + f[SYMBOLIC as usize] = 0x01; + u32at(&mut f, SYMBOLIC + 1, 0x20); + put(&mut f, SYMBOLIC + 5, b"Sg\0"); + let point_mangled = TYPEREF; + let int_mangled = TYPEREF + 16; + let color_mangled = TYPEREF + 20; + + // The module descriptor every other context hangs off. + u32at(&mut f, MODULE, 0); + u32at(&mut f, MODULE + 4, 0); + rel(&mut f, MODULE + 8, module); + + // struct Point, with a field descriptor and a metadata accessor. + u32at(&mut f, POINT, 17 | 0x40); + rel(&mut f, POINT + 4, MODULE); + rel(&mut f, POINT + 8, point); + rel(&mut f, POINT + 12, TEXT_CODE); + rel(&mut f, POINT + 16, FIELDMD); + u32at(&mut f, POINT + 20, 2); + u32at(&mut f, POINT + 24, 2); + + // enum Color. + u32at(&mut f, COLOR, 18 | 0x40); + rel(&mut f, COLOR + 4, MODULE); + rel(&mut f, COLOR + 8, color); + rel(&mut f, COLOR + 12, TEXT_CODE + 0x20); + rel(&mut f, COLOR + 16, FIELDMD + 0x28); + + // Point: Drawable. The protocol is reached by the indirect form, which is + // what an import looks like once the linker has made a slot for it. + u32at( + &mut f, + CONFORMANCE, + ((GOT as i64 - CONFORMANCE as i64) as i32 | 1) as u32, + ); + rel(&mut f, CONFORMANCE + 4, POINT); + rel(&mut f, CONFORMANCE + 8, WITNESS); + u32at(&mut f, CONFORMANCE + 12, 0); + u64at(&mut f, GOT, MACHO_BASE + PROTOCOL); + + // protocol Drawable, with two requirements. + u32at(&mut f, PROTOCOL, 3 | 0x40); + rel(&mut f, PROTOCOL + 4, MODULE); + rel(&mut f, PROTOCOL + 8, drawable); + u32at(&mut f, PROTOCOL + 12, 0); + u32at(&mut f, PROTOCOL + 16, 2); + + // The field descriptors, packed one after another with no count in front. + rel(&mut f, FIELDMD, point_mangled); + u16at(&mut f, FIELDMD + 8, 0); // struct + u16at(&mut f, FIELDMD + 10, 12); + u32at(&mut f, FIELDMD + 12, 2); + u32at(&mut f, FIELDMD + 16, 2); // var + rel(&mut f, FIELDMD + 20, int_mangled); + rel(&mut f, FIELDMD + 24, x); + u32at(&mut f, FIELDMD + 28, 0); // let + rel(&mut f, FIELDMD + 32, int_mangled); + rel(&mut f, FIELDMD + 36, y); + + let second = FIELDMD + 40; + rel(&mut f, second, color_mangled); + u16at(&mut f, second + 8, 2); // enum + u16at(&mut f, second + 10, 12); + u32at(&mut f, second + 12, 2); + u32at(&mut f, second + 16, 0); + // A case with no payload has no type name, which is a null relative + // pointer and not a pointer to an empty string. + u32at(&mut f, second + 20, 0); + rel(&mut f, second + 24, red); + u32at(&mut f, second + 28, 1); // indirect case + rel(&mut f, second + 32, SYMBOLIC); + rel(&mut f, second + 36, green); + + // The three lists of relative pointers. The second type record is the + // indirect form: the low bits of a type record carry the reference kind, + // and one means the displacement reaches a slot holding the descriptor. + rel(&mut f, TYPES, POINT); + u64at(&mut f, GOT + 8, MACHO_BASE + COLOR); + u32at( + &mut f, + TYPES + 4, + ((GOT as i64 + 8 - (TYPES as i64 + 4)) as i32 | 1) as u32, + ); + rel(&mut f, PROTO, CONFORMANCE); + rel(&mut f, PROTOS, PROTOCOL); + f +} + +fn read(bytes: &[u8]) -> swift::SwiftMetadata { + let obj = load(bytes, &LoadOptions::default()).expect("the synthesized image does not load"); + swift::read(&obj) +} + +#[test] +fn nominal_types_are_read_with_their_module() { + let found = read(&synth_swift()); + assert_eq!(found.types.len(), 2, "{:#?}", found.types); + + let point = &found.types[0]; + assert_eq!(point.name, "Point"); + assert_eq!(point.qualified_name, "Demo.Point"); + assert_eq!(point.kind, ContextKind::Struct); + assert_eq!(point.addr, at(POINT)); + // The accessor is a function, and the only address here that is one. + assert_eq!(point.accessor, Some(at(TEXT_CODE))); + assert_eq!(point.field_descriptor, Some(at(FIELDMD))); + + // Reached through a slot, which is what a record with its kind bit set + // means and what a reader that ignores the bit gets wrong. + let color = &found.types[1]; + assert_eq!(color.addr, at(COLOR)); + assert_eq!(color.qualified_name, "Demo.Color"); + assert_eq!(color.kind, ContextKind::Enum); + assert_eq!(color.accessor, Some(at(TEXT_CODE + 0x20))); + + assert!( + found.warnings.is_empty(), + "a fixture built to the ABI should produce no complaints: {:#?}", + found.warnings + ); +} + +#[test] +fn field_descriptors_name_every_field_with_its_type() { + let found = read(&synth_swift()); + assert_eq!(found.field_descriptors.len(), 2); + + let point = &found.field_descriptors[0]; + assert_eq!(point.addr, at(FIELDMD)); + assert_eq!(point.kind, FieldDescriptorKind::Struct); + assert_eq!( + point.mangled_type_name.as_ref().map(|m| m.text.as_str()), + Some("$s4Demo5PointV") + ); + assert!(point.superclass.is_none()); + let names: Vec<&str> = point.fields.iter().map(|f| f.name.as_str()).collect(); + assert_eq!(names, ["x", "y"]); + assert!(point.fields[0].mutable, "x is a var"); + assert!(!point.fields[1].mutable, "y is a let"); + for field in &point.fields { + assert_eq!( + field.mangled_type.as_ref().map(|m| m.text.as_str()), + Some("Si") + ); + } + + let color = &found.field_descriptors[1]; + assert_eq!(color.kind, FieldDescriptorKind::Enum); + let names: Vec<&str> = color.fields.iter().map(|f| f.name.as_str()).collect(); + assert_eq!(names, ["red", "green"]); + // A case with no payload has no type, and inventing one would be worse + // than saying so. + assert!(color.fields[0].mangled_type.is_none()); + assert!(color.fields[1].indirect_case); +} + +#[test] +fn a_symbolic_reference_is_followed_and_does_not_end_the_name() { + // The displacement in this reference is 0x20, whose upper three bytes are + // zero. A reader that treats a mangled name as a C string stops there and + // loses both the reference and the rest of the name. + let found = read(&synth_swift()); + let green = &found.field_descriptors[1].fields[1]; + let mangled = green + .mangled_type + .as_ref() + .expect("the indirect case has a payload type"); + assert!(mangled.symbolic, "the reference was not noticed"); + assert_eq!(mangled.references, vec![at(SYMBOLIC + 1 + 0x20)]); + assert_eq!( + mangled.text, "Sg", + "the characters after the reference were lost" + ); +} + +#[test] +fn a_conformance_names_both_sides() { + let found = read(&synth_swift()); + assert_eq!(found.conformances.len(), 1); + let c = &found.conformances[0]; + assert_eq!(c.addr, at(CONFORMANCE)); + assert_eq!(c.type_reference, TypeReference::DirectDescriptor); + assert_eq!(c.type_descriptor, Some(at(POINT))); + assert_eq!(c.type_name.as_deref(), Some("Point")); + // Reached through the slot, which is the whole point of the low bit. + assert_eq!(c.protocol_descriptor, Some(at(PROTOCOL))); + assert_eq!(c.protocol_name.as_deref(), Some("Drawable")); + assert_eq!(c.witness_table, Some(at(WITNESS))); + assert!(!c.retroactive); + assert_eq!(c.conditional_requirements, 0); + + assert_eq!(found.protocols.len(), 1); + assert_eq!(found.protocols[0].qualified_name, "Demo.Drawable"); + assert_eq!(found.protocols[0].requirements, 2); +} + +#[test] +fn the_accessors_become_hints_and_nothing_else_does() { + let bytes = synth_swift(); + let obj = load(&bytes, &LoadOptions::default()).unwrap(); + let found = metadata::read(&obj); + let mut names: Vec = found + .hints + .iter() + .filter(|h| h.provenance.best == e5r_core::Evidence::SwiftMetadata) + .map(|h| format!("{:#x} {}", h.addr.get(), h.name.clone().unwrap_or_default())) + .collect(); + names.sort(); + let mut want = vec![ + format!( + "{:#x} type metadata accessor for Demo.Point", + at(TEXT_CODE).get() + ), + format!( + "{:#x} type metadata accessor for Demo.Color", + at(TEXT_CODE + 0x20).get() + ), + ]; + want.sort(); + assert_eq!( + names, want, + "the witness table or a descriptor was reported as a function" + ); + assert_eq!( + found.notes.get("swift.types").map(String::as_str), + Some("2") + ); + assert_eq!( + found.notes.get("swift.fields").map(String::as_str), + Some("4") + ); + assert_eq!( + found.notes.get("swift.conformances").map(String::as_str), + Some("1") + ); +} + +#[test] +fn an_accessor_outside_executable_memory_is_a_warning_and_not_a_hint() { + let mut bytes = synth_swift(); + // Point the accessor at the string section instead, which is mapped but + // is not code. + let field = POINT + 12; + let displacement = (REFLSTR as i64 - field as i64) as i32 as u32; + bytes[field as usize..field as usize + 4].copy_from_slice(&displacement.to_le_bytes()); + let found = read(&bytes); + assert!(found.types[0].accessor.is_none()); + assert!( + found.warnings.iter().any(|w| w.contains("executable")), + "{:#?}", + found.warnings + ); +} + +#[test] +fn a_type_record_pointing_out_of_the_image_is_refused() { + let mut bytes = synth_swift(); + bytes[TYPES as usize..TYPES as usize + 4].copy_from_slice(&0x4000_0000u32.to_le_bytes()); + let found = read(&bytes); + assert_eq!(found.types.len(), 1, "the unmapped descriptor was believed"); + assert!( + found + .warnings + .iter() + .any(|w| w.contains("outside the image")) + ); +} + +#[test] +fn a_field_count_larger_than_the_section_allocates_nothing() { + let mut bytes = synth_swift(); + bytes[(FIELDMD + 12) as usize..(FIELDMD + 12) as usize + 4] + .copy_from_slice(&u32::MAX.to_le_bytes()); + let started = Instant::now(); + let found = read(&bytes); + assert!( + started.elapsed() < Duration::from_secs(2), + "a lying field count took too long" + ); + // Room for three more records is all there is behind the header. + assert!(found.field_descriptors[0].fields.len() <= 3); + assert!(found.warnings.iter().any(|w| w.contains("room for"))); +} + +#[test] +fn a_parent_chain_that_points_at_itself_terminates() { + let mut bytes = synth_swift(); + // The module's parent is itself, and the type's parent is the module. + let field = MODULE + 4; + let displacement = (MODULE as i64 - field as i64) as i32 as u32; + bytes[field as usize..field as usize + 4].copy_from_slice(&displacement.to_le_bytes()); + let started = Instant::now(); + let found = read(&bytes); + assert!(started.elapsed() < Duration::from_secs(2)); + // Eight hops and then it stops, rather than running forever. + assert!(found.types[0].qualified_name.ends_with("Point")); +} + +#[test] +fn a_record_size_the_walk_cannot_advance_on_is_refused() { + let mut bytes = synth_swift(); + bytes[(FIELDMD + 10) as usize..(FIELDMD + 10) as usize + 2] + .copy_from_slice(&0u16.to_le_bytes()); + let found = read(&bytes); + assert!(found.field_descriptors.is_empty()); + assert!( + found + .warnings + .iter() + .any(|w| w.contains("byte records") || w.contains("could not be read")) + ); +} + +#[test] +fn every_truncation_of_the_image_is_survivable() { + let full = synth_swift(); + let started = Instant::now(); + for cut in (0x20..full.len()).step_by(16) { + if let Ok(obj) = load(&full[..cut], &LoadOptions::default()) { + let found = metadata::read(&obj); + // The contract is a value, not a guess: whatever survives has to + // be internally consistent. + if let Some(s) = found.swift { + for d in &s.field_descriptors { + assert!(d.fields.len() < 0x1000); + } + } + } + } + assert!( + started.elapsed() < Duration::from_secs(20), + "truncation sweep took {:?}", + started.elapsed() + ); +} + +#[test] +fn targeted_corruption_of_every_descriptor_byte_never_panics() { + let full = synth_swift(); + let started = Instant::now(); + // Only the regions the reader walks, one byte at a time, set to the two + // values that break arithmetic: all ones and zero. + for region in [CONST, TYPEREF, FIELDMD, TYPES, PROTO, PROTOS, GOT] { + for i in region..region + 0x60 { + for value in [0xffu8, 0x00] { + let mut bad = full.clone(); + bad[i as usize] = value; + if let Ok(obj) = load(&bad, &LoadOptions::default()) { + let _ = metadata::read(&obj); + } + } + } + } + assert!( + started.elapsed() < Duration::from_secs(30), + "corruption sweep took {:?}", + started.elapsed() + ); +} + +#[test] +fn an_image_with_no_swift_sections_says_so() { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + let Ok(bytes) = std::fs::read(dir.join("greeter.macho.a64.o")) else { + return; + }; + let Ok(obj) = load(&bytes, &LoadOptions::default()) else { + return; + }; + let found = swift::read(&obj); + assert!(found.is_empty()); + assert!(found.warnings.is_empty()); +} + +#[test] +fn the_elf_section_names_are_read_too() { + // Swift on ELF spells the same sections without a segment prefix, and two + // of them under different names. Only the matching differs, so renaming + // the sections of a loaded image tests exactly the part that is different + // without pretending to be an ELF Swift binary. + let bytes = synth_swift(); + let mut obj = load(&bytes, &LoadOptions::default()).unwrap(); + for s in &mut obj.sections { + s.name = match s.name.rsplit(',').next().unwrap_or_default() { + "__swift5_types" => "swift5_type_metadata".into(), + "__swift5_fieldmd" => "swift5_fieldmd".into(), + "__swift5_proto" => "swift5_protocol_conformances".into(), + "__swift5_protos" => "swift5_protocols".into(), + "__swift5_typeref" => "swift5_typeref".into(), + "__swift5_reflstr" => "swift5_reflstr".into(), + other => other.into(), + }; + } + let found = swift::read(&obj); + assert_eq!(found.types.len(), 2); + assert_eq!(found.field_descriptors.len(), 2); + assert_eq!(found.conformances.len(), 1); + assert_eq!(found.protocols.len(), 1); + assert!(found.warnings.is_empty(), "{:#?}", found.warnings); +} diff --git a/crates/e5r-format/tests/windows.rs b/crates/e5r-format/tests/windows.rs new file mode 100644 index 0000000..2b6e1d5 --- /dev/null +++ b/crates/e5r-format/tests/windows.rs @@ -0,0 +1,1251 @@ +//! The Windows-specific data directories: TLS callbacks, `.pdata`/`.xdata`, +//! SEH scope tables, base relocations and the load config. +//! +//! Two halves, for two different questions. +//! +//! The first half runs against real linked PE images from `fixtures/build`, +//! with `llvm-readobj` as the oracle. Comparing a parser against itself proves +//! nothing, so every number that LLVM prints is read back out of LLVM and the +//! two lists are compared entry for entry: runtime function bounds, unwind +//! codes, frame registers, handler addresses, ARM64 packed frame descriptions, +//! base relocations and the guard function table. LLVM does not print SEH scope +//! tables, so those are checked against the one anchor it does print, the +//! handler routine, plus the shape the source is known to have. +//! +//! The second half builds PE images byte by byte to reach the paths a working +//! compiler never emits: a callback array with no terminator, a relocation +//! block shorter than its own header, an unwind chain that points at itself, a +//! C++ `FuncInfo` where a scope table would be. Those are the inputs that +//! decide whether the loader is safe to point at something hostile. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use e5r_core::{Addr, Arch, Caps, Evidence, Strength}; +use e5r_format::pdata::{ScopeHandler, UnwindOp}; +use e5r_format::{LoadOptions, Object, pe}; + +// --------------------------------------------------------------------------- +// The corpus and the oracle. +// --------------------------------------------------------------------------- + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option<(Object, pe::WindowsInfo)> { + let p = corpus()?.join(name); + let data = std::fs::read(&p).ok()?; + Some( + pe::load_windows(&data, &LoadOptions::default()) + .unwrap_or_else(|e| panic!("loading {}: {e}", p.display())), + ) +} + +/// `llvm-readobj`, under whichever name this machine installed it as. +fn readobj(name: &str, args: &[&str]) -> Option { + let p = corpus()?.join(name); + if !p.exists() { + return None; + } + for tool in ["llvm-readobj", "llvm-readobj-18", "llvm-readobj-15"] { + let Ok(out) = Command::new(tool).args(args).arg(&p).output() else { + continue; + }; + if out.status.success() { + return Some(String::from_utf8_lossy(&out.stdout).into_owned()); + } + } + None +} + +/// The hexadecimal number in `Key: 0x...` or `Key: (0x...)`, for the first line +/// whose key matches. +fn field_hex(text: &str, key: &str) -> Option { + field_hex_all(text, key).into_iter().next() +} + +fn field_hex_all(text: &str, key: &str) -> Vec { + text.lines() + .filter_map(|l| { + let l = l.trim(); + let rest = l.strip_prefix(key)?.strip_prefix(':')?; + let rest = rest.trim().trim_start_matches('(').trim_end_matches(')'); + let digits = rest.strip_prefix("0x")?; + u64::from_str_radix(digits.split_whitespace().next()?, 16).ok() + }) + .collect() +} + +fn field_dec_all(text: &str, key: &str) -> Vec { + text.lines() + .filter_map(|l| { + let l = l.trim(); + let rest = l.strip_prefix(key)?.strip_prefix(':')?; + rest.trim().parse::().ok() + }) + .collect() +} + +/// The images the fixture script builds, when it has run. +const IMAGES: [&str; 4] = [ + "win.x64.O0.exe", + "win.x64.O2.exe", + "win.a64.O0.exe", + "win.a64.O2.exe", +]; + +// --------------------------------------------------------------------------- +// Exception directory, against llvm-readobj --unwind. +// --------------------------------------------------------------------------- + +#[test] +fn runtime_function_bounds_match_llvm_readobj() { + let mut checked = 0; + for name in IMAGES { + let Some((_, win)) = open(name) else { continue }; + let Some(text) = readobj(name, &["--unwind"]) else { + continue; + }; + // x86-64 prints a start and an end; ARM64 prints a start and a length. + let want: Vec<(u64, Option)> = if name.contains("x64") { + let starts = field_hex_all(&text, "StartAddress"); + let ends = field_hex_all(&text, "EndAddress"); + assert_eq!(starts.len(), ends.len(), "{name}: malformed oracle output"); + starts.into_iter().zip(ends.into_iter().map(Some)).collect() + } else { + let starts = field_hex_all(&text, "Function"); + let lengths = field_dec_all(&text, "FunctionLength"); + assert_eq!( + starts.len(), + lengths.len(), + "{name}: every ARM64 entry should carry a length" + ); + starts + .iter() + .zip(&lengths) + .map(|(s, n)| (*s, Some(s + n))) + .collect() + }; + assert!(!want.is_empty(), "{name}: the oracle found no entries"); + + let got: Vec<(u64, Option)> = win + .exceptions + .functions + .iter() + .map(|f| (f.start.get(), f.end.map(|e| e.get()))) + .collect(); + assert_eq!(got, want, "{name}: runtime function bounds disagree"); + checked += 1; + } + assert!(checked > 0 || corpus().is_none(), "no image was checked"); +} + +#[test] +fn every_runtime_function_is_a_proven_hint_with_its_size() { + for name in IMAGES { + let Some((obj, win)) = open(name) else { + continue; + }; + for f in &win.exceptions.functions { + let hint = obj + .function_hints + .iter() + .find(|h| h.addr == f.start) + .unwrap_or_else(|| panic!("{name}: no hint for the function at {}", f.start)); + assert_eq!( + hint.provenance.strength(), + Strength::Proven, + "{name}: {} is not proven", + f.start + ); + assert!( + hint.provenance.has(Evidence::PeUnwind), + "{name}: {} does not cite the unwind record", + f.start + ); + if let Some(end) = f.end { + assert_eq!( + hint.size, + Some(end.get() - f.start.get()), + "{name}: {} lost its size", + f.start + ); + } + } + } +} + +/// Render an unwind code the way `llvm-readobj` does, so the two can be +/// compared as text rather than through a second opinion about the encoding. +fn render(op: &UnwindOp, frame_register: Option, frame_offset: u32) -> String { + fn reg(n: u8) -> String { + e5r_format::pdata::x64_register(n).to_uppercase() + } + match op { + UnwindOp::PushNonVolatile { reg: r } => format!("PUSH_NONVOL reg={}", reg(*r)), + // LLVM keeps the two allocation encodings apart; the decoded size is + // the substance and is what both sides normalize to. + UnwindOp::Alloc { bytes } => format!("ALLOC size={bytes}"), + UnwindOp::SetFramePointer => format!( + "SET_FPREG reg={}, offset={:#x}", + reg(frame_register.unwrap_or(0)), + frame_offset + ), + UnwindOp::SaveNonVolatile { reg: r, offset } => { + format!("SAVE_NONVOL reg={}, offset={offset:#x}", reg(*r)) + } + UnwindOp::SaveXmm128 { reg: r, offset } => { + format!("SAVE_XMM128 reg=XMM{r}, offset={offset:#x}") + } + UnwindOp::PushMachineFrame { error_code } => { + format!("PUSH_MACHFRAME errcode={error_code}") + } + UnwindOp::Epilogue => "EPILOG".into(), + UnwindOp::Unknown { op, info } => format!("UNKNOWN op={op} info={info}"), + } +} + +fn normalize_oracle_code(s: &str) -> String { + if let Some(rest) = s.strip_prefix("ALLOC_LARGE size=") { + return format!("ALLOC size={rest}"); + } + if let Some(rest) = s.strip_prefix("ALLOC_SMALL size=") { + return format!("ALLOC size={rest}"); + } + s.to_string() +} + +#[test] +fn x64_unwind_codes_match_llvm_readobj() { + for name in ["win.x64.O0.exe", "win.x64.O2.exe"] { + let Some((_, win)) = open(name) else { continue }; + let Some(text) = readobj(name, &["--unwind"]) else { + continue; + }; + // One block per RuntimeFunction, in directory order. + let blocks: Vec<&str> = text.split("RuntimeFunction {").skip(1).collect(); + assert_eq!( + blocks.len(), + win.exceptions.unwind.len(), + "{name}: the oracle and the loader disagree on how many records exist" + ); + let mut codes_seen = 0; + for (block, info) in blocks.iter().zip(&win.exceptions.unwind) { + assert_eq!( + field_hex(block, "StartAddress"), + Some(info.function.get()), + "{name}: records are out of order" + ); + assert_eq!( + field_dec_all(block, "Version").first().copied(), + Some(info.version as u64), + "{name}: version disagrees for {}", + info.function + ); + assert_eq!( + field_dec_all(block, "PrologSize").first().copied(), + Some(info.prolog_size as u64), + "{name}: prologue size disagrees for {}", + info.function + ); + assert_eq!( + field_hex(block, "Handler"), + info.handler.map(|h| h.get()), + "{name}: handler disagrees for {}", + info.function + ); + // FrameRegister prints as "RBP (0x5)" or "-" when there is none. + let want_frame = block + .lines() + .find_map(|l| l.trim().strip_prefix("FrameRegister:")) + .map(|v| v.trim().to_string()); + let got_frame = info + .frame_register + .map(|r| e5r_format::pdata::x64_register(r).to_uppercase()); + match (want_frame.as_deref(), got_frame) { + (Some("-"), g) => assert_eq!(g, None, "{name}: invented a frame register"), + (Some(w), Some(g)) => assert!( + w.starts_with(&g), + "{name}: frame register {g} is not the oracle's {w}" + ), + (Some(w), None) => panic!("{name}: missed the frame register {w}"), + (None, _) => {} + } + + let want: Vec = block + .lines() + .filter_map(|l| { + let l = l.trim(); + let rest = l.strip_prefix("0x")?; + let (_, text) = rest.split_once(": ")?; + Some(normalize_oracle_code(text)) + }) + .collect(); + // A chained record's own codes come first; the parent's are + // appended, and the oracle does not follow the chain. + let got: Vec = info + .codes + .iter() + .take(want.len()) + .map(|c| render(&c.op, info.frame_register, info.frame_offset)) + .collect(); + assert_eq!( + got, want, + "{name}: prologue for {} disagrees with the oracle", + info.function + ); + codes_seen += want.len(); + } + assert!( + codes_seen > 10, + "{name}: only {codes_seen} unwind codes, the fixture is not exercising much" + ); + } +} + +#[test] +fn arm64_packed_unwind_matches_llvm_readobj() { + for name in ["win.a64.O0.exe", "win.a64.O2.exe"] { + let Some((_, win)) = open(name) else { continue }; + let Some(text) = readobj(name, &["--unwind"]) else { + continue; + }; + let blocks: Vec<&str> = text.split("RuntimeFunction {").skip(1).collect(); + assert_eq!(blocks.len(), win.exceptions.functions.len()); + let mut packed_seen = 0; + for (block, f) in blocks.iter().zip(&win.exceptions.functions) { + // The oracle prints RegF only for the packed form. + let Some(reg_f) = field_dec_all(block, "RegF").first().copied() else { + assert!( + f.packed.is_none(), + "{name}: {} was decoded as packed but the oracle read .xdata", + f.start + ); + continue; + }; + let p = f + .packed + .unwrap_or_else(|| panic!("{name}: {} should be packed", f.start)); + assert_eq!(reg_f, p.reg_f as u64, "{name}: RegF at {}", f.start); + assert_eq!( + field_dec_all(block, "RegI").first().copied(), + Some(p.reg_i as u64), + "{name}: RegI at {}", + f.start + ); + assert_eq!( + field_dec_all(block, "CR").first().copied(), + Some(p.cr as u64), + "{name}: CR at {}", + f.start + ); + assert_eq!( + field_dec_all(block, "FrameSize").first().copied(), + Some(p.frame_size as u64), + "{name}: FrameSize at {}", + f.start + ); + assert_eq!( + field_dec_all(block, "FunctionLength").first().copied(), + Some(p.function_length as u64), + "{name}: FunctionLength at {}", + f.start + ); + let homed = block.contains("HomedParameters: Yes"); + assert_eq!(homed, p.homed_parameters, "{name}: H at {}", f.start); + packed_seen += 1; + } + assert!( + packed_seen > 0, + "{name}: no packed entry, so the packed decoder was never measured" + ); + } +} + +#[test] +fn arm64_exception_handlers_match_llvm_readobj() { + for name in ["win.a64.O0.exe", "win.a64.O2.exe"] { + let Some((obj, _)) = open(name) else { continue }; + let Some(text) = readobj(name, &["--unwind"]) else { + continue; + }; + let routines = field_hex_all(&text, "Routine"); + assert!( + !routines.is_empty(), + "{name}: the fixture should carry an ARM64 handler" + ); + for r in routines { + let hint = obj + .function_hints + .iter() + .find(|h| h.addr == Addr(r)) + .unwrap_or_else(|| panic!("{name}: the handler at {r:#x} produced no hint")); + assert_eq!(hint.provenance.strength(), Strength::Proven); + assert!(hint.provenance.has(Evidence::PeUnwind)); + } + } +} + +// --------------------------------------------------------------------------- +// SEH scope tables. +// --------------------------------------------------------------------------- + +#[test] +fn the_seh_scope_table_names_a_try_region_inside_its_function() { + // llvm-readobj does not print scope tables, so the oracle here is the + // handler routine it does print, plus the shape the fixture's source has: + // one __try region guarded by __except(1), which the compiler folds to the + // constant EXCEPTION_EXECUTE_HANDLER. + for name in ["win.x64.O0.exe", "win.x64.O2.exe"] { + let Some((obj, win)) = open(name) else { + continue; + }; + let Some(text) = readobj(name, &["--unwind"]) else { + continue; + }; + let handlers = field_hex_all(&text, "Handler"); + assert!(!handlers.is_empty(), "{name}: the fixture lost its handler"); + + let with_scopes: Vec<_> = win + .exceptions + .unwind + .iter() + .filter(|u| !u.scopes.is_empty()) + .collect(); + assert!( + !with_scopes.is_empty(), + "{name}: no scope table was recovered" + ); + for info in with_scopes { + assert!( + handlers.contains(&info.handler.expect("a scope table needs a handler").get()), + "{name}: the handler is not one the oracle listed" + ); + let func = win + .exceptions + .functions + .iter() + .find(|f| f.start == info.function) + .expect("scope table without its function"); + let end = func.end.expect("x86-64 always states an end"); + for s in &info.scopes { + assert!( + s.begin >= func.start && s.end <= end && s.begin < s.end, + "{name}: guarded region {}..{} escapes {}..{end}", + s.begin, + s.end, + func.start + ); + assert_eq!( + s.handler, + ScopeHandler::Constant(1), + "{name}: __except(1) should fold to a constant filter" + ); + let target = s.target.expect("__except has a body to jump to"); + assert!(obj.memory.is_executable(target)); + } + } + } +} + +#[test] +fn the_handler_routine_becomes_a_proven_hint() { + for name in ["win.x64.O0.exe", "win.x64.O2.exe"] { + let Some((obj, win)) = open(name) else { + continue; + }; + let mut seen = 0; + for info in &win.exceptions.unwind { + let Some(h) = info.handler else { continue }; + let hint = obj + .function_hints + .iter() + .find(|x| x.addr == h) + .unwrap_or_else(|| panic!("{name}: the handler at {h} produced no hint")); + assert_eq!(hint.provenance.strength(), Strength::Proven); + seen += 1; + } + assert!(seen > 0, "{name}: no handler was found at all"); + } +} + +// --------------------------------------------------------------------------- +// TLS. +// --------------------------------------------------------------------------- + +#[test] +fn the_tls_directory_matches_llvm_readobj() { + for name in IMAGES { + let Some((_, win)) = open(name) else { continue }; + let Some(text) = readobj(name, &["--coff-tls-directory"]) else { + continue; + }; + let tls = win + .tls + .as_ref() + .unwrap_or_else(|| panic!("{name}: the TLS directory was not read")); + assert_eq!( + tls.callback_array.map(|a| a.get()), + field_hex(&text, "AddressOfCallBacks"), + "{name}: callback array address" + ); + assert_eq!( + tls.index.map(|a| a.get()), + field_hex(&text, "AddressOfIndex"), + "{name}: TLS index address" + ); + assert_eq!( + tls.raw_data.map(|r| r.start().get()), + field_hex(&text, "StartAddressOfRawData"), + "{name}: raw data start" + ); + assert_eq!( + tls.raw_data.map(|r| r.end().get()), + field_hex(&text, "EndAddressOfRawData"), + "{name}: raw data end" + ); + } +} + +#[test] +fn tls_callbacks_are_functions_the_guard_table_also_knows() { + // The fixture registers two callbacks. Nothing but the TLS array points at + // them, so the check that they are real comes from a second table: the + // linker's control-flow-guard list of every address-taken function, which + // llvm-readobj prints and which must contain both. + for name in IMAGES { + let Some((obj, win)) = open(name) else { + continue; + }; + let tls = win.tls.as_ref().expect("no TLS directory"); + assert_eq!( + tls.callbacks.len(), + 2, + "{name}: expected the two callbacks the fixture registers" + ); + let Some(text) = readobj(name, &["--coff-load-config"]) else { + continue; + }; + let fids: Vec = text + .lines() + .skip_while(|l| !l.contains("GuardFidTable")) + .filter_map(|l| { + let l = l.trim(); + u64::from_str_radix(l.strip_prefix("0x")?, 16).ok() + }) + .collect(); + assert!( + !fids.is_empty(), + "{name}: the oracle printed no guard table" + ); + for (i, cb) in tls.callbacks.iter().enumerate() { + assert!( + fids.contains(&cb.get()), + "{name}: callback {i} at {cb} is not an address-taken function" + ); + let hint = obj + .function_hints + .iter() + .find(|h| h.addr == *cb) + .unwrap_or_else(|| panic!("{name}: callback {i} produced no hint")); + assert_eq!( + hint.provenance.strength(), + Strength::Proven, + "{name}: a callback landing in executable memory is proven" + ); + assert!(hint.provenance.has(Evidence::InitArray)); + assert_eq!( + hint.name.as_deref(), + Some(format!("tls_callback_{i}")).as_deref() + ); + } + assert_eq!( + obj.metadata.get("pe.tls_callbacks").map(String::as_str), + Some("2") + ); + } +} + +// --------------------------------------------------------------------------- +// Base relocations and load config. +// --------------------------------------------------------------------------- + +#[test] +fn base_relocations_match_llvm_readobj() { + for name in IMAGES { + let Some((obj, win)) = open(name) else { + continue; + }; + let Some(text) = readobj(name, &["--coff-basereloc"]) else { + continue; + }; + // The oracle prints a type name and an RVA; the loader reports a type + // number and a loaded address. The name goes to a number rather than + // the number to a name, so the test does not depend on our spelling. + let number = |n: &str| -> u8 { + match n { + "HIGH" => 1, + "LOW" => 2, + "HIGHLOW" => 3, + "HIGHADJ" => 4, + "DIR64" => 10, + other => panic!("{name}: the oracle printed an unmapped type {other}"), + } + }; + let kinds: Vec = text + .lines() + .filter_map(|l| Some(number(l.trim().strip_prefix("Type:")?.trim()))) + .collect(); + let addrs = field_hex_all(&text, "Address"); + assert_eq!(kinds.len(), addrs.len()); + let want: Vec<(u8, u64)> = kinds.into_iter().zip(addrs).collect(); + assert!(!want.is_empty(), "{name}: the oracle found no relocations"); + let base = obj.image_base.get(); + let got: Vec<(u8, u64)> = win + .relocations + .iter() + .map(|r| (r.kind, r.addr.get() - base)) + .collect(); + assert_eq!(got, want, "{name}: base relocations disagree"); + assert_eq!( + obj.metadata.get("pe.relocations").map(String::as_str), + Some(got.len().to_string().as_str()) + ); + } +} + +#[test] +fn the_load_config_guard_table_matches_llvm_readobj() { + for name in IMAGES { + let Some((_, win)) = open(name) else { continue }; + let Some(text) = readobj(name, &["--coff-load-config"]) else { + continue; + }; + let cfg = win + .load_config + .as_ref() + .unwrap_or_else(|| panic!("{name}: no load config")); + assert_eq!( + cfg.security_cookie.map(|a| a.get()), + field_hex(&text, "SecurityCookie"), + "{name}: security cookie" + ); + assert_eq!( + cfg.guard_cf_function_table.map(|a| a.get()), + field_hex(&text, "GuardCFFunctionTable"), + "{name}: guard table address" + ); + assert_eq!( + cfg.guard_cf_function_count, + field_dec_all(&text, "GuardCFFunctionCount") + .first() + .copied() + .unwrap_or(0), + "{name}: guard table count" + ); + let fids: Vec = text + .lines() + .skip_while(|l| !l.contains("GuardFidTable")) + .filter_map(|l| u64::from_str_radix(l.trim().strip_prefix("0x")?, 16).ok()) + .collect(); + let got: Vec = cfg.guard_functions.iter().map(|a| a.get()).collect(); + assert_eq!(got, fids, "{name}: guard function list"); + } +} + +// --------------------------------------------------------------------------- +// Byte-built images, for what a compiler will not produce. +// --------------------------------------------------------------------------- + +const BASE: u64 = 0x1_4000_0000; +const TEXT_RVA: u32 = 0x1000; +const RDATA_RVA: u32 = 0x2000; +const TEXT_OFF: usize = 0x400; +const RDATA_OFF: usize = 0x600; +const RDATA_LEN: usize = 0x400; + +/// A PE32+ image with one code section and one read-only data section whose +/// contents and data directories the caller chooses. +/// +/// `dirs` is `(index, rva, size)`. `rdata` is placed at RVA 0x2000; the helper +/// `rdata_rva` below converts an offset inside it to an RVA. +fn synth(rdata: &[u8], dirs: &[(usize, u32, u32)]) -> Vec { + assert!(rdata.len() <= RDATA_LEN); + let mut b: Vec = Vec::new(); + let push = |b: &mut Vec, v: &[u8]| b.extend_from_slice(v); + + b.extend_from_slice(b"MZ"); + b.resize(0x3c, 0); + push(&mut b, &0x80u32.to_le_bytes()); + b.resize(0x80, 0); + b.extend_from_slice(b"PE\0\0"); + + // COFF header. + push(&mut b, &0x8664u16.to_le_bytes()); + push(&mut b, &2u16.to_le_bytes()); + push(&mut b, &0u32.to_le_bytes()); + push(&mut b, &0u32.to_le_bytes()); + push(&mut b, &0u32.to_le_bytes()); + push(&mut b, &240u16.to_le_bytes()); + push(&mut b, &0x0022u16.to_le_bytes()); + + let opt_start = b.len(); + push(&mut b, &0x20bu16.to_le_bytes()); + b.extend_from_slice(&[14, 0]); + for v in [0x200u32, 0x200, 0, TEXT_RVA, TEXT_RVA] { + push(&mut b, &v.to_le_bytes()); + } + push(&mut b, &BASE.to_le_bytes()); + push(&mut b, &0x1000u32.to_le_bytes()); + push(&mut b, &0x200u32.to_le_bytes()); + for v in [6u16, 0, 0, 0, 6, 0] { + push(&mut b, &v.to_le_bytes()); + } + push(&mut b, &0u32.to_le_bytes()); + push(&mut b, &0x4000u32.to_le_bytes()); + push(&mut b, &0x400u32.to_le_bytes()); + push(&mut b, &0u32.to_le_bytes()); + push(&mut b, &3u16.to_le_bytes()); + push(&mut b, &0x0160u16.to_le_bytes()); + for v in [0x100000u64, 0x1000, 0x100000, 0x1000] { + push(&mut b, &v.to_le_bytes()); + } + push(&mut b, &0u32.to_le_bytes()); + push(&mut b, &16u32.to_le_bytes()); + let dirs_at = b.len(); + for _ in 0..16 { + push(&mut b, &0u64.to_le_bytes()); + } + assert_eq!(b.len() - opt_start, 240); + + let sec = |b: &mut Vec, name: &[u8; 8], rva: u32, size: u32, off: u32, ch: u32| { + b.extend_from_slice(name); + for v in [size, rva, size, off, 0, 0] { + b.extend_from_slice(&v.to_le_bytes()); + } + b.extend_from_slice(&0u16.to_le_bytes()); + b.extend_from_slice(&0u16.to_le_bytes()); + b.extend_from_slice(&ch.to_le_bytes()); + }; + sec( + &mut b, + b".text\0\0\0", + TEXT_RVA, + 0x200, + TEXT_OFF as u32, + 0x6000_0020, + ); + sec( + &mut b, + b".rdata\0\0", + RDATA_RVA, + RDATA_LEN as u32, + RDATA_OFF as u32, + 0x4000_0040, + ); + + b.resize(TEXT_OFF, 0); + b.extend_from_slice(&[0xc3; 0x200]); + b.resize(RDATA_OFF, 0); + b.extend_from_slice(rdata); + b.resize(RDATA_OFF + RDATA_LEN, 0); + + for (i, rva, size) in dirs { + let at = dirs_at + i * 8; + b[at..at + 4].copy_from_slice(&rva.to_le_bytes()); + b[at + 4..at + 8].copy_from_slice(&size.to_le_bytes()); + } + b +} + +fn rdata_rva(off: usize) -> u32 { + RDATA_RVA + off as u32 +} + +fn rdata_va(off: usize) -> u64 { + BASE + rdata_rva(off) as u64 +} + +fn text_va(off: u32) -> u64 { + BASE + TEXT_RVA as u64 + off as u64 +} + +fn le64(v: u64) -> [u8; 8] { + v.to_le_bytes() +} + +fn le32(v: u32) -> [u8; 4] { + v.to_le_bytes() +} + +/// A TLS directory at offset 0 of `.rdata` pointing at a callback array whose +/// contents the caller supplies. +fn tls_image(callbacks: &[u64], terminate: bool) -> Vec { + let array_at = 0x40usize; + let mut rdata = vec![0u8; RDATA_LEN]; + rdata[0x00..0x08].copy_from_slice(&le64(rdata_va(0x100))); + rdata[0x08..0x10].copy_from_slice(&le64(rdata_va(0x140))); + rdata[0x10..0x18].copy_from_slice(&le64(rdata_va(0x180))); + rdata[0x18..0x20].copy_from_slice(&le64(rdata_va(array_at))); + for (i, cb) in callbacks.iter().enumerate() { + let at = array_at + i * 8; + rdata[at..at + 8].copy_from_slice(&le64(*cb)); + } + if !terminate { + // Every remaining slot is a live pointer, so nothing terminates it. + let mut at = array_at + callbacks.len() * 8; + while at + 8 <= RDATA_LEN { + rdata[at..at + 8].copy_from_slice(&le64(text_va(0))); + at += 8; + } + } + synth(&rdata, &[(9, rdata_rva(0), 40)]) +} + +#[test] +fn a_tls_callback_in_executable_memory_is_proven() { + let data = tls_image(&[text_va(0x10), text_va(0x20)], true); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + let tls = win.tls.expect("no TLS directory"); + assert_eq!( + tls.callbacks, + vec![Addr(text_va(0x10)), Addr(text_va(0x20))] + ); + for (i, cb) in tls.callbacks.iter().enumerate() { + let h = obj.function_hints.iter().find(|h| h.addr == *cb).unwrap(); + assert_eq!(h.provenance.strength(), Strength::Proven); + assert_eq!( + h.name.as_deref(), + Some(format!("tls_callback_{i}")).as_deref() + ); + } +} + +#[test] +fn a_tls_callback_outside_executable_memory_is_a_warning_not_a_hint() { + // The pointer lands in .rdata, which the file marks read-only. It is not a + // function and claiming it were one would be the invention the whole + // evidence model exists to prevent. + let data = tls_image(&[rdata_va(0x200)], true); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + assert!(win.tls.unwrap().callbacks.is_empty()); + assert!( + obj.function_hints + .iter() + .all(|h| h.addr != Addr(rdata_va(0x200))) + ); + assert!( + obj.warnings.iter().any(|w| w.contains("not executable")), + "no warning: {:?}", + obj.warnings + ); +} + +#[test] +fn a_tls_callback_array_with_no_terminator_stops_at_the_section_end() { + let data = tls_image(&[text_va(0x10)], false); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + let tls = win.tls.expect("no TLS directory"); + // 0x400 bytes of .rdata, the array starting 0x40 in: 120 slots and no more. + assert_eq!(tls.callbacks.len(), (RDATA_LEN - 0x40) / 8); + assert!( + obj.warnings + .iter() + .any(|w| w.contains("without a terminator")), + "the walk must say it stopped early: {:?}", + obj.warnings + ); +} + +#[test] +fn a_tls_callback_array_outside_every_section_is_a_warning() { + let mut rdata = vec![0u8; RDATA_LEN]; + rdata[0x18..0x20].copy_from_slice(&le64(BASE + 0x0f00_0000)); + let data = synth(&rdata, &[(9, rdata_rva(0), 40)]); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + assert!(win.tls.unwrap().callbacks.is_empty()); + assert!(obj.warnings.iter().any(|w| w.contains("no section maps"))); +} + +/// An image with one runtime function covering `.text` and an UNWIND_INFO whose +/// bytes the caller supplies at offset 0x100 of `.rdata`. +fn unwind_image(unwind: &[u8], func_len: u32) -> Vec { + let mut rdata = vec![0u8; RDATA_LEN]; + // The exception directory: one entry at offset 0. + rdata[0x00..0x04].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x04..0x08].copy_from_slice(&le32(TEXT_RVA + func_len)); + rdata[0x08..0x0c].copy_from_slice(&le32(rdata_rva(0x100))); + rdata[0x100..0x100 + unwind.len()].copy_from_slice(unwind); + synth(&rdata, &[(3, rdata_rva(0), 12)]) +} + +#[test] +fn an_unwind_prologue_is_decoded_in_order() { + // version 1, no flags; prologue 9 bytes; three codes; frame register RBP + // with offset 0x20. + let unwind = [ + 0x01, 0x09, 0x03, 0x25, // header + 0x09, 0x03, // SET_FPREG at 9 + 0x04, 0x62, // ALLOC_SMALL 0x38 + 0x01, 0x50, // PUSH_NONVOL RBP + 0x00, 0x00, // padding for the odd count + ]; + let data = unwind_image(&unwind, 0x40); + let (_, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + let info = &win.exceptions.unwind[0]; + assert_eq!(info.version, 1); + assert_eq!(info.prolog_size, 9); + assert_eq!( + info.frame_register.map(e5r_format::pdata::x64_register), + Some("rbp") + ); + assert_eq!(info.frame_offset, 0x20); + assert_eq!( + info.codes.iter().map(|c| c.op).collect::>(), + vec![ + UnwindOp::SetFramePointer, + UnwindOp::Alloc { bytes: 0x38 }, + UnwindOp::PushNonVolatile { reg: 5 }, + ] + ); + assert_eq!(info.stack_alloc, 0x38); +} + +#[test] +fn a_chained_unwind_record_contributes_the_parent_prologue() { + let mut rdata = vec![0u8; RDATA_LEN]; + // Two runtime functions: a funclet at +0x80 chaining to the one at +0. + rdata[0x00..0x04].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x04..0x08].copy_from_slice(&le32(TEXT_RVA + 0x80)); + rdata[0x08..0x0c].copy_from_slice(&le32(rdata_rva(0x100))); + rdata[0x0c..0x10].copy_from_slice(&le32(TEXT_RVA + 0x80)); + rdata[0x10..0x14].copy_from_slice(&le32(TEXT_RVA + 0xc0)); + rdata[0x14..0x18].copy_from_slice(&le32(rdata_rva(0x140))); + + // The parent: one push. + rdata[0x100..0x104].copy_from_slice(&[0x01, 0x04, 0x01, 0x00]); + rdata[0x104..0x108].copy_from_slice(&[0x04, 0x30, 0x00, 0x00]); + // The funclet: UNW_FLAG_CHAININFO, one alloc, then the parent's entry. + rdata[0x140..0x144].copy_from_slice(&[0x21, 0x04, 0x01, 0x00]); + rdata[0x144..0x148].copy_from_slice(&[0x04, 0x22, 0x00, 0x00]); + rdata[0x148..0x14c].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x14c..0x150].copy_from_slice(&le32(TEXT_RVA + 0x80)); + rdata[0x150..0x154].copy_from_slice(&le32(rdata_rva(0x100))); + + let data = synth(&rdata, &[(3, rdata_rva(0), 24)]); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + let funclet = win + .exceptions + .unwind + .iter() + .find(|u| u.function == Addr(text_va(0x80))) + .expect("the funclet lost its record"); + assert_eq!(funclet.chained, Some(Addr(text_va(0)))); + // Its own allocation, then the register the parent pushed. + assert_eq!( + funclet.codes.iter().map(|c| c.op).collect::>(), + vec![ + UnwindOp::Alloc { bytes: 0x18 }, + UnwindOp::PushNonVolatile { reg: 3 }, + ] + ); + assert!( + obj.function_hints + .iter() + .any(|h| h.addr == Addr(text_va(0))) + ); +} + +#[test] +fn an_unwind_chain_that_points_at_itself_terminates() { + let mut rdata = vec![0u8; RDATA_LEN]; + rdata[0x00..0x04].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x04..0x08].copy_from_slice(&le32(TEXT_RVA + 0x80)); + rdata[0x08..0x0c].copy_from_slice(&le32(rdata_rva(0x100))); + // A chained record naming a different record that names this one back. + rdata[0x100..0x104].copy_from_slice(&[0x21, 0x00, 0x00, 0x00]); + rdata[0x104..0x108].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x108..0x10c].copy_from_slice(&le32(TEXT_RVA + 0x80)); + rdata[0x10c..0x110].copy_from_slice(&le32(rdata_rva(0x140))); + rdata[0x140..0x144].copy_from_slice(&[0x21, 0x00, 0x00, 0x00]); + rdata[0x144..0x148].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x148..0x14c].copy_from_slice(&le32(TEXT_RVA + 0x80)); + rdata[0x14c..0x150].copy_from_slice(&le32(rdata_rva(0x100))); + + let data = synth(&rdata, &[(3, rdata_rva(0), 12)]); + let (obj, _) = pe::load_windows(&data, &LoadOptions::default()).expect("load"); + assert!( + obj.warnings.iter().any(|w| w.contains("deep")), + "the chain limit should be reported: {:?}", + obj.warnings + ); +} + +/// An image whose UNWIND_INFO names a handler and puts `language` after it. +fn scope_image(language: &[u8]) -> Vec { + let mut rdata = vec![0u8; RDATA_LEN]; + rdata[0x00..0x04].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x04..0x08].copy_from_slice(&le32(TEXT_RVA + 0x80)); + rdata[0x08..0x0c].copy_from_slice(&le32(rdata_rva(0x100))); + // UNW_FLAG_EHANDLER, no codes. + rdata[0x100..0x104].copy_from_slice(&[0x09, 0x00, 0x00, 0x00]); + rdata[0x104..0x108].copy_from_slice(&le32(TEXT_RVA + 0x100)); + rdata[0x108..0x108 + language.len()].copy_from_slice(language); + synth(&rdata, &[(3, rdata_rva(0), 12)]) +} + +#[test] +fn a_scope_table_handler_outside_the_function_becomes_a_hint() { + let mut lang = Vec::new(); + lang.extend_from_slice(&le32(1)); + lang.extend_from_slice(&le32(TEXT_RVA + 0x10)); // begin + lang.extend_from_slice(&le32(TEXT_RVA + 0x20)); // end + lang.extend_from_slice(&le32(TEXT_RVA + 0x120)); // filter, a real funclet + lang.extend_from_slice(&le32(TEXT_RVA + 0x140)); // __except body + let data = scope_image(&lang); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + let scopes = &win.exceptions.unwind[0].scopes; + assert_eq!(scopes.len(), 1); + assert_eq!( + scopes[0].handler, + ScopeHandler::Address(Addr(text_va(0x120))) + ); + for a in [text_va(0x120), text_va(0x140)] { + let h = obj + .function_hints + .iter() + .find(|h| h.addr == Addr(a)) + .unwrap_or_else(|| panic!("{a:#x} produced no hint")); + assert_eq!(h.provenance.strength(), Strength::Proven); + assert!(h.provenance.has(Evidence::PeUnwind)); + } +} + +#[test] +fn a_scope_table_handler_outside_every_section_is_a_warning_not_a_hint() { + let mut lang = Vec::new(); + lang.extend_from_slice(&le32(1)); + lang.extend_from_slice(&le32(TEXT_RVA + 0x10)); + lang.extend_from_slice(&le32(TEXT_RVA + 0x20)); + lang.extend_from_slice(&le32(0x0f00_0000)); // nowhere at all + lang.extend_from_slice(&le32(0)); + let data = scope_image(&lang); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + // The entry is still recorded: the file said it, and saying what the file + // said is not the same as believing it. + assert_eq!(win.exceptions.unwind[0].scopes.len(), 1); + let bogus = Addr(BASE + 0x0f00_0000); + assert!(obj.function_hints.iter().all(|h| h.addr != bogus)); + assert!( + obj.warnings + .iter() + .any(|w| w.contains("outside every section")), + "{:?}", + obj.warnings + ); +} + +#[test] +fn a_cxx_funcinfo_is_not_read_as_a_scope_table() { + // __CxxFrameHandler3 stores one RVA here, not a count. Read as a count it + // asks for gigabytes, which is the first bound the parser checks. + let data = scope_image(&le32(0x0020_1234)); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + assert!(win.exceptions.unwind[0].scopes.is_empty()); + assert!( + obj.warnings.iter().any(|w| w.contains("not a scope table")), + "{:?}", + obj.warnings + ); +} + +#[test] +fn a_scope_table_entry_outside_its_function_refuses_the_whole_table() { + let mut lang = Vec::new(); + lang.extend_from_slice(&le32(2)); + lang.extend_from_slice(&le32(TEXT_RVA + 0x10)); + lang.extend_from_slice(&le32(TEXT_RVA + 0x20)); + lang.extend_from_slice(&le32(1)); + lang.extend_from_slice(&le32(TEXT_RVA + 0x30)); + // The second entry guards code the function does not contain. + lang.extend_from_slice(&le32(TEXT_RVA + 0x400)); + lang.extend_from_slice(&le32(TEXT_RVA + 0x410)); + lang.extend_from_slice(&le32(1)); + lang.extend_from_slice(&le32(TEXT_RVA + 0x420)); + let data = scope_image(&lang); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + assert!(win.exceptions.unwind[0].scopes.is_empty()); + assert!( + obj.warnings + .iter() + .any(|w| w.contains("not inside the function")) + ); +} + +#[test] +fn a_relocation_block_shorter_than_its_header_stops_the_walk() { + // The classic unbounded loop: a block size of four advances the cursor by + // nothing and the walk never ends. + let mut rdata = vec![0u8; RDATA_LEN]; + rdata[0x00..0x04].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x04..0x08].copy_from_slice(&le32(4)); + let data = synth(&rdata, &[(5, rdata_rva(0), 0x100)]); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + assert!(win.relocations.is_empty()); + assert!( + obj.warnings + .iter() + .any(|w| w.contains("fewer than its own header")), + "{:?}", + obj.warnings + ); +} + +#[test] +fn base_relocations_skip_padding_and_carry_the_highadj_addend() { + let mut rdata = vec![0u8; RDATA_LEN]; + rdata[0x00..0x04].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x04..0x08].copy_from_slice(&le32(8 + 2 * 5)); + // One offset past 0x7ff, so the twelve-bit field is pinned rather than only + // the low eleven bits a small section would exercise. + let entries: [u16; 5] = [ + 0xa000 | 0x008, // DIR64 at +8 + 0x4000 | 0xabc, // HIGHADJ at +0xabc, with an addend after it + 0x1234, // the addend, which is not an entry + 0x3000 | 0x020, // HIGHLOW at +0x20 + 0x0000, // padding + ]; + for (i, e) in entries.iter().enumerate() { + let at = 8 + i * 2; + rdata[at..at + 2].copy_from_slice(&e.to_le_bytes()); + } + let data = synth(&rdata, &[(5, rdata_rva(0), 8 + 2 * 5)]); + let (_, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + let got: Vec<(u64, u8)> = win + .relocations + .iter() + .map(|r| (r.addr.get() - BASE, r.kind)) + .collect(); + assert_eq!( + got, + vec![ + (TEXT_RVA as u64 + 8, 10), + (TEXT_RVA as u64 + 0xabc, 4), + (TEXT_RVA as u64 + 0x20, 3), + ] + ); +} + +#[test] +fn a_directory_claiming_more_than_the_file_holds_reads_only_what_fits() { + let mut rdata = vec![0u8; RDATA_LEN]; + rdata[0x00..0x04].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x04..0x08].copy_from_slice(&le32(TEXT_RVA + 0x10)); + rdata[0x08..0x0c].copy_from_slice(&le32(0)); + // The size says a hundred megabytes of runtime functions. + let data = synth(&rdata, &[(3, rdata_rva(0), 100 << 20)]); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + assert_eq!(win.exceptions.functions.len(), 1); + assert!( + obj.warnings.iter().any(|w| w.contains("fit in the file")), + "{:?}", + obj.warnings + ); +} + +#[test] +fn a_guard_table_claiming_more_entries_than_the_file_holds_is_bounded() { + let mut rdata = vec![0u8; RDATA_LEN]; + // A 64-bit load config with only the guard table filled in. + let size = 0xc8u32; + rdata[0x00..0x04].copy_from_slice(&size.to_le_bytes()); + rdata[0x80..0x88].copy_from_slice(&le64(rdata_va(0x200))); + rdata[0x88..0x90].copy_from_slice(&le64(1 << 40)); + rdata[0x200..0x204].copy_from_slice(&le32(TEXT_RVA)); + rdata[0x204..0x208].copy_from_slice(&le32(TEXT_RVA + 8)); + let data = synth(&rdata, &[(10, rdata_rva(0), size)]); + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + let cfg = win.load_config.expect("no load config"); + assert_eq!(cfg.guard_cf_function_count, 1 << 40); + // 0x200 bytes left in the section, four per entry. + assert_eq!(cfg.guard_functions.len(), 2); + assert!(obj.warnings.iter().any(|w| w.contains("fit in the file"))); +} + +#[test] +fn an_image_view_translates_rvas_addresses_and_file_offsets() { + // The three translations every directory walk depends on, checked against + // the layout the builder above lays down rather than against themselves. + let data = tls_image(&[text_va(0x10)], true); + let (obj, _) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + let img = pe::Image::of(&data, &obj); + assert_eq!(img.addr_of(TEXT_RVA), Addr(text_va(0))); + assert_eq!(img.offset_of(TEXT_RVA), Some(TEXT_OFF as u64)); + assert_eq!(img.offset_of(RDATA_RVA), Some(RDATA_OFF as u64)); + // An image loaded where it asked to be stores absolute addresses as is. + assert_eq!(img.addr_of_va(rdata_va(0x20)), Some(Addr(rdata_va(0x20)))); + assert_eq!(img.addr_of_va(0), None); + assert!(img.is_code(Addr(text_va(0x10)))); + assert!(!img.is_code(Addr(rdata_va(0x10)))); + assert!(img.is_mapped(Addr(rdata_va(0x10)))); + assert!(!img.is_mapped(Addr(BASE + 0x0f00_0000))); + // The header is in no section, so a walk there falls back to the file. + assert_eq!(img.section_bytes_after(0), None); + assert_eq!( + img.section_bytes_after(RDATA_OFF as u64), + Some(RDATA_LEN as u64) + ); + assert_eq!(e5r_format::windirs::reloc_kind(10), "dir64"); +} + +#[test] +fn corrupted_directories_never_panic() { + // Every byte of the headers and of the directory payload, three ways. + let base = tls_image(&[text_va(0x10)], true); + let mut data = base.clone(); + for i in (0..RDATA_OFF + 0x40).step_by(5) { + let old = data[i]; + for v in [0xffu8, 0x00, 0x80] { + data[i] = v; + let _ = pe::load_windows(&data, &LoadOptions::default()); + } + data[i] = old; + } + let mut n = 1; + while n < base.len() { + let _ = pe::load_windows(&base[..n], &LoadOptions::default()); + n = (n * 2).max(n + 101); + } +} + +#[test] +fn caps_bound_what_the_directories_allocate() { + let opts = LoadOptions { + caps: Caps { + symbols: 4, + ..Caps::default() + }, + ..LoadOptions::default() + }; + let data = tls_image(&[text_va(0x10)], false); + let (_, win) = pe::load_windows(&data, &opts).unwrap(); + assert!(win.tls.unwrap().callbacks.len() <= 4); +} + +#[test] +fn a_coff_object_has_no_windows_directories() { + let Some(dir) = corpus() else { return }; + let Ok(data) = std::fs::read(dir.join("wide.coff.o")) else { + return; + }; + let (obj, win) = pe::load_windows(&data, &LoadOptions::default()).unwrap(); + assert_eq!(obj.arch, Arch::X86_64); + // An object has section-level .pdata but no data directories, so nothing + // here should claim otherwise. + assert!(win.tls.is_none()); + assert!(win.exceptions.functions.is_empty()); + assert!(win.relocations.is_empty()); + assert!(win.load_config.is_none()); +} diff --git a/crates/e5r-ir/Cargo.toml b/crates/e5r-ir/Cargo.toml new file mode 100644 index 0000000..08fafcb --- /dev/null +++ b/crates/e5r-ir/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "e5r-ir" +description = "e5r — the intermediate representation, its lifters and its interpreter." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true + +[dependencies] +e5r-core = { workspace = true } +e5r-arch = { workspace = true } +e5r-types = { workspace = true } +serde = { workspace = true } + +[dev-dependencies] +e5r-format = { workspace = true } +e5r-analysis = { workspace = true } +e5r-arch = { workspace = true } diff --git a/crates/e5r-ir/src/abi.rs b/crates/e5r-ir/src/abi.rs new file mode 100644 index 0000000..2c5b5a1 --- /dev/null +++ b/crates/e5r-ir/src/abi.rs @@ -0,0 +1,393 @@ +//! What the calling convention says, per architecture. +//! +//! Two passes need this and neither can work without it. Dead code elimination +//! has to know which registers the caller may read after a return, or it +//! deletes the result. The decompiler has to know which registers arrive with +//! values, or every function takes no arguments. +//! +//! This is the default convention, not a recovered one. A function that does +//! something else is [`crate::conv`]'s per-function detection, which this is +//! the floor for: detection is a comparison, and this is the thing compared +//! against. +//! +//! More than one convention exists per architecture. The same x86-64 code +//! passes its first argument in `rdi` under System V and in `rcx` under the +//! Microsoft convention, so the container decides as much as the machine does +//! and [`of_named`] takes the convention as an argument. [`of`] keeps the +//! platform default for an architecture, which is what every existing caller +//! wants. + +use e5r_core::Arch; + +use crate::lift::{aarch64, arm, x86}; +use crate::op::Space; +use crate::ssa::Location; + +/// A calling convention with a name, as opposed to one a compiler invented. +/// +/// Naming them is what lets detection say "this is `__fastcall`" rather than +/// "this is not the default": a non-standard convention that happens to be +/// another platform's standard one is a different finding from a convention +/// with no name at all. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Named { + /// System V AMD64, the ELF and Mach-O default on x86-64. + SysV, + /// AAPCS64, the default on AArch64 everywhere including Windows. + Aapcs64, + /// AAPCS32, the default on 32-bit ARM: four integer registers and the + /// stack, with the return address in the link register. + Aapcs32, + /// The Microsoft x64 convention: four integer registers, four vector + /// ones, and thirty-two bytes of shadow space the caller reserves. + Win64, + /// 32-bit x86 `cdecl`: everything on the stack, the caller pops. + Cdecl, + /// 32-bit x86 `__stdcall`: everything on the stack, the callee pops. + Stdcall, + /// 32-bit x86 `__fastcall`: `ecx` and `edx`, then the stack, callee pops. + Fastcall, + /// 32-bit x86 `__thiscall`: the object in `ecx`, the rest on the stack, + /// callee pops. + Thiscall, + /// 32-bit x86 `__vectorcall`: `__fastcall` plus `xmm0` through `xmm5`. + Vectorcall, +} + +impl Named { + /// The word used in output and in JSON. + pub fn as_str(self) -> &'static str { + match self { + Named::SysV => "sysv", + Named::Aapcs64 => "aapcs64", + Named::Aapcs32 => "aapcs32", + Named::Win64 => "win64", + Named::Cdecl => "cdecl", + Named::Stdcall => "stdcall", + Named::Fastcall => "fastcall", + Named::Thiscall => "thiscall", + Named::Vectorcall => "vectorcall", + } + } +} + +impl std::fmt::Display for Named { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// The registers a convention uses, as byte offsets into the register file. +#[derive(Debug, Clone)] +pub struct Abi { + /// Which convention this is. + pub name: Named, + /// Integer argument registers, in order. + pub integer_arguments: Vec, + /// Floating point argument registers, in order. + pub float_arguments: Vec, + /// Registers holding the result. + pub results: Vec, + /// Registers the callee must restore, so the caller may read them after. + pub callee_saved: Vec, + /// Registers a call may leave holding anything. + pub caller_saved: Vec, + /// The stack pointer. + pub stack_pointer: u64, + /// Where the vector registers start, for naming. + pub vector_base: u64, + /// Offset from the entry stack pointer of the first argument passed there. + /// + /// x86 pushes the return address as part of the call, so its stack + /// arguments start one slot past the entry pointer; AArch64 puts the + /// return address in a register and starts at zero. The Microsoft x64 + /// convention adds thirty-two bytes of shadow space on top of that. + pub stack_argument_base: i64, + /// Bytes the call itself pushes, which the callee has to step over on the + /// way out. The return address on x86, nothing on AArch64. + pub return_address_bytes: u64, + /// True when the callee removes the stack arguments before returning, + /// which on x86 is visible as a `ret imm16`. + pub callee_pops: bool, + /// The register a member function receives the object in, when the + /// convention gives it one of its own rather than making it the first + /// ordinary argument. + pub this_register: Option, + /// The register the caller puts the address of a memory-returned result + /// in, when it is not simply the first argument register. + pub indirect_result: Option, + /// The register a variadic call reports its vector register count in. + /// System V uses `al`; nothing else here does. + pub varargs_count: Option, +} + +impl Abi { + /// The locations live when a function returns: the result and everything + /// the caller is entitled to find unchanged. + pub fn live_at_return(&self) -> Vec { + let mut out: Vec = self + .results + .iter() + .chain(self.callee_saved.iter()) + .map(|offset| Location { + space: Space::Register, + offset: *offset, + size: 8, + }) + .collect(); + out.push(Location { + space: Space::Register, + offset: self.stack_pointer, + size: 8, + }); + out + } + + /// The name an incoming value gets, when it arrives in a register the + /// convention passes arguments in. + pub fn argument_name(&self, offset: u64) -> Option { + if let Some(n) = self.integer_arguments.iter().position(|o| *o == offset) { + return Some(format!("arg{n}")); + } + if let Some(n) = self.float_arguments.iter().position(|o| *o == offset) { + return Some(format!("farg{n}")); + } + None + } + + /// Which argument slot a register is, when it is one. + pub fn slot(&self, offset: u64) -> Option { + if let Some(n) = self.integer_arguments.iter().position(|o| *o == offset) { + return Some(Slot::Integer(n)); + } + if let Some(n) = self.float_arguments.iter().position(|o| *o == offset) { + return Some(Slot::Float(n)); + } + None + } + + /// True when the convention passes an argument in this register. + pub fn passes_arguments_in(&self, offset: u64) -> bool { + self.slot(offset).is_some() + } + + /// True when the convention lets a caller read this register back + /// unchanged. + pub fn preserves(&self, offset: u64) -> bool { + self.callee_saved.contains(&offset) || offset == self.stack_pointer + } +} + +/// Which argument register of a convention a register is. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Slot { + /// The nth integer argument register. + Integer(usize), + /// The nth floating point one. + Float(usize), +} + +/// The default convention for an architecture. +/// +/// The ELF and Mach-O answer. A PE loader wants [`Named::Win64`] on x86-64 and +/// says so through [`of_named`]. +pub fn of(arch: &Arch) -> Abi { + match arch { + Arch::X86_64 => of_named(arch, Named::SysV), + Arch::X86 => of_named(arch, Named::Cdecl), + Arch::Arm | Arch::Thumb => of_named(arch, Named::Aapcs32), + _ => of_named(arch, Named::Aapcs64), + } +} + +/// The default convention for an architecture under a container. +/// +/// The same instruction set has a different default depending on who linked +/// it, and the machine code cannot say which. This is the one place that +/// decision is written down. +pub fn of_container(arch: &Arch, windows: bool) -> Abi { + match (arch, windows) { + (Arch::X86_64, true) => of_named(arch, Named::Win64), + _ => of(arch), + } +} + +/// A named convention's registers. +pub fn of_named(arch: &Arch, name: Named) -> Abi { + match name { + Named::Win64 => win64(), + Named::SysV if *arch == Arch::X86_64 => sysv64(), + Named::Aapcs32 => aapcs32(), + Named::Cdecl | Named::Stdcall | Named::Fastcall | Named::Thiscall | Named::Vectorcall => { + x86_32(name) + } + // Everything else is the 64-bit ARM convention, which is also what an + // architecture with no model of its own falls back to. + _ => aapcs64(), + } +} + +fn sysv64() -> Abi { + Abi { + name: Named::SysV, + // System V: rdi, rsi, rdx, rcx, r8, r9. + integer_arguments: [7u8, 6, 2, 1, 8, 9] + .iter() + .map(|n| x86::gpr_offset(*n)) + .collect(), + float_arguments: (0..8).map(x86::vec_offset).collect(), + results: vec![x86::gpr_offset(0), x86::gpr_offset(2), x86::vec_offset(0)], + // rbx, rbp, r12 through r15. + callee_saved: [3u8, 5, 12, 13, 14, 15] + .iter() + .map(|n| x86::gpr_offset(*n)) + .collect(), + // rax, rcx, rdx, rsi, rdi and r8 through r11. + caller_saved: [0u8, 1, 2, 6, 7, 8, 9, 10, 11] + .iter() + .map(|n| x86::gpr_offset(*n)) + .collect(), + stack_pointer: x86::sp_offset(), + vector_base: x86::vec_offset(0), + stack_argument_base: 8, + return_address_bytes: 8, + callee_pops: false, + this_register: None, + indirect_result: None, + // `al` carries the vector register count of a variadic call, so a read + // of rax at entry is the convention and not a custom argument. + varargs_count: Some(x86::gpr_offset(0)), + } +} + +fn win64() -> Abi { + Abi { + name: Named::Win64, + // rcx, rdx, r8, r9, and no more: the fifth argument is on the stack. + integer_arguments: [1u8, 2, 8, 9].iter().map(|n| x86::gpr_offset(*n)).collect(), + float_arguments: (0..4).map(x86::vec_offset).collect(), + results: vec![x86::gpr_offset(0), x86::vec_offset(0)], + // rbx, rbp, rsi, rdi, r12 through r15. The vector half of the list is + // xmm6 upwards, which matters to dead code elimination across a call. + callee_saved: [3u8, 5, 6, 7, 12, 13, 14, 15] + .iter() + .map(|n| x86::gpr_offset(*n)) + .chain((6..16).map(x86::vec_offset)) + .collect(), + caller_saved: [0u8, 1, 2, 8, 9, 10, 11] + .iter() + .map(|n| x86::gpr_offset(*n)) + .chain((0..6).map(x86::vec_offset)) + .collect(), + stack_pointer: x86::sp_offset(), + vector_base: x86::vec_offset(0), + // The return address, then thirty-two bytes of shadow space the caller + // reserves for the four register arguments. The fifth argument is the + // first one actually passed on the stack. + stack_argument_base: 40, + return_address_bytes: 8, + callee_pops: false, + this_register: None, + indirect_result: None, + varargs_count: None, + } +} + +/// The 32-bit x86 conventions, which differ from each other only in how many +/// registers they use and who pops. +/// AAPCS32, the 32-bit ARM convention. +/// +/// Four integer registers and then the stack, the result in r0 and r1, and the +/// return address in the link register rather than on the stack -- the same +/// shape as AAPCS64 with half the registers. +fn aapcs32() -> Abi { + Abi { + name: Named::Aapcs32, + integer_arguments: (0..4).map(arm::gpr_offset).collect(), + // Floating point arguments go in the VFP registers under the hard + // float variant and in the integer ones under the soft float variant. + // The hard float set is named here; a soft float call still resolves, + // because its arguments are in the integer registers above. + float_arguments: (0..8).map(arm::vec_offset).collect(), + results: vec![arm::gpr_offset(0), arm::gpr_offset(1), arm::vec_offset(0)], + // r4 through r11. + callee_saved: (4..=11).map(arm::gpr_offset).collect(), + // r0 through r3, and r12, which is the intra-procedure scratch. + caller_saved: (0..=3) + .chain(std::iter::once(12)) + .map(arm::gpr_offset) + .collect(), + stack_pointer: arm::sp_offset(), + vector_base: arm::vec_offset(0), + stack_argument_base: 0, + // The call leaves the return address in the link register. + return_address_bytes: 0, + callee_pops: false, + this_register: None, + // A result too large for the registers is written through a pointer + // the caller passes in r0, which shifts every declared argument along. + indirect_result: Some(arm::gpr_offset(0)), + varargs_count: None, + } +} + +fn x86_32(name: Named) -> Abi { + let integer_arguments: Vec = match name { + // ecx, edx. + Named::Fastcall | Named::Vectorcall => vec![x86::gpr_offset(1), x86::gpr_offset(2)], + // The object, and nothing else. + Named::Thiscall => vec![x86::gpr_offset(1)], + _ => Vec::new(), + }; + let float_arguments: Vec = match name { + Named::Vectorcall => (0..6).map(x86::vec_offset).collect(), + _ => Vec::new(), + }; + Abi { + name, + integer_arguments, + float_arguments, + // eax, then edx for the upper half of a 64-bit value. + results: vec![x86::gpr_offset(0), x86::gpr_offset(2), x86::vec_offset(0)], + // ebx, ebp, esi, edi. + callee_saved: [3u8, 5, 6, 7].iter().map(|n| x86::gpr_offset(*n)).collect(), + // eax, ecx, edx. + caller_saved: [0u8, 1, 2].iter().map(|n| x86::gpr_offset(*n)).collect(), + stack_pointer: x86::sp_offset(), + vector_base: x86::vec_offset(0), + stack_argument_base: 4, + return_address_bytes: 4, + callee_pops: name != Named::Cdecl, + this_register: (name == Named::Thiscall).then(|| x86::gpr_offset(1)), + indirect_result: None, + varargs_count: None, + } +} + +fn aapcs64() -> Abi { + Abi { + name: Named::Aapcs64, + integer_arguments: (0..8).map(aarch64::gpr_offset).collect(), + float_arguments: (0..8).map(aarch64::vec_offset).collect(), + results: vec![ + aarch64::gpr_offset(0), + aarch64::gpr_offset(1), + aarch64::vec_offset(0), + ], + // x19 through x28, and the frame pointer. + callee_saved: (19..=29).map(aarch64::gpr_offset).collect(), + // x0 through x18, which a callee may use for anything. + caller_saved: (0..=18).map(aarch64::gpr_offset).collect(), + stack_pointer: aarch64::sp_offset(), + vector_base: aarch64::vec_offset(0), + stack_argument_base: 0, + // The call leaves the return address in x30, not on the stack. + return_address_bytes: 0, + callee_pops: false, + this_register: None, + // x8 carries the address of a result returned in memory, so a read of + // it at entry is the convention rather than a custom argument. + indirect_result: Some(aarch64::gpr_offset(8)), + varargs_count: None, + } +} diff --git a/crates/e5r-ir/src/conv.rs b/crates/e5r-ir/src/conv.rs new file mode 100644 index 0000000..4c89f02 --- /dev/null +++ b/crates/e5r-ir/src/conv.rs @@ -0,0 +1,1130 @@ +//! The convention a function actually uses, rather than the one it was +//! assumed to use. +//! +//! [`crate::abi`] says what the platform declares. That is the right default +//! and the wrong answer for a large class of real functions: a `static` a +//! compiler gave a convention of its own, passing in whatever registers were +//! free; a function that takes nothing where the fixed order hands it six +//! arguments; a `__stdcall` that pops the caller's arguments itself. Assuming +//! the default there does not leave a gap, it produces confident nonsense, +//! which is worse. +//! +//! So the convention is observed instead: +//! +//! * an argument is a register read before this function writes it, whether or +//! not the default passes arguments there. A read of a register the default +//! passes nothing in is evidence of a convention the compiler invented, not +//! something to drop; +//! * a result is a register the callers read after the call. A function whose +//! result nobody reads probably has none, and `void` beats a fabricated +//! `uint64_t`; +//! * the callee popping the caller's arguments is visible in the stack pointer +//! at the return, which is where `ret imm16` shows up. +//! +//! What comes out is classified against the default and, when it contradicts +//! it, said so: a [`Departure`] is never normalised away. Where the evidence is +//! thin the answer is [`Convention::Unknown`] rather than a guess. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +use e5r_core::provenance::Strength; +use e5r_core::{Addr, Arch}; + +use crate::abi::{Abi, Named, Slot}; +use crate::op::{Op, Space}; +use crate::ssa::{Location, Operand, SsaFunction, SsaKind, SsaOp, Value}; + +/// Where a value was defined, which several of these walks need and none of +/// them should rebuild. +pub(crate) type Definitions = BTreeMap; + +/// How a function's convention compares with the platform default. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Convention { + /// The evidence is too thin to say. A function the lifter did not model + /// completely reads registers it would have written, so its entry reads + /// are not arguments and calling them arguments would be an invention. + #[default] + Unknown, + /// The platform default, using every argument the default passes: nothing + /// is known to be missing. + Standard, + /// The platform default, with fewer arguments than the fixed order would + /// give it. + /// + /// The registers it reads leave a gap: it reads the fourth integer + /// argument register and not the first three, so the count taken from the + /// highest slot used is three arguments too high. The gap is the evidence + /// that the fixed order is not what the caller filled, and it is the case + /// that puts phantom parameters in a decompilation. + Fewer, + /// Not the platform default. [`Detected::departures`] says how. + NonStandard, +} + +impl Convention { + /// The word used in output and in JSON. + pub fn as_str(self) -> &'static str { + match self { + Convention::Unknown => "unknown", + Convention::Standard => "standard", + Convention::Fewer => "standard-fewer-arguments", + Convention::NonStandard => "non-standard", + } + } + + /// True when nothing about the function contradicts the default. + pub fn is_standard(self) -> bool { + matches!(self, Convention::Standard | Convention::Fewer) + } +} + +impl fmt::Display for Convention { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// What the default makes of one incoming value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Purpose { + /// The nth integer argument register of the default. + Integer(usize), + /// The nth floating point one. + Float(usize), + /// A slot at this offset from the entry stack pointer. + Stack(i64), + /// The address the caller wants a memory-returned result written to, + /// which AAPCS64 passes in `x8`. + IndirectResult, + /// How many vector registers a variadic call filled, which System V passes + /// in `al`. + VarargsCount, + /// A register the default passes nothing in. + Custom, +} + +impl Purpose { + /// True when this is an argument the caller passed, as opposed to a + /// housekeeping register the convention reserves. + pub fn is_argument(self) -> bool { + !matches!(self, Purpose::IndirectResult | Purpose::VarargsCount) + } + + /// Sort key: the default's own order first, then what it has no slot for. + fn order(self) -> (u8, i64) { + match self { + Purpose::Integer(n) => (0, n as i64), + Purpose::Float(n) => (1, n as i64), + Purpose::Stack(o) => (2, o), + Purpose::IndirectResult => (3, 0), + Purpose::VarargsCount => (4, 0), + Purpose::Custom => (5, 0), + } + } +} + +/// One value that arrives with the function. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Incoming { + /// Where it arrives. + pub location: Location, + /// What the default makes of that place. + pub purpose: Purpose, +} + +/// One way a function contradicts the platform default. +/// +/// The interesting case, and what this module exists for. Every one of these +/// is reported; none is silently normalised into the default. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Departure { + /// An argument arrives in a register the default passes nothing in. + Argument { + /// Its register file offset. + register: u64, + }, + /// The callee removes the caller's arguments from the stack, which on x86 + /// is a `ret imm16`. + CalleePops { + /// How many bytes. + bytes: u64, + }, + /// A register the default promises to give back unchanged is left changed. + Clobbers { + /// Its register file offset. + register: u64, + }, + /// A caller reads a result out of a register the default does not return + /// values in. + Result { + /// Its register file offset. + register: u64, + }, + /// Nothing in the function returns to its caller, so the default's result + /// register says nothing about it. A function that never comes back, or + /// one whose only exit is a tail call. + NeverReturns, +} + +impl fmt::Display for Departure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Departure::Argument { register } => write!( + f, + "an argument arrives in {register:#x}, which the convention passes nothing in" + ), + Departure::CalleePops { bytes } => { + write!(f, "the callee pops {bytes} bytes of arguments") + } + Departure::Clobbers { register } => write!( + f, + "{register:#x} is callee-saved and this function does not put it back" + ), + Departure::Result { register } => write!( + f, + "a caller reads the result from {register:#x}, which is not a result register" + ), + Departure::NeverReturns => write!(f, "nothing returns to the caller"), + } + } +} + +/// The convention one function was found to use. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Detected { + /// How it compares with the platform default. + pub convention: Convention, + /// A convention with a name that this matches, when one does. A function + /// that departs from the default but matches another platform's standard + /// convention is a different finding from one with no name at all. + pub named: Option, + /// How strongly the classification is known. + pub strength: Strength, + /// How many call sites corroborated it. + /// + /// A convention confirmed at several call sites is better evidence than + /// one read off a single function's entry reads, and [`Strength`] has no + /// rung between them, so the corroboration is counted rather than folded + /// into the rung. + pub support: usize, + /// Everything that arrives with a value, in the default's order and then + /// the places it has no slot for. + pub arguments: Vec, + /// Where the result is left, when there is one. + pub result: Option, + /// True when the result register is a vector one. + pub result_float: bool, + /// Bytes of the caller's arguments the function removes before returning. + pub callee_pops: u64, + /// The register the object arrives in, when the function looks like a + /// member function. + /// + /// Always [`Strength::Heuristic`]: under every convention modelled here + /// but `__thiscall` the object is an ordinary first argument, so this + /// names a shape rather than a departure from anything. + pub this_register: Option, + /// False when nothing in the function returns to its caller. + pub returns_to_caller: bool, + /// Every way this contradicts the default. + pub departures: Vec, +} + +impl Default for Detected { + fn default() -> Detected { + Detected { + convention: Convention::Unknown, + named: None, + // Nothing has been looked at, so nothing is known. `Strength` has + // no `Default` on purpose and this is the honest floor. + strength: Strength::Heuristic, + support: 0, + arguments: Vec::new(), + result: None, + result_float: false, + callee_pops: 0, + this_register: None, + returns_to_caller: false, + departures: Vec::new(), + } + } +} + +impl Detected { + /// How many arguments the caller passes, housekeeping registers aside. + pub fn arity(&self) -> usize { + self.arguments + .iter() + .filter(|a| a.purpose.is_argument()) + .count() + } + + /// How many arguments the fixed order alone would give this function: one + /// past the highest slot it touches, whether or not the slots below were + /// ever read. The number [`Convention::Fewer`] says is too high. + pub fn assumed_arity(&self) -> usize { + let slot = |pick: fn(Purpose) -> Option| { + self.arguments + .iter() + .filter_map(|a| pick(a.purpose)) + .map(|n| n + 1) + .max() + .unwrap_or(0) + }; + slot(|p| match p { + Purpose::Integer(n) => Some(n), + _ => None, + }) + slot(|p| match p { + Purpose::Float(n) => Some(n), + _ => None, + }) + self + .arguments + .iter() + .filter(|a| matches!(a.purpose, Purpose::Stack(_))) + .count() + } + + /// The registers the default passes nothing in and this function reads. + pub fn custom_registers(&self) -> Vec { + self.arguments + .iter() + .filter(|a| a.purpose == Purpose::Custom) + .map(|a| a.location.offset) + .collect() + } + + /// Rank two detections: the stronger evidence first, then the better + /// corroborated. + pub fn rank(&self, other: &Detected) -> std::cmp::Ordering { + other + .strength + .cmp(&self.strength) + .then_with(|| other.support.cmp(&self.support)) + } + + /// A one-line description, for output that has to say why. + pub fn describe(&self) -> String { + let mut out = format!("{} ({}", self.convention, self.strength); + if self.support > 0 { + out.push_str(&format!(", {} call site(s)", self.support)); + } + out.push(')'); + for d in &self.departures { + out.push_str(&format!("; {d}")); + } + out + } +} + +/// What the callers of one function said about it. +/// +/// Built by [`observe`] over each caller and merged per callee. Everything in +/// here is evidence the function itself cannot produce: whether anyone reads +/// the result, and which registers the callers actually fill. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Observed { + /// How many calls were seen. + pub sites: usize, + /// How many of them read what the function left in a result register. + pub result_read: usize, + /// Register offsets a caller set before the call, and at how many sites. + pub set: BTreeMap, + /// Register offsets a caller read back after the call, and at how many + /// sites. A register a caller reads back is one the function returned + /// something in. + pub read_after: BTreeMap, + /// What the compiler recorded about the arguments, one entry per call site + /// it described, as register file offsets. + /// + /// Ground truth: a producer emitting `DW_TAG_call_site_parameter` is + /// saying where it put each argument. Translate the DWARF register numbers + /// with [`crate::dwreg::offset`] and put them here, and the detection is + /// [`Strength::Proven`] rather than inferred. + pub recorded: Vec>, +} + +impl Observed { + /// Fold another observation of the same function into this one. + pub fn merge(&mut self, other: &Observed) { + self.sites += other.sites; + self.result_read += other.result_read; + for (k, v) in &other.set { + *self.set.entry(*k).or_default() += v; + } + for (k, v) in &other.read_after { + *self.read_after.entry(*k).or_default() += v; + } + self.recorded.extend(other.recorded.iter().cloned()); + } + + /// The registers the compiler said hold arguments, over every call site it + /// described. + pub fn recorded_registers(&self) -> BTreeSet { + self.recorded.iter().flatten().copied().collect() + } + + /// The most arguments the compiler recorded at any one call site, which is + /// the arity it compiled against. + pub fn recorded_arity(&self) -> Option { + self.recorded.iter().map(|r| r.len()).max() + } + + /// True when nothing was seen at all. + pub fn is_empty(&self) -> bool { + self.sites == 0 && self.recorded.is_empty() + } +} + +/// Detect one function's convention from the function alone. +pub fn detect(f: &SsaFunction, abi: &Abi) -> Detected { + detect_with(f, abi, &Observed::default()) +} + +/// Detect one function's convention, with what its callers said. +pub fn detect_with(f: &SsaFunction, abi: &Abi, observed: &Observed) -> Detected { + detect_from(f, abi, &facts(f), &f.definitions(), observed) +} + +/// What one pass over a function establishes, so recovery and detection share +/// the walk rather than disagreeing about it. +pub(crate) struct Facts { + /// Locations read before this function wrote them. + pub live_in: BTreeSet, + /// Register offsets this function writes. + pub written: BTreeSet, + /// True when every instruction was modelled. + pub complete: bool, + /// True when some block returns to the caller. + pub returns_to_caller: bool, +} + +/// Walk the function once. +pub(crate) fn facts(f: &SsaFunction) -> Facts { + let mut live_in = BTreeSet::new(); + let mut written = BTreeSet::new(); + let mut complete = true; + let mut returns_to_caller = false; + for b in f.blocks.values() { + for op in &b.ops { + for i in &op.inputs { + if let Operand::Undefined(l) = i { + live_in.insert(*l); + } + } + match op.kind { + SsaKind::Op(Op::Unimplemented) => complete = false, + SsaKind::Op(Op::Return) => returns_to_caller = true, + // An undefined value is not a write: it says the location + // holds something this function did not compute. + SsaKind::Op(Op::Undefine) => continue, + _ => {} + } + if let Some(v) = op.out { + if v.location.space == Space::Register { + written.insert(v.location.offset); + } + } + } + } + Facts { + live_in, + written, + complete, + returns_to_caller, + } +} + +/// The detection proper, on facts already gathered. +pub(crate) fn detect_from( + f: &SsaFunction, + abi: &Abi, + facts: &Facts, + definitions: &Definitions, + observed: &Observed, +) -> Detected { + let arguments = incoming(f, abi, facts, definitions); + let unsaved = clobbered(f, abi, &facts.written, definitions); + let pops = callee_pops(f, abi, definitions); + + let mut departures: Vec = Vec::new(); + for a in &arguments { + if a.purpose == Purpose::Custom { + departures.push(Departure::Argument { + register: register_base(abi, a.location.offset), + }); + } + } + for register in &unsaved { + departures.push(Departure::Clobbers { + register: *register, + }); + } + if pops > 0 { + departures.push(Departure::CalleePops { bytes: pops }); + } + if !facts.returns_to_caller && !f.blocks.is_empty() { + departures.push(Departure::NeverReturns); + } + // A register a caller reads back that the default does not return values + // in is a result the default cannot name. A register the default passes + // arguments in is excluded: a value sitting in one after a call is the + // caller setting up the next call far more often than it is a result, and + // a convention that returns in its own first argument register is rarer + // than that noise. + for (register, sites) in &observed.read_after { + if *sites > 0 + && !abi.results.contains(register) + && !abi.passes_arguments_in(*register) + && facts.written.contains(register) + { + departures.push(Departure::Result { + register: *register, + }); + } + } + departures.sort(); + departures.dedup(); + + let custom = arguments.iter().any(|a| a.purpose == Purpose::Custom); + let convention = if !facts.complete || f.blocks.is_empty() { + // A function the lifter did not model reads registers it would have + // written, so its entry reads are not evidence of anything. + Convention::Unknown + } else if departures.iter().any(|d| *d != Departure::NeverReturns) { + Convention::NonStandard + } else if contiguous(&arguments) { + Convention::Standard + } else { + Convention::Fewer + }; + + let (strength, support) = confidence(convention, custom, &arguments, observed); + let result = result(f, abi, facts, definitions, observed); + + Detected { + convention, + named: named(abi, convention, custom, pops), + strength, + support, + result, + result_float: result.is_some_and(|r| r >= abi.vector_base), + callee_pops: pops, + this_register: object_register(f, abi, facts), + returns_to_caller: facts.returns_to_caller, + arguments, + departures, + } +} + +/// True when the argument slots used are the ones the fixed order predicts: +/// the first n integer registers and the first m floating point ones, with no +/// gaps. +/// +/// A gap means the function takes fewer arguments than its highest slot says, +/// which is the difference between a prototype and a prototype with phantom +/// parameters in it. +fn contiguous(arguments: &[Incoming]) -> bool { + let mut integers = 0usize; + let mut floats = 0usize; + for a in arguments { + match a.purpose { + Purpose::Integer(n) if n == integers => integers += 1, + Purpose::Integer(_) => return false, + Purpose::Float(n) if n == floats => floats += 1, + Purpose::Float(_) => return false, + _ => {} + } + } + true +} + +/// Everything that arrives with a value, classified against the default. +fn incoming(f: &SsaFunction, abi: &Abi, facts: &Facts, definitions: &Definitions) -> Vec { + let mut out: Vec = Vec::new(); + for l in &facts.live_in { + let purpose = match l.space { + // A slot at or above where the convention starts passing arguments + // was filled by the caller. Below that is the return address on + // x86 and the shadow space on the Microsoft convention, neither of + // which is an argument. + Space::Stack => { + let offset = l.offset as i64; + if offset < abi.stack_argument_base { + continue; + } + Purpose::Stack(offset) + } + Space::Register => { + // The flags are a byte each and arrive holding nothing anyone + // passed. + if l.size == 1 || housekeeping(&f.arch, abi, l.offset) { + continue; + } + let base = register_base(abi, l.offset); + if Some(base) == abi.indirect_result { + Purpose::IndirectResult + } else if Some(base) == abi.varargs_count { + Purpose::VarargsCount + } else { + match abi.slot(base) { + Some(Slot::Integer(n)) => Purpose::Integer(n), + Some(Slot::Float(n)) => Purpose::Float(n), + // A callee-saved register read at entry is the + // prologue saving it, not an argument: either the + // value goes straight to the frame and nowhere else, + // or it is back in place at the return. A register the + // function reads and then computes with, and never + // puts back, is one the compiler took for itself, + // which is the custom convention this looks for. + None if abi.callee_saved.contains(&base) + && (only_spilled(f, *l) || restored(f, definitions, l.offset)) => + { + continue; + } + None => Purpose::Custom, + } + } + } + // A temporary read before it was written is a lifting artifact, + // and memory is not somewhere the convention passes anything. + Space::Unique | Space::Ram | Space::Const => continue, + }; + out.push(Incoming { + location: *l, + purpose, + }); + } + out.sort_by_key(|i| (i.purpose.order(), i.location.offset)); + out +} + +/// The register an offset is part of. +/// +/// A vector register is sixteen bytes and SSA versions eight-byte locations, +/// so the upper half of `xmm6` is an offset of its own. Left alone it looks +/// like a register no convention names, and a Windows prologue saving +/// `xmm6` through `xmm15` would read as ten arguments the compiler invented. +fn register_base(abi: &Abi, offset: u64) -> u64 { + if offset < abi.vector_base { + return offset; + } + abi.vector_base + (offset - abi.vector_base) / 16 * 16 +} + +/// Registers that are the machine's business rather than the caller's: the +/// stack pointer, the program counter, and the link register that arrives +/// holding the return address. +fn housekeeping(arch: &Arch, abi: &Abi, offset: u64) -> bool { + if offset == abi.stack_pointer { + return true; + } + match arch { + Arch::X86_64 | Arch::X86 => offset == crate::lift::x86::pc_offset(), + Arch::Arm | Arch::Thumb => offset == crate::lift::arm::pc_offset(), + // x30 arrives with the return address under every AArch64 convention. + Arch::AArch64 => offset == crate::lift::aarch64::gpr_offset(30), + _ => false, + } +} + +/// Callee-saved registers this function writes and does not put back. +pub(crate) fn clobbered( + f: &SsaFunction, + abi: &Abi, + written: &BTreeSet, + definitions: &Definitions, +) -> Vec { + abi.callee_saved + .iter() + .copied() + .filter(|offset| written.contains(offset) && !restored(f, definitions, *offset)) + .collect() +} + +/// Where the result is left, once the callers have had their say. +fn result( + f: &SsaFunction, + abi: &Abi, + facts: &Facts, + definitions: &Definitions, + observed: &Observed, +) -> Option { + // A register a caller reads back is a result whatever else is true, and it + // is the only direct evidence there is. + for register in abi.results.iter() { + if observed.read_after.get(register).copied().unwrap_or(0) > 0 { + return Some(*register); + } + } + // Every caller ignored what it left, so it left nothing: `void` beats a + // fabricated result. A function nothing calls keeps its own answer, + // because absence of evidence is not evidence. + if observed.sites > 0 && observed.result_read == 0 { + return None; + } + crate::proto::returned(f, abi, &facts.written, definitions) +} + +/// How many bytes of the caller's arguments the function pops on its way out. +/// +/// The stack pointer at the return, minus what it started at, minus whatever +/// the call itself pushed. On x86 that is `ret imm16`; on AArch64 the stack +/// pointer comes back where it started and this is zero. +fn callee_pops(f: &SsaFunction, abi: &Abi, definitions: &Definitions) -> u64 { + let location = Location { + space: Space::Register, + offset: abi.stack_pointer, + size: 8, + }; + let mut delta: Option = None; + for b in f.blocks.values() { + let Some(at) = b + .ops + .iter() + .position(|op| op.kind == SsaKind::Op(Op::Return)) + else { + continue; + }; + // What the stack pointer holds when the return runs: the last thing in + // this block that wrote it. A block that writes it nowhere leaves the + // question to a predecessor, and an unproven pop is not claimed. + let Some(value) = b.ops[..at] + .iter() + .rev() + .filter_map(|op| op.out) + .find(|v| v.location == location) + else { + return 0; + }; + let Some(found) = entry_offset(f, definitions, value, location, 0) else { + return 0; + }; + match delta { + // Returns that disagree describe two conventions, which is not a + // thing to average. + Some(seen) if seen != found => return 0, + _ => delta = Some(found), + } + } + let popped = delta.unwrap_or(0) - abi.return_address_bytes as i64; + popped.max(0) as u64 +} + +/// A value's offset from what a location held at entry, when it is a chain of +/// constant adjustments away from it. +fn entry_offset( + f: &SsaFunction, + definitions: &Definitions, + value: Value, + location: Location, + depth: u32, +) -> Option { + if depth > 16 { + return None; + } + let (block, index) = definitions.get(&value)?; + let op = f.blocks.get(block)?.ops.get(*index)?; + let step = |operand: &Operand| -> Option { + match operand { + Operand::Undefined(l) if *l == location => Some(0), + Operand::Value(v) => entry_offset(f, definitions, *v, location, depth + 1), + _ => None, + } + }; + match op.kind { + SsaKind::Op(Op::Copy) => step(op.inputs.first()?), + SsaKind::Op(Op::IntAdd) | SsaKind::Op(Op::IntSub) => { + let (a, b) = (op.inputs.first()?, op.inputs.get(1)?); + let subtract = op.kind == SsaKind::Op(Op::IntSub); + match (a.as_const(), b.as_const()) { + (None, Some(n)) if subtract => Some(step(a)? - sign_extend(n, op.size)), + (None, Some(n)) => Some(step(a)? + sign_extend(n, op.size)), + // Only the left operand can be the pointer in a subtraction. + (Some(n), None) if !subtract => Some(step(b)? + sign_extend(n, op.size)), + _ => None, + } + } + SsaKind::Phi => { + let mut found: Option = None; + for i in &op.inputs { + let n = step(i)?; + if found.is_some_and(|seen| seen != n) { + return None; + } + found = Some(n); + } + found + } + _ => None, + } +} + +fn sign_extend(value: u64, size: u8) -> i64 { + match size { + 0 | 8.. => value as i64, + n => { + let bits = n as u32 * 8; + ((value << (64 - bits)) as i64) >> (64 - bits) + } + } +} + +/// The register the object arrives in, when the function looks like a member +/// function: a pointer only ever dereferenced and never used as a number. +fn object_register(f: &SsaFunction, abi: &Abi, facts: &Facts) -> Option { + let candidate = abi + .this_register + .or_else(|| abi.integer_arguments.first().copied())?; + let location = Location { + space: Space::Register, + offset: candidate, + size: 8, + }; + if !facts.live_in.contains(&location) { + return None; + } + only_dereferenced(f, location).then_some(candidate) +} + +/// True when the value a location arrives with goes to the frame and nowhere +/// else, which is a prologue saving a register rather than an argument being +/// used. +fn only_spilled(f: &SsaFunction, location: Location) -> bool { + let mut spills = 0usize; + for b in f.blocks.values() { + for op in &b.ops { + for (n, i) in op.inputs.iter().enumerate() { + if !matches!(i, Operand::Undefined(l) if *l == location) { + continue; + } + match op.kind { + // `*sp = reg`, or the same after stack promotion turned the + // slot into a location of its own. + SsaKind::Op(Op::Store) if n == 1 => spills += 1, + SsaKind::Op(Op::Copy) + if op.out.is_some_and(|v| v.location.space == Space::Stack) => + { + spills += 1 + } + _ => return false, + } + } + } + } + spills > 0 +} + +/// True when an operation only moves a pointer a constant distance, so what it +/// produces is still the same object. +fn derivation(op: &SsaOp) -> bool { + match op.kind { + SsaKind::Op(Op::Copy) => true, + SsaKind::Op(Op::IntAdd) | SsaKind::Op(Op::IntSub) => { + op.inputs.iter().any(|i| i.as_const().is_some()) + } + _ => false, + } +} + +/// True when the value arriving in a location is used only as the address of a +/// memory access, directly or a constant away from one. +fn only_dereferenced(f: &SsaFunction, location: Location) -> bool { + let ours = |operand: &Operand, derived: &BTreeSet| match operand { + Operand::Undefined(l) => *l == location, + Operand::Value(v) => derived.contains(v), + Operand::Const(..) => false, + }; + + // Grow the set of values that are the object, or a constant away from it. + // Three sweeps: an address computed in one block and used in another needs + // the set complete before the uses are judged, and a compiler's field + // access is one or two steps deep. + let mut derived: BTreeSet = BTreeSet::new(); + for _ in 0..3 { + for b in f.blocks.values() { + for op in &b.ops { + if !derivation(op) { + continue; + } + if op.inputs.iter().any(|i| ours(i, &derived)) { + if let Some(v) = op.out { + derived.insert(v); + } + } + } + } + } + + let mut loads = 0usize; + for b in f.blocks.values() { + for op in &b.ops { + for (n, i) in op.inputs.iter().enumerate() { + if !ours(i, &derived) { + continue; + } + match op.kind { + SsaKind::Op(Op::Load) if n == 0 => loads += 1, + SsaKind::Op(Op::Store) if n == 0 => {} + // Anything else treats the object as a value, which a + // `this` pointer is not. + _ if derivation(op) => {} + _ => return false, + } + } + } + } + loads > 0 +} + +/// True when a register's value at every return is the one it arrived with. +pub(crate) fn restored(f: &SsaFunction, definitions: &Definitions, offset: u64) -> bool { + let location = Location { + space: Space::Register, + offset, + size: 8, + }; + for b in f.blocks.values() { + if !b.ops.iter().any(|op| op.kind == SsaKind::Op(Op::Return)) { + continue; + } + // What the register holds at the return: the last definition in this + // block, or whatever reached it. + let last = b + .ops + .iter() + .rev() + .filter_map(|op| op.out) + .find(|v| v.location == location); + match last { + // Nothing in the returning block wrote it, so it still holds + // whatever the entry left there. + None => continue, + Some(v) => { + if !crate::proto::is_entry_value(f, definitions, v, location, 0) { + return false; + } + } + } + } + true +} + +/// Which named convention the evidence matches, when one does. +fn named(abi: &Abi, convention: Convention, custom: bool, pops: u64) -> Option { + match convention { + Convention::Unknown => None, + Convention::Standard | Convention::Fewer => Some(abi.name), + // A departure that is only the callee popping is another platform's + // standard convention rather than an invention, and on 32-bit x86 the + // pop is exactly what tells `__stdcall` from `cdecl`. + Convention::NonStandard if !custom && pops > 0 => match abi.name { + Named::Cdecl => Some(Named::Stdcall), + _ => None, + }, + Convention::NonStandard => None, + } +} + +/// How well the classification is known, and how much corroborated. +/// +/// The ladder: a function the lifter did not model, or one with nothing to go +/// on, is a guess. A function's own entry reads are a step past a proven +/// decode, which is what `Inferred` means. What the compiler recorded about +/// its own call sites is stated by the file in a structure the format defines, +/// which is `Proven`, and it is the strongest gate there is. +fn confidence( + convention: Convention, + custom: bool, + arguments: &[Incoming], + observed: &Observed, +) -> (Strength, usize) { + if convention == Convention::Unknown { + return (Strength::Heuristic, 0); + } + if !observed.recorded.is_empty() { + let recorded = observed.recorded_registers(); + let found: BTreeSet = arguments + .iter() + .filter(|a| a.location.space == Space::Register && a.purpose.is_argument()) + .map(|a| a.location.offset) + .collect(); + // The compiler named the registers it passed arguments in. Agreeing + // with it is as good as this gets; disagreeing means the reads are + // measuring something else and the claim drops back to a guess. + if recorded.is_subset(&found) { + return (Strength::Proven, observed.recorded.len()); + } + return (Strength::Heuristic, observed.recorded.len()); + } + // Call sites that fill every register this function reads corroborate what + // its own entry says. + let support = arguments + .iter() + .filter(|a| a.location.space == Space::Register && a.purpose.is_argument()) + .filter_map(|a| observed.set.get(&a.location.offset)) + .copied() + .min() + .unwrap_or(0); + // Callers that fill none of the registers a custom convention needs are + // evidence against it, not for it. + if custom && support == 0 && observed.sites > 0 { + return (Strength::Heuristic, 0); + } + (Strength::Inferred, support) +} + +/// What one caller's code says about the functions it calls. +/// +/// One observation per direct call target. A caller of a function in another +/// compilation unit says nothing about it here, which is correct: the evidence +/// is the code that is present. +pub fn observe(caller: &SsaFunction, abi: &Abi) -> BTreeMap { + let mut out: BTreeMap = BTreeMap::new(); + let used = consumed(caller, abi); + // A register a call clobbered counts as read back only when something + // actually reads it. The convention's own rule -- a value sitting in an + // argument register when the next call runs is passed to it -- would make + // every clobbered argument register look like a result, which is how a + // destructor ends up returning `rcx`. + let operands = operands(caller); + + for b in caller.blocks.values() { + // What each register was last given in this block, so the value a call + // finds there is the one the caller put there. + let mut latest: BTreeMap = BTreeMap::new(); + // The call whose clobbers are being walked, if the last thing seen was + // a call or one of the undefined values it leaves behind. + let mut after: Option = None; + for op in &b.ops { + if op.kind == SsaKind::Op(Op::Call) { + after = None; + if let Some(target) = op.inputs.first().and_then(|i| i.as_const()) { + let at = Addr(target); + let entry = out.entry(at).or_default(); + entry.sites += 1; + for offset in latest.keys() { + if *offset != abi.stack_pointer { + *entry.set.entry(*offset).or_default() += 1; + } + } + if let Some(v) = op.out { + if used.contains(&v) { + entry.result_read += 1; + *entry.read_after.entry(v.location.offset).or_default() += 1; + } + } + after = Some(at); + } + // Everything a call clobbers is the callee's answer from here + // on, not the caller's. + latest.clear(); + continue; + } + if op.kind == SsaKind::Op(Op::Undefine) { + // A register the call left holding something nobody promised. + // A caller that goes on to read it is reading a result the + // convention does not name. + if let (Some(at), Some(v)) = (after, op.out) { + if v.location.space == Space::Register + && v.location.size > 1 + && v.location.offset != abi.stack_pointer + && operands.contains(&v) + { + if let Some(entry) = out.get_mut(&at) { + *entry.read_after.entry(v.location.offset).or_default() += 1; + } + } + } + continue; + } + after = None; + if let Some(v) = op.out { + if v.location.space == Space::Register { + latest.insert(v.location.offset, v); + } + } + } + } + out +} + +/// Every value something in this function reads as an operand. +/// +/// A phi input does not count. A phi is a merge the SSA builder inserted, not +/// something the code does, and a register a call clobbered flows into one +/// whenever the call is inside a branch. Counting that as a read would make +/// every clobbered register look like a result the callee returned. +fn operands(f: &SsaFunction) -> BTreeSet { + let mut out = BTreeSet::new(); + for b in f.blocks.values() { + for op in &b.ops { + if op.kind == SsaKind::Phi { + continue; + } + for i in &op.inputs { + if let Some(v) = i.as_value() { + out.insert(v); + } + } + } + } + out +} + +/// Every value this function does something with. +/// +/// Reading it as an operand is the obvious way and not the only one: a value +/// left in an argument register before a call is passed to that call, and one +/// left in a result register at a return is handed back. Neither appears as an +/// operand, because the registers are the convention rather than the +/// instruction. +fn consumed(f: &SsaFunction, abi: &Abi) -> BTreeSet { + let mut out = BTreeSet::new(); + for b in f.blocks.values() { + for op in &b.ops { + for i in &op.inputs { + if let Some(v) = i.as_value() { + out.insert(v); + } + } + } + } + + let argument: BTreeSet = abi + .integer_arguments + .iter() + .chain(abi.float_arguments.iter()) + .copied() + .collect(); + let result: BTreeSet = abi.results.iter().copied().collect(); + + for b in f.blocks.values() { + let mut latest: BTreeMap = BTreeMap::new(); + for op in &b.ops { + match op.kind { + SsaKind::Op(Op::Call) | SsaKind::Op(Op::CallInd) => { + for offset in &argument { + if let Some(v) = latest.get(offset) { + out.insert(*v); + } + } + } + SsaKind::Op(Op::Return) => { + for offset in &result { + if let Some(v) = latest.get(offset) { + out.insert(*v); + } + } + } + _ => {} + } + if let Some(v) = op.out { + if v.location.space == Space::Register { + latest.insert(v.location.offset, v); + } + } + } + } + out +} diff --git a/crates/e5r-ir/src/dwreg.rs b/crates/e5r-ir/src/dwreg.rs new file mode 100644 index 0000000..379f75c --- /dev/null +++ b/crates/e5r-ir/src/dwreg.rs @@ -0,0 +1,81 @@ +//! DWARF register numbers, and where they land in e5r's register file. +//! +//! DWARF numbers registers its own way, once per architecture, and the number +//! is the only thing a `DW_OP_reg` or a `DW_TAG_call_site_parameter` location +//! carries. Without this table the compiler's own record of what each call +//! passes where is unreadable: it says "register 5" and nothing here knows +//! that means `rdi`. +//! +//! The numbering is deliberately not the encoding order on x86-64. The System +//! V psABI assigns 0 to `rax`, 1 to `rdx` and 2 to `rcx`, where the instruction +//! encoding has `rcx` at 1 and `rdx` at 2, so a mapping that looked like the +//! identity would be wrong for exactly the two registers the convention cares +//! about most. +//! +//! Nothing here depends on a DWARF reader, so the IR keeps no debug-format +//! dependency: a caller that has parsed the numbers passes them in. + +use e5r_core::Arch; + +use crate::lift::{aarch64, x86}; + +/// x86-64 DWARF number to encoding number, for the first eight registers. The +/// System V psABI, figure 3.36: rax, rdx, rcx, rbx, rsi, rdi, rbp, rsp. +const X86_64_LOW: [u8; 8] = [0, 2, 1, 3, 6, 7, 5, 4]; + +/// The register file offset a DWARF register number names. +/// +/// `None` when the number names a register e5r does not model, which is an +/// honest gap rather than a guess: a segment register or an x87 stack slot has +/// no offset here, and inventing one would misplace an argument. +pub fn offset(arch: &Arch, number: u16) -> Option { + match arch { + Arch::X86_64 => match number { + 0..=7 => Some(x86::gpr_offset(X86_64_LOW[number as usize])), + 8..=15 => Some(x86::gpr_offset(number as u8)), + // 16 is the return address, which lives on the stack rather than + // in a register, and 17 through 32 are xmm0 through xmm15. + 17..=32 => Some(x86::vec_offset((number - 17) as u8)), + _ => None, + }, + Arch::X86 => match number { + // eax, ecx, edx, ebx, esp, ebp, esi, edi: the encoding order, for + // once. 8 is eip and 9 is eflags. + 0..=7 => Some(x86::gpr_offset(number as u8)), + 21..=28 => Some(x86::vec_offset((number - 21) as u8)), + _ => None, + }, + Arch::AArch64 => match number { + // x0 through x30, then the stack pointer. 32 is the PC and 33 the + // exception link register, neither of which is in the file. + 0..=30 => Some(aarch64::gpr_offset(number as u8)), + 31 => Some(aarch64::sp_offset()), + // v0 through v31. 48 through 63 are the SVE predicates and 96 + // upwards the SVE vectors, none of which are modelled. + 64..=95 => Some(aarch64::vec_offset((number - 64) as u8)), + _ => None, + }, + _ => None, + } +} + +/// The DWARF register number for a register file offset, the other way round. +/// +/// Useful for reporting: a detected convention that names offsets can say what +/// the compiler would have called them. +pub fn number(arch: &Arch, offset_of: u64) -> Option { + // Small enough that a search beats a second table that could disagree with + // the first one. + let limit = match arch { + Arch::X86_64 => 33u16, + Arch::X86 => 29, + Arch::AArch64 => 96, + _ => 0, + }; + (0..limit).find(|n| offset(arch, *n) == Some(offset_of)) +} + +/// True when the architecture has a mapping at all. +pub fn known(arch: &Arch) -> bool { + matches!(arch, Arch::X86 | Arch::X86_64 | Arch::AArch64) +} diff --git a/crates/e5r-ir/src/exec.rs b/crates/e5r-ir/src/exec.rs new file mode 100644 index 0000000..bb8d647 --- /dev/null +++ b/crates/e5r-ir/src/exec.rs @@ -0,0 +1,143 @@ +//! Running lifted code. +//! +//! Decode, lift, interpret, one machine instruction at a time. This is the +//! harness the lifter tests use: compile a function, run it here, and compare +//! against the answer computed independently. A lifter cannot be checked by +//! reading it. + +use e5r_core::{Addr, Arch, MemoryMap}; + +use crate::interp::{Machine, Step, Stop, step}; +use crate::lift; + +/// How a run ended, and what it did on the way. +#[derive(Debug, Clone)] +pub struct Outcome { + /// Why it stopped. + pub stop: Stop, + /// Machine instructions executed. + pub insns: u64, + /// IR operations executed. + pub ops: u64, + /// Instructions the lifter did not model, by address. + pub unlifted: Vec, +} + +impl Outcome { + /// True when the run ended by returning, with nothing unmodelled. + pub fn returned_cleanly(&self) -> bool { + self.stop == Stop::Returned && self.unlifted.is_empty() + } +} + +/// Run from `entry` until the code returns or the budget runs out, without +/// following calls: a call stops the run and reports its target. +pub fn run(m: &mut Machine<'_>, image: &MemoryMap, arch: &Arch, entry: Addr) -> Outcome { + run_with(m, image, arch, entry, 0) +} + +/// Run, following calls up to `depth` frames deep. +/// +/// Following calls is what makes a function that uses a helper, or recurses, +/// testable at all; the depth limit is what keeps a runaway from being +/// indistinguishable from a hang. +pub fn run_with( + m: &mut Machine<'_>, + image: &MemoryMap, + arch: &Arch, + entry: Addr, + depth: u32, +) -> Outcome { + let mut pc = entry; + let mut insns = 0u64; + let mut ops = 0u64; + let mut unlifted = Vec::new(); + let mut budget = m.budget; + let mut frames = 0u32; + + loop { + if budget == 0 { + return Outcome { + stop: Stop::Budget, + insns, + ops, + unlifted, + }; + } + let Some(window) = image.decode_window(pc, arch.max_insn_len()) else { + return Outcome { + stop: Stop::NoCode(pc), + insns, + ops, + unlifted, + }; + }; + let Some(insn) = e5r_arch::decode(arch, window, pc) else { + return Outcome { + stop: Stop::NoCode(pc), + insns, + ops, + unlifted, + }; + }; + let lifted = lift::lift(arch, &insn); + if !lifted.complete { + unlifted.push(pc); + } + + m.end_instruction(); + let mut next = insn.next(); + let mut jumped = false; + for ir in &lifted.ops { + budget = budget.saturating_sub(1); + ops += 1; + match step(m, ir) { + Step::Next => {} + Step::Jump(t) => { + next = t; + jumped = true; + break; + } + Step::Call(target) => { + if frames >= depth { + return Outcome { + stop: Stop::Call(target), + insns: insns + 1, + ops, + unlifted, + }; + } + frames += 1; + next = target; + jumped = true; + break; + } + Step::Leave(target) => { + if frames == 0 { + return Outcome { + stop: Stop::Returned, + insns: insns + 1, + ops, + unlifted, + }; + } + frames -= 1; + next = target; + jumped = true; + break; + } + Step::Halt(s) => { + return Outcome { + stop: s, + insns: insns + 1, + ops, + unlifted, + }; + } + } + } + let _ = jumped; + insns += 1; + pc = next; + } +} diff --git a/crates/e5r-ir/src/func.rs b/crates/e5r-ir/src/func.rs new file mode 100644 index 0000000..d6dcdb2 --- /dev/null +++ b/crates/e5r-ir/src/func.rs @@ -0,0 +1,177 @@ +//! A whole function's IR, as blocks in a graph. +//! +//! Built from the machine control flow graph by lifting each block's +//! instructions in order. The IR keeps the machine's block structure rather +//! than inventing its own, so every operation can still be traced to the +//! instruction it came from. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_arch::Insn; +use e5r_core::{Addr, Arch, MemoryMap}; + +use crate::lift; +use crate::op::{IrOp, Op}; + +/// One block of IR. +#[derive(Debug, Clone)] +pub struct Block { + /// Where the block starts. + pub addr: Addr, + /// The operations, in order. + pub ops: Vec, + /// Blocks control can reach from here. + pub successors: Vec, + /// Blocks that can reach here. + pub predecessors: Vec, +} + +/// A function's IR. +#[derive(Debug, Clone)] +pub struct Function { + /// Which architecture the code came from, so a later pass can ask what the + /// calling convention is without being told again. + pub arch: Arch, + /// Where the function starts. + pub entry: Addr, + /// Blocks by start address. + pub blocks: BTreeMap, + /// Instructions the lifter did not model. + pub unlifted: Vec, +} + +impl Function { + /// Blocks in reverse post-order from the entry, which is the order every + /// forward dataflow pass wants. + pub fn reverse_postorder(&self) -> Vec { + let mut seen = BTreeSet::new(); + let mut post = Vec::new(); + // An explicit stack rather than recursion: a function with ten + // thousand blocks would overflow one. + let mut stack = vec![(self.entry, 0usize)]; + seen.insert(self.entry); + while let Some((at, i)) = stack.pop() { + let Some(b) = self.blocks.get(&at) else { + continue; + }; + if i < b.successors.len() { + stack.push((at, i + 1)); + let next = b.successors[i]; + if self.blocks.contains_key(&next) && seen.insert(next) { + stack.push((next, 0)); + } + } else { + post.push(at); + } + } + post.reverse(); + post + } + + /// How many operations the function holds. + pub fn op_count(&self) -> usize { + self.blocks.values().map(|b| b.ops.len()).sum() + } + + /// True when every instruction was modelled. + pub fn is_complete(&self) -> bool { + self.unlifted.is_empty() + } +} + +/// Lift a function, given its machine blocks. +/// +/// `blocks` maps each block's start to its address range and its successors, +/// which is what the analysis crate's control flow graph already holds. +pub fn build( + mem: &MemoryMap, + arch: &Arch, + entry: Addr, + blocks: &BTreeMap)>, +) -> Function { + let mut out: BTreeMap = BTreeMap::new(); + let mut unlifted = Vec::new(); + + for (start, (end, succs)) in blocks { + let mut ops = Vec::new(); + let mut at = *start; + while at < *end { + let Some(window) = mem.decode_window(at, arch.max_insn_len()) else { + break; + }; + let Some(insn) = e5r_arch::decode(arch, window, at) else { + break; + }; + let lifted = lift::lift(arch, &insn); + if !lifted.complete { + unlifted.push(at); + } + // Temporaries are numbered per instruction, so they are renamed to + // be unique within the block before being appended. + let base = ops.len() as u64 * crate::lift::MAX_TEMPS; + ops.extend(lifted.ops.iter().map(|o| rebase_temps(*o, base))); + at = insn.next(); + } + out.insert( + *start, + Block { + addr: *start, + ops, + successors: succs.clone(), + predecessors: Vec::new(), + }, + ); + } + + // Predecessors, derived rather than stored twice. + let edges: Vec<(Addr, Addr)> = out + .iter() + .flat_map(|(a, b)| b.successors.iter().map(move |s| (*a, *s))) + .collect(); + for (from, to) in edges { + if let Some(b) = out.get_mut(&to) { + if !b.predecessors.contains(&from) { + b.predecessors.push(from); + } + } + } + + Function { + arch: arch.clone(), + entry, + blocks: out, + unlifted, + } +} + +/// Shift an operation's temporaries so they do not collide with another +/// instruction's. +fn rebase_temps(mut op: IrOp, base: u64) -> IrOp { + if let Some(o) = op.out.as_mut() { + if o.space == crate::op::Space::Unique { + o.offset += base; + } + } + op.map_inputs(|v| { + if v.space == crate::op::Space::Unique { + crate::op::Varnode { + offset: v.offset + base, + ..v + } + } else { + v + } + }); + op +} + +/// Decode one instruction, for a caller that wants the machine view too. +pub fn decode_at(mem: &MemoryMap, arch: &Arch, at: Addr) -> Option { + let window = mem.decode_window(at, arch.max_insn_len())?; + e5r_arch::decode(arch, window, at) +} + +/// True when the operation ends a block. +pub fn terminates(op: &IrOp) -> bool { + op.op.is_branch() || op.op == Op::Unimplemented +} diff --git a/crates/e5r-ir/src/interp.rs b/crates/e5r-ir/src/interp.rs new file mode 100644 index 0000000..92bfaab --- /dev/null +++ b/crates/e5r-ir/src/interp.rs @@ -0,0 +1,706 @@ +//! An interpreter for the IR. +//! +//! This exists to test the lifters, and that is not a secondary use. A lifter +//! is a translation between two semantics and there is no way to check one by +//! reading it: the only honest test is to run the original and the translation +//! and compare. Ghidra's PCodeTest works exactly this way, and it is why its +//! forty processor specifications are trustworthy. +//! +//! It is a plain interpreter, not a symbolic one. Unknown memory reads as zero +//! and are reported, so a test can tell a real answer from one the model +//! invented. + +use std::collections::BTreeMap; + +use e5r_core::{Addr, MemoryMap}; + +use crate::op::{IrOp, Op, Space, Varnode}; + +/// Why execution stopped. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Stop { + /// The program returned. + Returned, + /// The step budget ran out, which is how a loop that does not terminate + /// is reported rather than hung on. + Budget, + /// An operation the lifter did not model was reached. + Unimplemented(Addr), + /// A branch went somewhere with no code. + NoCode(Addr), + /// A call was reached and calls are not being followed. + Call(Addr), + /// Division by zero. + DivideByZero(Addr), +} + +/// The machine the IR runs on. +pub struct Machine<'a> { + /// The register file, by byte offset. Sparse, because most of it is never + /// touched and a dense array would be mostly zeros. + registers: BTreeMap, + /// Writable memory, overlaying the image. + memory: BTreeMap, + /// The image, read when `memory` has nothing at an address. + image: Option<&'a MemoryMap>, + /// Lifting temporaries, cleared between machine instructions. + temps: BTreeMap, + /// Addresses read that nothing had written. + pub unknown_reads: Vec, + /// How many operations may run before [`Stop::Budget`]. + pub budget: u64, +} + +impl<'a> Machine<'a> { + /// A machine with nothing in it. + pub fn new() -> Machine<'a> { + Machine { + registers: BTreeMap::new(), + memory: BTreeMap::new(), + image: None, + temps: BTreeMap::new(), + unknown_reads: Vec::new(), + budget: 1 << 22, + } + } + + /// A machine that can read the program's own bytes. + pub fn over(image: &'a MemoryMap) -> Machine<'a> { + Machine { + image: Some(image), + ..Machine::new() + } + } + + /// Read a register as an integer. + pub fn reg(&self, offset: u64, size: u8) -> u64 { + let mut v = 0u64; + for i in 0..size as u64 { + let b = self.registers.get(&(offset + i)).copied().unwrap_or(0); + v |= (b as u64) << (i * 8); + } + v + } + + /// Write a register. + pub fn set_reg(&mut self, offset: u64, size: u8, value: u64) { + for i in 0..size as u64 { + self.registers.insert(offset + i, (value >> (i * 8)) as u8); + } + } + + /// Put bytes in memory. + /// The memory the machine has written, which is everything not in the + /// image it started from. + pub fn written(&self) -> &BTreeMap { + &self.memory + } + + /// Write bytes into the machine's memory. + pub fn write_mem(&mut self, at: u64, bytes: &[u8]) { + for (i, b) in bytes.iter().enumerate() { + self.memory.insert(at + i as u64, *b); + } + } + + /// Read bytes from memory, falling back to the image. + pub fn read_mem(&mut self, at: u64, size: u8) -> u64 { + let mut v = 0u64; + for i in 0..size as u64 { + let addr = at.wrapping_add(i); + let b = match self.memory.get(&addr) { + Some(b) => *b, + None => match self.image.and_then(|m| m.slice(Addr(addr), 1)) { + Some(s) => s[0], + None => { + self.unknown_reads.push(Addr(addr)); + 0 + } + }, + }; + v |= (b as u64) << (i * 8); + } + v + } + + fn read(&mut self, v: Varnode) -> u64 { + let raw = match v.space { + Space::Const => v.offset, + Space::Register => self.reg(v.offset, v.size), + // A promoted slot is storage of its own; the interpreter runs + // unpromoted code, so this exists for completeness. + Space::Unique | Space::Stack => self.temps.get(&v.offset).copied().unwrap_or(0), + Space::Ram => self.read_mem(v.offset, v.size), + }; + raw & v.mask() + } + + fn write(&mut self, v: Varnode, value: u64) { + let value = value & v.mask(); + match v.space { + Space::Const => {} + Space::Register => self.set_reg(v.offset, v.size, value), + Space::Unique | Space::Stack => { + self.temps.insert(v.offset, value); + } + Space::Ram => { + for i in 0..v.size as u64 { + self.memory.insert(v.offset + i, (value >> (i * 8)) as u8); + } + } + } + } + + /// Clear the lifting temporaries, which do not live across a machine + /// instruction. + pub fn end_instruction(&mut self) { + self.temps.clear(); + } +} + +impl Default for Machine<'_> { + fn default() -> Self { + Machine::new() + } +} + +/// The sign bit of the low `size` bytes of `v`. +fn sign_bit(v: u64, size: u8) -> bool { + if size == 0 || size > 8 { + return false; + } + v >> (size as u32 * 8 - 1) & 1 == 1 +} + +/// Sign-extend the low `size` bytes of `v`. +fn sext(v: u64, size: u8) -> i64 { + match size { + 0 => 0, + 1..=7 => { + let bits = size as u32 * 8; + let shift = 64 - bits; + ((v << shift) as i64) >> shift + } + _ => v as i64, + } +} + +/// What one operation did. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Step { + /// Carry on with the next operation. + Next, + /// Continue at this machine address. + Jump(Addr), + /// Enter a call at this address. + Call(Addr), + /// Return to this address. + Leave(Addr), + /// Stop. + Halt(Stop), +} + +/// Run one operation. +pub fn step(m: &mut Machine<'_>, ir: &IrOp) -> Step { + let a = |m: &mut Machine<'_>| ir.input(0).map(|v| m.read(v)).unwrap_or(0); + + match ir.op { + Op::Unimplemented => return Step::Halt(Stop::Unimplemented(ir.addr)), + Op::Branch => { + let Some(t) = ir.input(0).and_then(|v| v.value()) else { + return Step::Halt(Stop::Unimplemented(ir.addr)); + }; + return Step::Jump(Addr(t)); + } + Op::CBranch => { + let cond = ir.input(1).map(|v| m.read(v)).unwrap_or(0); + if cond != 0 { + let Some(t) = ir.input(0).and_then(|v| v.value()) else { + return Step::Halt(Stop::Unimplemented(ir.addr)); + }; + return Step::Jump(Addr(t)); + } + return Step::Next; + } + Op::BranchInd | Op::Return => { + let t = a(m); + return if ir.op == Op::Return { + Step::Leave(Addr(t)) + } else { + Step::Jump(Addr(t)) + }; + } + Op::Call | Op::CallInd => { + let t = a(m); + return Step::Call(Addr(t)); + } + _ => {} + } + + let out = match ir.out { + Some(o) => o, + // An operation with no output and no branch is a store or nothing. + None => { + if ir.op == Op::Store { + let (Some(addr), Some(value)) = (ir.input(0), ir.input(1)) else { + return Step::Next; + }; + let at = m.read(addr); + let v = m.read(value); + for i in 0..value.size as u64 { + m.memory.insert(at.wrapping_add(i), (v >> (i * 8)) as u8); + } + } + return Step::Next; + } + }; + + let x = ir.input(0).map(|v| m.read(v)).unwrap_or(0); + let xs = ir.input(0).map(|v| sext(m.read(v), v.size)).unwrap_or(0); + let y = ir.input(1).map(|v| m.read(v)).unwrap_or(0); + let ys = ir.input(1).map(|v| sext(m.read(v), v.size)).unwrap_or(0); + + let value = match ir.op { + // The callee wrote whatever is there; the machine is not told to + // forget it, only the analysis is. + Op::Undefine => return Step::Next, + Op::Copy => x, + Op::Load => { + let size = out.size; + m.read_mem(x, size) + } + Op::IntAdd => x.wrapping_add(y), + Op::IntSub => x.wrapping_sub(y), + Op::IntMul => x.wrapping_mul(y), + Op::IntDiv + | Op::IntRem + | Op::IntSDiv + | Op::IntSRem + | Op::IntDiv128 + | Op::IntRem128 + | Op::IntSDiv128 + | Op::IntSRem128 + if divisor(ir, m) == 0 => + { + return Step::Halt(Stop::DivideByZero(ir.addr)); + } + Op::IntDiv => x / y, + Op::IntRem => x % y, + Op::IntSDiv => xs.wrapping_div(ys) as u64, + Op::IntSRem => xs.wrapping_rem(ys) as u64, + Op::IntAnd => x & y, + Op::IntOr => x | y, + Op::IntXor => x ^ y, + Op::IntNot => !x, + Op::IntNegate => x.wrapping_neg(), + // A shift past the operand's width produces zero, not the wrap a Rust + // shift would give. + Op::IntLeft => { + let bits = ir.input(0).map(|v| v.size as u64 * 8).unwrap_or(64); + if y >= bits { 0 } else { x << y } + } + Op::IntRight => { + let bits = ir.input(0).map(|v| v.size as u64 * 8).unwrap_or(64); + if y >= bits { 0 } else { x >> y } + } + Op::IntSRight => { + let bits = ir.input(0).map(|v| v.size as u64 * 8).unwrap_or(64); + if y >= bits { + if xs < 0 { u64::MAX } else { 0 } + } else { + (xs >> y) as u64 + } + } + Op::IntEqual => (x == y) as u64, + Op::IntNotEqual => (x != y) as u64, + Op::IntLess => (x < y) as u64, + Op::IntSLess => (xs < ys) as u64, + Op::IntLessEqual => (x <= y) as u64, + Op::IntSLessEqual => (xs <= ys) as u64, + // A carry out happened when the truncated sum came out below either + // addend, which is the only test that works at every width. + Op::IntCarry => { + let mask = mask_of(ir, 0); + ((x.wrapping_add(y) & mask) < x) as u64 + } + // Signed overflow from the sign bits. Comparing a sign-extended result + // against the true one only works below 64 bits, where the true result + // still fits; at 64 bits it can never differ and the flag was always + // false. + Op::IntSCarry => { + let size = ir.input(0).map(|v| v.size).unwrap_or(8); + let sum = x.wrapping_add(y); + (sign_bit(x, size) == sign_bit(y, size) && sign_bit(sum, size) != sign_bit(x, size)) + as u64 + } + Op::IntSBorrow => { + let size = ir.input(0).map(|v| v.size).unwrap_or(8); + let d = x.wrapping_sub(y); + (sign_bit(x, size) != sign_bit(y, size) && sign_bit(d, size) != sign_bit(x, size)) + as u64 + } + // The half of a product that does not fit. x86's `mul` writes it and + // AArch64's `umulh` reads it, so it is an operation rather than + // something a lifter can approximate. + Op::IntMulHigh => { + let bits = ir.input(0).map(|v| v.size as u32 * 8).unwrap_or(64); + ((x as u128 * y as u128) >> bits) as u64 + } + Op::IntSMulHigh => { + let bits = ir.input(0).map(|v| v.size as u32 * 8).unwrap_or(64); + ((xs as i128 * ys as i128) >> bits) as u64 + } + // A double-width dividend, which x86's one-operand divide needs. The + // third input is the divisor; the first two are the halves. + Op::IntDiv128 | Op::IntRem128 | Op::IntSDiv128 | Op::IntSRem128 => { + let bits = ir.input(1).map(|v| v.size as u32 * 8).unwrap_or(64); + let d = ir.input(2).map(|v| m.read(v)).unwrap_or(0); + let size = ir.input(1).map(|v| v.size).unwrap_or(8); + match ir.op { + Op::IntDiv128 => { + let n = ((x as u128) << bits) | y as u128; + (n / d as u128) as u64 + } + Op::IntRem128 => { + let n = ((x as u128) << bits) | y as u128; + (n % d as u128) as u64 + } + Op::IntSDiv128 => { + let n = sext128((x as u128) << bits | y as u128, bits * 2); + (n.wrapping_div(sext(d, size) as i128)) as u64 + } + _ => { + let n = sext128((x as u128) << bits | y as u128, bits * 2); + (n.wrapping_rem(sext(d, size) as i128)) as u64 + } + } + } + // Floating point, at the width of the operands. The bits are the + // value; nothing here reinterprets a pattern it was not given. + Op::FloatAdd + | Op::FloatSub + | Op::FloatMul + | Op::FloatDiv + | Op::FloatMax + | Op::FloatMin + | Op::FloatNeg + | Op::FloatAbs + | Op::FloatSqrt + | Op::FloatTrunc + | Op::FloatRound + | Op::FloatCeil + | Op::FloatFloor => { + // A single precision result is computed in single precision, so + // the rounding happens where the machine does it. + if out.size == 4 { + let a = as_f32(x); + let c = as_f32(y); + let r = match ir.op { + Op::FloatAdd => a + c, + Op::FloatSub => a - c, + Op::FloatMul => a * c, + Op::FloatDiv => a / c, + Op::FloatMax => a.max(c), + Op::FloatMin => a.min(c), + Op::FloatNeg => -a, + Op::FloatAbs => a.abs(), + Op::FloatSqrt => a.sqrt(), + Op::FloatTrunc => a.trunc(), + Op::FloatRound => round_ties_even(a as f64) as f32, + Op::FloatCeil => a.ceil(), + _ => a.floor(), + }; + r.to_bits() as u64 + } else { + let size = ir.input(0).map(|v| v.size).unwrap_or(8); + let a = as_f64(x, size); + let c = as_f64(y, ir.input(1).map(|v| v.size).unwrap_or(size)); + let r = match ir.op { + Op::FloatAdd => a + c, + Op::FloatSub => a - c, + Op::FloatMul => a * c, + Op::FloatDiv => a / c, + Op::FloatMax => a.max(c), + Op::FloatMin => a.min(c), + Op::FloatNeg => -a, + Op::FloatAbs => a.abs(), + Op::FloatSqrt => a.sqrt(), + Op::FloatTrunc => a.trunc(), + Op::FloatRound => round_ties_even(a), + Op::FloatCeil => a.ceil(), + _ => a.floor(), + }; + r.to_bits() + } + } + // Fused: one rounding, which is why it is an operation rather than a + // multiply followed by an add. + Op::FloatMulAdd => { + let z = ir.input(2).map(|v| m.read(v)).unwrap_or(0); + if out.size == 4 { + as_f32(x).mul_add(as_f32(y), as_f32(z)).to_bits() as u64 + } else { + f64::from_bits(x) + .mul_add(f64::from_bits(y), f64::from_bits(z)) + .to_bits() + } + } + Op::FloatEqual | Op::FloatNotEqual | Op::FloatLess | Op::FloatLessEqual | Op::FloatNan => { + let size = ir.input(0).map(|v| v.size).unwrap_or(8); + let a = as_f64(x, size); + let c = as_f64(y, ir.input(1).map(|v| v.size).unwrap_or(size)); + match ir.op { + Op::FloatEqual => (a == c) as u64, + Op::FloatNotEqual => (a != c) as u64, + Op::FloatLess => (a < c) as u64, + Op::FloatLessEqual => (a <= c) as u64, + _ => a.is_nan() as u64, + } + } + Op::IntToFloat | Op::UIntToFloat => { + let size = ir.input(0).map(|v| v.size).unwrap_or(8); + if out.size == 4 { + let v = if ir.op == Op::IntToFloat { + sext(x, size) as f32 + } else { + (x & mask_of(ir, 0)) as f32 + }; + v.to_bits() as u64 + } else { + let v = if ir.op == Op::IntToFloat { + sext(x, size) as f64 + } else { + (x & mask_of(ir, 0)) as f64 + }; + v.to_bits() + } + } + // Saturating, which is what both architectures do with a value the + // integer cannot hold. + Op::FloatToInt | Op::FloatToUInt => { + let size = ir.input(0).map(|v| v.size).unwrap_or(8); + let v = as_f64(x, size).trunc(); + if ir.op == Op::FloatToInt { + match out.size { + 1 => v as i8 as u64, + 2 => v as i16 as u64, + 4 => v as i32 as u64, + _ => v as i64 as u64, + } + } else { + match out.size { + 1 => v as u8 as u64, + 2 => v as u16 as u64, + 4 => v as u32 as u64, + _ => v as u64, + } + } + } + Op::FloatConvert => { + let size = ir.input(0).map(|v| v.size).unwrap_or(8); + let v = as_f64(x, size); + if out.size == 4 { + (v as f32).to_bits() as u64 + } else { + v.to_bits() + } + } + Op::IntZExt => x, + Op::IntSExt => xs as u64, + Op::PopCount => x.count_ones() as u64, + Op::LzCount => { + let bits = ir.input(0).map(|v| v.size as u32 * 8).unwrap_or(64); + (x << (64 - bits)).leading_zeros() as u64 + } + Op::BoolAnd => ((x != 0) && (y != 0)) as u64, + Op::BoolOr => ((x != 0) || (y != 0)) as u64, + Op::BoolXor => ((x != 0) ^ (y != 0)) as u64, + Op::BoolNot => (x == 0) as u64, + Op::Piece => { + let low_bits = ir.input(1).map(|v| v.size as u32 * 8).unwrap_or(0); + (x << low_bits) | y + } + Op::SubPiece => x >> (y * 8), + _ => return Step::Next, + }; + + m.write(out, value); + Step::Next +} + +/// Read a double-width value as signed. The halves compose into 128 bits but +/// the value occupies only twice the operand width, so the sign lives there. +fn sext128(v: u128, bits: u32) -> i128 { + if bits >= 128 { + return v as i128; + } + ((v << (128 - bits)) as i128) >> (128 - bits) +} + +/// Read a bit pattern as a floating point number of the given width. +fn as_f64(bits: u64, size: u8) -> f64 { + if size == 4 { + f32::from_bits(bits as u32) as f64 + } else { + f64::from_bits(bits) + } +} + +/// Read a bit pattern as single precision. +fn as_f32(bits: u64) -> f32 { + f32::from_bits(bits as u32) +} + +/// Round to nearest with ties to even, the mode both architectures start in. +fn round_ties_even(v: f64) -> f64 { + let r = v.round(); + if (v - v.trunc()).abs() == 0.5 && r % 2.0 != 0.0 { + r - v.signum() + } else { + r + } +} + +/// The divisor of a divide, which the double-width forms take third. +fn divisor(ir: &IrOp, m: &mut Machine) -> u64 { + let n = match ir.op { + Op::IntDiv128 | Op::IntRem128 | Op::IntSDiv128 | Op::IntSRem128 => 2, + _ => 1, + }; + ir.input(n).map(|v| m.read(v)).unwrap_or(0) +} + +/// The mask of the operation's first input, for the carry computation. +fn mask_of(ir: &IrOp, n: usize) -> u64 { + ir.input(n).map(|v| v.mask()).unwrap_or(u64::MAX) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn op(o: Op, out: Option, ins: &[Varnode]) -> IrOp { + let mut ir = IrOp::new(Addr(0x1000), o, out); + for i in ins { + ir = ir.with(*i); + } + ir + } + + #[test] + fn arithmetic_truncates_to_the_output_size() { + let mut m = Machine::new(); + let out = Varnode::register(0, 4); + step( + &mut m, + &op( + Op::IntAdd, + Some(out), + &[Varnode::constant(0xffff_ffff, 4), Varnode::constant(1, 4)], + ), + ); + // Four bytes, so the carry out is dropped rather than kept. + assert_eq!(m.reg(0, 4), 0); + assert_eq!(m.reg(0, 8), 0); + } + + #[test] + fn signed_and_unsigned_comparison_differ() { + let mut m = Machine::new(); + let out = Varnode::register(0, 1); + let neg = Varnode::constant(0xffff_ffff_ffff_ffff, 8); + let one = Varnode::constant(1, 8); + step(&mut m, &op(Op::IntSLess, Some(out), &[neg, one])); + assert_eq!(m.reg(0, 1), 1, "-1 < 1 signed"); + step(&mut m, &op(Op::IntLess, Some(out), &[neg, one])); + assert_eq!(m.reg(0, 1), 0, "0xffff... < 1 unsigned is false"); + } + + #[test] + fn a_shift_past_the_width_is_zero_not_a_wrap() { + // Rust's shift operators panic or wrap; the machine produces zero. + let mut m = Machine::new(); + let out = Varnode::register(0, 4); + step( + &mut m, + &op( + Op::IntLeft, + Some(out), + &[Varnode::constant(1, 4), Varnode::constant(64, 1)], + ), + ); + assert_eq!(m.reg(0, 4), 0); + } + + #[test] + fn an_arithmetic_shift_of_a_negative_keeps_the_sign() { + let mut m = Machine::new(); + let out = Varnode::register(0, 4); + step( + &mut m, + &op( + Op::IntSRight, + Some(out), + &[Varnode::constant(0xffff_fff0, 4), Varnode::constant(4, 1)], + ), + ); + assert_eq!(m.reg(0, 4), 0xffff_ffff); + } + + #[test] + fn sign_extension_widens_correctly() { + let mut m = Machine::new(); + let out = Varnode::register(0, 8); + step( + &mut m, + &op(Op::IntSExt, Some(out), &[Varnode::constant(0xff, 1)]), + ); + assert_eq!(m.reg(0, 8), u64::MAX); + step( + &mut m, + &op(Op::IntZExt, Some(out), &[Varnode::constant(0xff, 1)]), + ); + assert_eq!(m.reg(0, 8), 0xff); + } + + #[test] + fn overlapping_registers_really_overlap() { + // The reason the register space is byte-addressed: writing the low + // four bytes has to be visible through the eight-byte view. + let mut m = Machine::new(); + m.set_reg(0, 8, 0xdead_beef_cafe_babe); + m.set_reg(0, 4, 0x1111_2222); + assert_eq!(m.reg(0, 4), 0x1111_2222); + assert_eq!(m.reg(0, 8), 0xdead_beef_1111_2222); + } + + #[test] + fn division_by_zero_stops_rather_than_panicking() { + let mut m = Machine::new(); + let out = Varnode::register(0, 8); + let r = step( + &mut m, + &op( + Op::IntDiv, + Some(out), + &[Varnode::constant(1, 8), Varnode::constant(0, 8)], + ), + ); + assert!(matches!(r, Step::Halt(Stop::DivideByZero(_)))); + } + + #[test] + fn an_unknown_read_is_recorded_rather_than_invented() { + let mut m = Machine::new(); + assert_eq!(m.read_mem(0x4000, 4), 0); + assert_eq!(m.unknown_reads.len(), 4); + } + + #[test] + fn memory_round_trips() { + let mut m = Machine::new(); + m.write_mem(0x1000, &[0x78, 0x56, 0x34, 0x12]); + assert_eq!(m.read_mem(0x1000, 4), 0x1234_5678); + assert!(m.unknown_reads.is_empty()); + } +} diff --git a/crates/e5r-ir/src/lib.rs b/crates/e5r-ir/src/lib.rs new file mode 100644 index 0000000..9a374a3 --- /dev/null +++ b/crates/e5r-ir/src/lib.rs @@ -0,0 +1,32 @@ +//! The intermediate representation, its lifters, and its interpreter. +//! +//! Lifting turns a decoded machine instruction into a sequence of operations +//! with no implicit effects, so analysis is written once rather than once per +//! architecture. +//! +//! The interpreter ships with the lifters rather than after them. A lifter is a +//! translation between two semantics, and reading one cannot tell you whether +//! it is right; running the original and the translation and comparing can. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod abi; +pub mod conv; +pub mod dwreg; +pub mod exec; +pub mod func; +pub mod interp; +pub mod lift; +pub mod op; +pub mod opt; +pub mod proto; +pub mod range; +pub mod shape; +pub mod ssa; +pub mod stack; + +pub use exec::{Outcome, run, run_with}; +pub use interp::{Machine, Step, Stop, step}; +pub use lift::{Lifted, lift}; +pub use op::{IrOp, Op, Space, Varnode}; diff --git a/crates/e5r-ir/src/lift.rs b/crates/e5r-ir/src/lift.rs new file mode 100644 index 0000000..ddc32c6 --- /dev/null +++ b/crates/e5r-ir/src/lift.rs @@ -0,0 +1,137 @@ +//! Turning decoded instructions into IR. +//! +//! One lifter per architecture, each producing operations with no implicit +//! effects. A machine instruction that sets four flags becomes four operations, +//! because dataflow that has to know which instruction touches which flag is +//! dataflow with a bug in it. +//! +//! An instruction the lifter does not model produces a single +//! [`Op::Unimplemented`], which stops the interpreter and is visible in the +//! coverage number. Producing nothing, or producing something approximate, +//! would make a wrong answer look like a right one. + +pub mod aarch64; +pub mod arm; +pub mod neon; +pub mod sse; +pub mod x86; + +use e5r_arch::Insn; +use e5r_core::Arch; + +use crate::op::{IrOp, Op, Varnode}; + +/// The most temporaries one instruction's lifting may use. Block building +/// shifts each instruction's temporaries by this much so they stay distinct. +pub const MAX_TEMPS: u64 = 64; + +/// The IR for one machine instruction. +#[derive(Debug, Clone, Default)] +pub struct Lifted { + /// The operations, in order. + pub ops: Vec, + /// True when the instruction was modelled completely. + pub complete: bool, +} + +impl Lifted { + /// True when nothing was produced. + pub fn is_empty(&self) -> bool { + self.ops.is_empty() + } +} + +/// A builder that hands out temporaries and collects operations. +pub struct Builder { + addr: e5r_core::Addr, + ops: Vec, + next_temp: u64, + /// How wide a pointer is on the machine this came from. + /// + /// The same instruction set is lifted at two widths -- x86 in long mode + /// and in protected mode -- and every address, stack adjustment and return + /// address differs by exactly this. Carrying it here rather than in each + /// lifter's own threading keeps one number in one place. + pub ptr: u8, +} + +impl Builder { + /// A builder for the instruction at `addr`, on a 64-bit machine. + pub fn new(addr: e5r_core::Addr) -> Builder { + Builder::sized(addr, 8) + } + + /// The same, for a machine whose pointers are `ptr` bytes. + pub fn sized(addr: e5r_core::Addr, ptr: u8) -> Builder { + Builder { + addr, + ops: Vec::new(), + next_temp: 0, + ptr, + } + } + + /// A fresh temporary of `size` bytes. + pub fn temp(&mut self, size: u8) -> Varnode { + // Wrapping rather than growing without bound: no instruction needs + // this many, and a runaway lifter should not silently corrupt another + // instruction's temporaries. + let v = Varnode::temp(self.next_temp % MAX_TEMPS, size); + self.next_temp += 1; + v + } + + /// Emit an operation. + pub fn emit(&mut self, op: Op, out: Option, inputs: &[Varnode]) { + let mut ir = IrOp::new(self.addr, op, out); + for i in inputs { + ir = ir.with(*i); + } + self.ops.push(ir); + } + + /// Emit an operation into a fresh temporary and return it. + pub fn eval(&mut self, op: Op, size: u8, inputs: &[Varnode]) -> Varnode { + let t = self.temp(size); + self.emit(op, Some(t), inputs); + t + } + + /// Finish, saying whether the instruction was modelled completely. + pub fn finish(self, complete: bool) -> Lifted { + Lifted { + ops: self.ops, + complete, + } + } + + /// Finish a complete instruction without consuming the builder, for a + /// lifter that decides as it goes whether it handled the instruction. + pub fn clone_finish(&self) -> Lifted { + Lifted { + ops: self.ops.clone(), + complete: true, + } + } + + /// Give up on this instruction, honestly. + pub fn unimplemented(mut self) -> Lifted { + self.emit(Op::Unimplemented, None, &[]); + Lifted { + ops: self.ops, + complete: false, + } + } +} + +/// Lift one instruction. +pub fn lift(arch: &Arch, insn: &Insn) -> Lifted { + match arch { + Arch::AArch64 => aarch64::lift(insn), + Arch::X86_64 => x86::lift(insn), + Arch::X86 => x86::lift32(insn), + Arch::Arm => arm::lift_mode(insn, false), + Arch::Thumb => arm::lift_mode(insn, true), + _ => Builder::new(insn.addr).unimplemented(), + } +} diff --git a/crates/e5r-ir/src/lift/aarch64.rs b/crates/e5r-ir/src/lift/aarch64.rs new file mode 100644 index 0000000..d220925 --- /dev/null +++ b/crates/e5r-ir/src/lift/aarch64.rs @@ -0,0 +1,1331 @@ +//! AArch64 lifting. +//! +//! Covers the integer instructions a compiler emits: arithmetic and logical +//! forms with their flag effects, shifts, multiplies and divides, conditional +//! selects, loads and stores, and branches. Floating point and SIMD are not +//! modelled and say so rather than approximating. +//! +//! The register file is byte-addressed, so `w0` is the low four bytes of `x0` +//! and the overlap is a property of the addresses rather than of a rule. A +//! 32-bit write zeroes the upper half on this architecture, which is a real +//! effect and is emitted rather than assumed. + +use e5r_arch::{AddrMode, Extend, Flow, Insn, Mem, Operand, Reg, RegClass, Shift, Width}; +use e5r_core::Addr; + +use crate::lift::{Builder, Lifted}; +use crate::op::{Op, Space, Varnode}; + +/// Byte offset of `x0` in the register file. +const X_BASE: u64 = 0; +/// Byte offset of the stack pointer. +const SP: u64 = 31 * 8; +/// Byte offset of the link register. +const LR: u64 = X_BASE + 30 * 8; +/// The four condition flags, one byte each. +const N: u64 = 33 * 8; +const Z: u64 = N + 1; +const C: u64 = N + 2; +const V: u64 = N + 3; +/// Byte offset of `v0`; vector registers are sixteen bytes each. +const V_BASE: u64 = 34 * 8; + +/// The negative flag. +pub fn flag_n() -> Varnode { + Varnode::register(N, 1) +} +/// The zero flag. +pub fn flag_z() -> Varnode { + Varnode::register(Z, 1) +} +/// The carry flag. +pub fn flag_c() -> Varnode { + Varnode::register(C, 1) +} +/// The overflow flag. +pub fn flag_v() -> Varnode { + Varnode::register(V, 1) +} + +/// The byte offset of a general purpose register, for a caller setting up a +/// machine before running lifted code. +pub fn gpr_offset(n: u8) -> u64 { + X_BASE + n as u64 * 8 +} + +/// The stack pointer's offset. +pub fn sp_offset() -> u64 { + SP +} + +/// The byte offset of a vector register; they are sixteen bytes each. +pub fn vec_offset(n: u8) -> u64 { + V_BASE + n as u64 * 16 +} + +fn width_bytes(w: Width) -> u8 { + match w { + Width::W8 => 1, + Width::W16 => 2, + Width::W32 => 4, + Width::W64 => 8, + Width::W128 => 16, + } +} + +/// The varnode for a register operand. +/// +/// The zero register is not storage: it reads as zero and discards writes, so +/// it becomes a constant. +fn reg(r: Reg) -> Varnode { + let size = width_bytes(r.width); + match r.class { + RegClass::Gpr => Varnode::register(gpr_offset(r.num), size), + RegClass::Sp => Varnode::register(SP, size), + RegClass::Zr => Varnode::constant(0, size), + RegClass::Vec => Varnode::register(V_BASE + r.num as u64 * 16, size), + _ => Varnode::constant(0, size), + } +} + +/// The low `size` bytes of a value. +/// +/// Narrowing a register varnode to one byte would name storage of its own, +/// because dataflow versions the register file in eight-byte units and keeps +/// one-byte register locations for the flags. The byte a narrowing store +/// writes has to be cut out of the register instead, so that the store depends +/// on whatever last wrote it. +fn narrow(b: &mut Builder, v: Varnode, size: u8) -> Varnode { + if v.size <= size { + return v; + } + if v.space == Space::Register && size == 1 { + return b.eval(Op::SubPiece, 1, &[v, Varnode::constant(0, 1)]); + } + Varnode { size, ..v } +} + +/// Write a value to a register, including the upper-half zeroing a 32-bit +/// write performs. +/// Reverse the bytes of every `lane`-byte group of a `size`-byte value. +/// +/// Built from shifts and masks because the IR has no byte-swap operation. It +/// is the exact computation and not an approximation, which is the only reason +/// it belongs here at all. +pub(crate) fn swap_bytes(b: &mut Builder, x: Varnode, size: u8, lane: u8) -> Varnode { + let mut out: Option = None; + for byte in 0..size { + // Where this byte lands: reversed within its own lane, and the lane + // itself does not move. + let within = byte % lane; + let to = byte - within + (lane - 1 - within); + let shift = i32::from(to) * 8 - i32::from(byte) * 8; + let moved = if shift >= 0 { + let masked = b.eval( + Op::IntAnd, + size, + &[x, Varnode::constant(0xffu64 << (byte * 8), size)], + ); + b.eval( + Op::IntLeft, + size, + &[masked, Varnode::constant(shift as u64, 1)], + ) + } else { + let shifted = b.eval( + Op::IntRight, + size, + &[x, Varnode::constant((-shift) as u64, 1)], + ); + b.eval( + Op::IntAnd, + size, + &[shifted, Varnode::constant(0xffu64 << (to * 8), size)], + ) + }; + out = Some(match out { + None => moved, + Some(acc) => b.eval(Op::IntOr, size, &[acc, moved]), + }); + } + out.unwrap_or(x) +} + +/// Reverse every bit of a `size`-byte value. +/// +/// Swap adjacent bits, then pairs, then nibbles, then reverse the bytes. Exact +/// for the same reason as above. +pub(crate) fn reverse_bits(b: &mut Builder, x: Varnode, size: u8) -> Varnode { + const PATTERNS: [(u64, u32); 3] = [ + (0x5555_5555_5555_5555, 1), + (0x3333_3333_3333_3333, 2), + (0x0f0f_0f0f_0f0f_0f0f, 4), + ]; + let mut v = x; + for (mask, by) in PATTERNS { + let low = b.eval(Op::IntAnd, size, &[v, Varnode::constant(mask, size)]); + let up = b.eval(Op::IntLeft, size, &[low, Varnode::constant(by as u64, 1)]); + let high = b.eval(Op::IntRight, size, &[v, Varnode::constant(by as u64, 1)]); + let high = b.eval(Op::IntAnd, size, &[high, Varnode::constant(mask, size)]); + v = b.eval(Op::IntOr, size, &[up, high]); + } + swap_bytes(b, v, size, size) +} + +fn write_reg(b: &mut Builder, r: Reg, value: Varnode) { + if r.class == RegClass::Zr { + return; + } + let dest = reg(r); + b.emit(Op::Copy, Some(dest), &[value]); + if r.width == Width::W32 && matches!(r.class, RegClass::Gpr | RegClass::Sp) { + // Emitting this rather than assuming it is the difference between + // modelling the machine and modelling what the reader expected. + let high = Varnode::register(dest.offset + 4, 4); + b.emit(Op::Copy, Some(high), &[Varnode::constant(0, 4)]); + } +} + +/// Materialize a source operand, applying any shift or extension. +fn source(b: &mut Builder, op: &Operand, size: u8) -> Option { + Some(match op { + Operand::Reg(r) => reg(*r), + Operand::Imm(v) => Varnode::constant(*v as u64, size), + Operand::UImm(v) => Varnode::constant(*v, size), + Operand::Count(v) => Varnode::constant(*v as u64, size), + Operand::Addr(a) => Varnode::constant(a.get(), 8), + Operand::Shifted(r, sh, amount) => { + let v = reg(*r); + let n = Varnode::constant(*amount as u64, 1); + match sh { + Shift::Lsl => b.eval(Op::IntLeft, v.size, &[v, n]), + Shift::Lsr => b.eval(Op::IntRight, v.size, &[v, n]), + Shift::Asr => b.eval(Op::IntSRight, v.size, &[v, n]), + // The machine has no rotate operation and neither does the IR. + Shift::Ror => { + let bits = v.size as u64 * 8; + let right = b.eval(Op::IntRight, v.size, &[v, n]); + let left_amount = Varnode::constant(bits - *amount as u64, 1); + let left = b.eval(Op::IntLeft, v.size, &[v, left_amount]); + b.eval(Op::IntOr, v.size, &[right, left]) + } + Shift::Msl => return None, + } + } + Operand::Extended(r, ext, amount) => { + let src = reg(*r); + let narrow_size = match ext { + Extend::Uxtb | Extend::Sxtb => 1, + Extend::Uxth | Extend::Sxth => 2, + Extend::Uxtw | Extend::Sxtw => 4, + _ => size, + }; + let narrowed = narrow(b, src, narrow_size.min(src.size)); + let signed = matches!( + ext, + Extend::Sxtb | Extend::Sxth | Extend::Sxtw | Extend::Sxtx + ); + let widened = if narrowed.size == size { + narrowed + } else { + b.eval( + if signed { Op::IntSExt } else { Op::IntZExt }, + size, + &[narrowed], + ) + }; + if *amount == 0 { + widened + } else { + b.eval( + Op::IntLeft, + size, + &[widened, Varnode::constant(*amount as u64, 1)], + ) + } + } + _ => return None, + }) +} + +/// Set the four flags from an addition or a subtraction. +fn set_flags(b: &mut Builder, size: u8, result: Varnode, x: Varnode, y: Varnode, sub: bool) { + let zero = Varnode::constant(0, size); + let shift = Varnode::constant(size as u64 * 8 - 1, 1); + let sign = b.eval(Op::IntRight, size, &[result, shift]); + let n = b.eval(Op::IntNotEqual, 1, &[sign, Varnode::constant(0, size)]); + b.emit(Op::Copy, Some(flag_n()), &[n]); + + let z = b.eval(Op::IntEqual, 1, &[result, zero]); + b.emit(Op::Copy, Some(flag_z()), &[z]); + + // For a subtraction the carry flag means "no borrow", which is an + // unsigned comparison rather than a carry out. + let c = if sub { + b.eval(Op::IntLessEqual, 1, &[y, x]) + } else { + b.eval(Op::IntCarry, 1, &[x, y]) + }; + b.emit(Op::Copy, Some(flag_c()), &[c]); + + let v = b.eval(if sub { Op::IntSBorrow } else { Op::IntSCarry }, 1, &[x, y]); + b.emit(Op::Copy, Some(flag_v()), &[v]); +} + +/// Set the flags a logical operation produces: N and Z from the result, with +/// C and V cleared. +fn set_logical_flags(b: &mut Builder, size: u8, result: Varnode) { + let zero = Varnode::constant(0, size); + let shift = Varnode::constant(size as u64 * 8 - 1, 1); + let sign = b.eval(Op::IntRight, size, &[result, shift]); + let n = b.eval(Op::IntNotEqual, 1, &[sign, zero]); + b.emit(Op::Copy, Some(flag_n()), &[n]); + let z = b.eval(Op::IntEqual, 1, &[result, zero]); + b.emit(Op::Copy, Some(flag_z()), &[z]); + b.emit(Op::Copy, Some(flag_c()), &[Varnode::constant(0, 1)]); + b.emit(Op::Copy, Some(flag_v()), &[Varnode::constant(0, 1)]); +} + +/// The condition a test selects on, for a lifter in another module. +pub fn condition_value(b: &mut Builder, cond: u8) -> Varnode { + condition(b, cond) +} + +/// The condition a `b.` or a conditional select tests, as one byte. +fn condition(b: &mut Builder, cond: u8) -> Varnode { + let (n, z, c, v) = (flag_n(), flag_z(), flag_c(), flag_v()); + // The low bit inverts the test the upper three bits select. + let base = match cond >> 1 { + 0b000 => z, + 0b001 => c, + 0b010 => n, + 0b011 => v, + 0b100 => { + let nz = b.eval(Op::BoolNot, 1, &[z]); + b.eval(Op::BoolAnd, 1, &[c, nz]) + } + 0b101 => b.eval(Op::IntEqual, 1, &[n, v]), + 0b110 => { + let nz = b.eval(Op::BoolNot, 1, &[z]); + let ge = b.eval(Op::IntEqual, 1, &[n, v]); + b.eval(Op::BoolAnd, 1, &[nz, ge]) + } + _ => Varnode::constant(1, 1), + }; + // `al` and `nv` both mean always, so the inversion does not apply to them. + if cond & 1 == 1 && cond >> 1 != 0b111 { + b.eval(Op::BoolNot, 1, &[base]) + } else { + base + } +} + +/// The address a memory operand names, and how many bytes it touches. +fn address(b: &mut Builder, m: &Mem, at: Addr) -> (Varnode, u8) { + let base = match m.base { + Some(r) if r.class == RegClass::Pc => Varnode::constant(at.get(), 8), + Some(r) => reg(r), + None => Varnode::constant(0, 8), + }; + let mut addr = base; + if let Some((ix, ext, shift)) = m.index { + let narrow = if matches!(ext, Extend::Uxtw | Extend::Sxtw) { + Varnode { size: 4, ..reg(ix) } + } else { + reg(ix) + }; + let signed = matches!(ext, Extend::Sxtw | Extend::Sxtx); + let widened = if narrow.size == 8 { + narrow + } else { + b.eval(if signed { Op::IntSExt } else { Op::IntZExt }, 8, &[narrow]) + }; + let scaled = if shift == 0 { + widened + } else { + b.eval( + Op::IntLeft, + 8, + &[widened, Varnode::constant(shift as u64, 1)], + ) + }; + addr = b.eval(Op::IntAdd, 8, &[addr, scaled]); + } + // A post-indexed access uses the base unmodified; the displacement is the + // writeback rather than part of the address. + if m.disp != 0 && m.mode != AddrMode::PostIndex { + addr = b.eval(Op::IntAdd, 8, &[addr, Varnode::constant(m.disp as u64, 8)]); + } + (addr, m.size.clamp(1, 16) as u8) +} + +/// Update the base register of a pre- or post-indexed access. +fn writeback(b: &mut Builder, m: &Mem) { + if m.mode == AddrMode::Offset { + return; + } + let Some(base) = m.base else { return }; + if base.class == RegClass::Pc || base.class == RegClass::Zr { + return; + } + let v = reg(base); + let updated = b.eval(Op::IntAdd, 8, &[v, Varnode::constant(m.disp as u64, 8)]); + b.emit(Op::Copy, Some(v), &[updated]); +} + +/// Say that a call left the caller-saved registers holding anything. +/// +/// Without this the dataflow believes whatever they held before the call, and +/// the decompiler prints it: a value the callee overwrote, read as if it had +/// survived. +fn clobber(b: &mut Builder) { + let abi = crate::abi::of(&e5r_core::Arch::AArch64); + for offset in &abi.caller_saved { + // Not the result register: the call itself defines that. + if *offset == gpr_offset(0) { + continue; + } + b.emit(Op::Undefine, Some(Varnode::register(*offset, 8)), &[]); + } + for flag in [flag_n(), flag_z(), flag_c(), flag_v()] { + b.emit(Op::Undefine, Some(flag), &[]); + } +} + +/// The four-bit encoding of a condition name. +fn cond_number(name: &str) -> Option { + const NAMES: [&str; 16] = [ + "eq", "ne", "cs", "cc", "mi", "pl", "vs", "vc", "hi", "ls", "ge", "lt", "gt", "le", "al", + "nv", + ]; + NAMES.iter().position(|n| *n == name).map(|i| i as u8) +} + +/// Lift one AArch64 instruction. +pub fn lift(i: &Insn) -> Lifted { + let mut b = Builder::new(i.addr); + let ops = i.operands(); + let next = Varnode::constant(i.next().get(), 8); + + // The decoder's flow classification is authoritative; the mnemonic does + // not have to be consulted twice for control transfer. + match i.flow { + Flow::Branch(t) => { + b.emit(Op::Branch, None, &[Varnode::constant(t.get(), 8)]); + return b.finish(true); + } + Flow::CondBranch(t) => return cond_branch(b, i, ops, t), + Flow::Call(t) => { + b.emit(Op::Copy, Some(Varnode::register(LR, 8)), &[next]); + b.emit( + Op::Call, + Some(Varnode::register(gpr_offset(0), 8)), + &[Varnode::constant(t.get(), 8)], + ); + clobber(&mut b); + return b.finish(true); + } + Flow::IndirectCall => { + let Some(Operand::Reg(r)) = ops.first() else { + return b.unimplemented(); + }; + let target = reg(*r); + b.emit(Op::Copy, Some(Varnode::register(LR, 8)), &[next]); + b.emit( + Op::CallInd, + Some(Varnode::register(gpr_offset(0), 8)), + &[target], + ); + clobber(&mut b); + return b.finish(true); + } + Flow::Return => { + let link = match ops.first() { + Some(Operand::Reg(r)) => reg(*r), + _ => Varnode::register(LR, 8), + }; + b.emit(Op::Return, None, &[link]); + return b.finish(true); + } + Flow::IndirectBranch => { + let Some(Operand::Reg(r)) = ops.first() else { + return b.unimplemented(); + }; + b.emit(Op::BranchInd, None, &[reg(*r)]); + return b.finish(true); + } + // A trap raises an exception and changes no register on the way; a + // supervisor call leaves the result and the caller-saved registers to + // the kernel, which is not knowable from here, so they are undefined + // rather than guessed. Both flows already stop the interpreter. + Flow::Trap => return b.finish(true), + Flow::Syscall => { + let abi = crate::abi::of(&e5r_core::Arch::AArch64); + for offset in &abi.caller_saved { + b.emit(Op::Undefine, Some(Varnode::register(*offset, 8)), &[]); + } + for r in &abi.results { + b.emit(Op::Undefine, Some(Varnode::register(*r, 8)), &[]); + } + return b.finish(true); + } + Flow::Next => {} + } + + // Anything naming a vector register goes to the SIMD lifter, where a lane + // is just a varnode inside the register. + if crate::lift::neon::handles(i) { + return crate::lift::neon::lift(b, i); + } + + let dest = match ops.first() { + Some(Operand::Reg(r)) if r.class != RegClass::Vec => Some(*r), + _ => None, + }; + let size = dest.map(|d| reg(d).size).unwrap_or(8); + + match i.mnemonic { + "nop" | "hint" | "paciasp" | "autiasp" | "pacibsp" | "autibsp" | "bti" | "dmb" | "dsb" + | "isb" | "sb" | "clrex" | "yield" | "xpaclri" => b.finish(true), + + "mov" => { + let (Some(d), Some(src)) = (dest, ops.get(1)) else { + return b.unimplemented(); + }; + let Some(v) = source(&mut b, src, size) else { + return b.unimplemented(); + }; + write_reg(&mut b, d, v); + b.finish(true) + } + + "mvn" => { + let (Some(d), Some(src)) = (dest, ops.get(1)) else { + return b.unimplemented(); + }; + let Some(v) = source(&mut b, src, size) else { + return b.unimplemented(); + }; + let r = b.eval(Op::IntNot, size, &[v]); + write_reg(&mut b, d, r); + b.finish(true) + } + + "neg" | "negs" => { + let (Some(d), Some(src)) = (dest, ops.get(1)) else { + return b.unimplemented(); + }; + let Some(v) = source(&mut b, src, size) else { + return b.unimplemented(); + }; + let zero = Varnode::constant(0, size); + let r = b.eval(Op::IntSub, size, &[zero, v]); + if i.mnemonic == "negs" { + set_flags(&mut b, size, r, zero, v, true); + } + write_reg(&mut b, d, r); + b.finish(true) + } + + "add" | "adds" | "sub" | "subs" | "cmp" | "cmn" => { + let sub = matches!(i.mnemonic, "sub" | "subs" | "cmp"); + let flags = matches!(i.mnemonic, "adds" | "subs" | "cmp" | "cmn"); + let discards = matches!(i.mnemonic, "cmp" | "cmn"); + let (a_op, b_op, dst) = if discards { + (ops.first(), ops.get(1), None) + } else { + (ops.get(1), ops.get(2), dest) + }; + let (Some(a_op), Some(b_op)) = (a_op, b_op) else { + return b.unimplemented(); + }; + let size = operand_size(dst, a_op, size); + let (Some(x), Some(y)) = (source(&mut b, a_op, size), source(&mut b, b_op, size)) + else { + return b.unimplemented(); + }; + let r = b.eval(if sub { Op::IntSub } else { Op::IntAdd }, size, &[x, y]); + if flags { + set_flags(&mut b, size, r, x, y, sub); + } + if let Some(d) = dst { + write_reg(&mut b, d, r); + } + b.finish(true) + } + + "and" | "ands" | "orr" | "eor" | "bic" | "bics" | "orn" | "eon" | "tst" => { + let discards = i.mnemonic == "tst"; + let (a_op, b_op, dst) = if discards { + (ops.first(), ops.get(1), None) + } else { + (ops.get(1), ops.get(2), dest) + }; + let (Some(a_op), Some(b_op)) = (a_op, b_op) else { + return b.unimplemented(); + }; + let size = operand_size(dst, a_op, size); + let (Some(x), Some(mut y)) = (source(&mut b, a_op, size), source(&mut b, b_op, size)) + else { + return b.unimplemented(); + }; + if matches!(i.mnemonic, "bic" | "bics" | "orn" | "eon") { + y = b.eval(Op::IntNot, size, &[y]); + } + let op = match i.mnemonic { + "and" | "ands" | "bic" | "bics" | "tst" => Op::IntAnd, + "orr" | "orn" => Op::IntOr, + _ => Op::IntXor, + }; + let r = b.eval(op, size, &[x, y]); + if matches!(i.mnemonic, "ands" | "bics" | "tst") { + set_logical_flags(&mut b, size, r); + } + if let Some(d) = dst { + write_reg(&mut b, d, r); + } + b.finish(true) + } + + "lsl" | "lsr" | "asr" | "ror" => { + let (Some(d), Some(a), Some(c)) = (dest, ops.get(1), ops.get(2)) else { + return b.unimplemented(); + }; + let (Some(x), Some(mut amount)) = (source(&mut b, a, size), source(&mut b, c, size)) + else { + return b.unimplemented(); + }; + // A variable shift uses only the low bits of the count. + if !amount.is_const() { + let mask = Varnode::constant(size as u64 * 8 - 1, size); + amount = b.eval(Op::IntAnd, size, &[amount, mask]); + } + let r = match i.mnemonic { + "lsl" => b.eval(Op::IntLeft, size, &[x, amount]), + "lsr" => b.eval(Op::IntRight, size, &[x, amount]), + "asr" => b.eval(Op::IntSRight, size, &[x, amount]), + _ => { + let bits = Varnode::constant(size as u64 * 8, size); + let right = b.eval(Op::IntRight, size, &[x, amount]); + let inv = b.eval(Op::IntSub, size, &[bits, amount]); + let left = b.eval(Op::IntLeft, size, &[x, inv]); + b.eval(Op::IntOr, size, &[right, left]) + } + }; + write_reg(&mut b, d, r); + b.finish(true) + } + + "mul" | "madd" | "msub" | "mneg" => { + let (Some(d), Some(a), Some(c)) = (dest, ops.get(1), ops.get(2)) else { + return b.unimplemented(); + }; + let (Some(x), Some(y)) = (source(&mut b, a, size), source(&mut b, c, size)) else { + return b.unimplemented(); + }; + let product = b.eval(Op::IntMul, size, &[x, y]); + let r = match (i.mnemonic, ops.get(3)) { + ("madd", Some(acc)) => { + let Some(a) = source(&mut b, acc, size) else { + return b.unimplemented(); + }; + b.eval(Op::IntAdd, size, &[a, product]) + } + ("msub", Some(acc)) => { + let Some(a) = source(&mut b, acc, size) else { + return b.unimplemented(); + }; + b.eval(Op::IntSub, size, &[a, product]) + } + ("mneg", _) => { + let zero = Varnode::constant(0, size); + b.eval(Op::IntSub, size, &[zero, product]) + } + _ => product, + }; + write_reg(&mut b, d, r); + b.finish(true) + } + + "smull" | "umull" => { + let (Some(d), Some(a), Some(c)) = (dest, ops.get(1), ops.get(2)) else { + return b.unimplemented(); + }; + let (Some(x), Some(y)) = (source(&mut b, a, 4), source(&mut b, c, 4)) else { + return b.unimplemented(); + }; + let signed = i.mnemonic.starts_with('s'); + let ext = if signed { Op::IntSExt } else { Op::IntZExt }; + let wx = b.eval(ext, 8, &[x]); + let wy = b.eval(ext, 8, &[y]); + let r = b.eval(Op::IntMul, 8, &[wx, wy]); + write_reg(&mut b, d, r); + b.finish(true) + } + + // The architecture defines division by zero as producing zero rather + // than trapping, so the IR says so instead of letting the interpreter + // stop. + "udiv" | "sdiv" => { + let (Some(d), Some(a), Some(c)) = (dest, ops.get(1), ops.get(2)) else { + return b.unimplemented(); + }; + let (Some(x), Some(y)) = (source(&mut b, a, size), source(&mut b, c, size)) else { + return b.unimplemented(); + }; + let zero = Varnode::constant(0, size); + let one = Varnode::constant(1, size); + let is_zero = b.eval(Op::IntEqual, 1, &[y, zero]); + // Divide by one when the divisor is zero, then mask the result + // away. Straight-line, so dataflow sees one definition. + let zx = b.eval(Op::IntZExt, size, &[is_zero]); + let denom = b.eval(Op::IntOr, size, &[y, zx]); + let q = b.eval( + if i.mnemonic == "udiv" { + Op::IntDiv + } else { + Op::IntSDiv + }, + size, + &[x, denom], + ); + let keep = b.eval(Op::IntSub, size, &[zx, one]); + let r = b.eval(Op::IntAnd, size, &[q, keep]); + write_reg(&mut b, d, r); + b.finish(true) + } + + "csel" | "csinc" | "csinv" | "csneg" | "cset" | "csetm" | "cinc" | "cinv" | "cneg" => { + conditional_select(b, i, ops, size) + } + + "sxtb" | "sxth" | "sxtw" | "uxtb" | "uxth" => { + let (Some(d), Some(Operand::Reg(s))) = (dest, ops.get(1)) else { + return b.unimplemented(); + }; + let narrow_size = match i.mnemonic { + "sxtb" | "uxtb" => 1, + "sxth" | "uxth" => 2, + _ => 4, + }; + let narrowed = narrow(&mut b, reg(*s), narrow_size); + let signed = i.mnemonic.starts_with('s'); + let r = b.eval( + if signed { Op::IntSExt } else { Op::IntZExt }, + size, + &[narrowed], + ); + write_reg(&mut b, d, r); + b.finish(true) + } + + "adr" | "adrp" => { + let (Some(d), Some(Operand::Addr(a))) = (dest, ops.get(1)) else { + return b.unimplemented(); + }; + write_reg(&mut b, d, Varnode::constant(a.get(), 8)); + b.finish(true) + } + + "ubfx" | "sbfx" => { + let (Some(d), Some(a), Some(Operand::Count(lsb)), Some(Operand::Count(width))) = + (dest, ops.get(1), ops.get(2), ops.get(3)) + else { + return b.unimplemented(); + }; + let Some(x) = source(&mut b, a, size) else { + return b.unimplemented(); + }; + let shifted = b.eval(Op::IntRight, size, &[x, Varnode::constant(*lsb as u64, 1)]); + let mask = if *width >= size as i64 * 8 { + u64::MAX + } else { + (1u64 << *width) - 1 + }; + let r = b.eval(Op::IntAnd, size, &[shifted, Varnode::constant(mask, size)]); + let r = if i.mnemonic == "sbfx" { + // Sign-extend from the extracted width by shifting up and back. + let up = size as u64 * 8 - *width as u64; + let left = b.eval(Op::IntLeft, size, &[r, Varnode::constant(up, 1)]); + b.eval(Op::IntSRight, size, &[left, Varnode::constant(up, 1)]) + } else { + r + }; + write_reg(&mut b, d, r); + b.finish(true) + } + + "ldr" | "ldrb" | "ldrh" | "ldrsb" | "ldrsh" | "ldrsw" | "ldur" | "ldurb" | "ldurh" + | "ldursb" | "ldursh" | "ldursw" => load(b, i, ops), + + "str" | "strb" | "strh" | "stur" | "sturb" | "sturh" => store(b, i, ops), + + "ldp" | "stp" | "ldpsw" | "ldnp" | "stnp" => pair(b, i, ops), + + // A prefetch has no architectural effect beyond timing. + "prfm" | "prfum" => b.finish(true), + + // The two that write the whole register rather than one halfword. + // `movz` places the immediate and zeroes the rest; `movn` places its + // complement and ones the rest. Both are constants at lift time, so + // neither needs the shift as an operation. + "movz" | "movn" => { + let Some(d) = dest else { + return b.unimplemented(); + }; + let v = match ops.get(1) { + Some(Operand::UImm(v)) => *v, + Some(Operand::Imm(v)) if *v >= 0 => *v as u64, + _ => return b.unimplemented(), + }; + let shift = match ops.get(2) { + Some(Operand::ShiftOp(_, n)) => *n as u64, + _ => 0, + }; + let placed = v << shift; + // Narrowed to the destination: a 32-bit `movn` complements within + // the word, and the upper half of the slot is written by + // `write_reg` rather than carried over from the complement. + let mask = if size >= 8 { + u64::MAX + } else { + (1u64 << (size * 8)) - 1 + }; + let value = if i.mnemonic == "movz" { + placed + } else { + !placed + } & mask; + write_reg(&mut b, d, Varnode::constant(value, size)); + b.finish(true) + } + + "movk" => { + let Some(d) = dest else { + return b.unimplemented(); + }; + let v = match ops.get(1) { + Some(Operand::UImm(v)) => *v, + Some(Operand::Imm(v)) if *v >= 0 => *v as u64, + _ => return b.unimplemented(), + }; + let shift = match ops.get(2) { + Some(Operand::ShiftOp(_, n)) => *n as u64, + _ => 0, + }; + // Keep the other halfwords and replace this one. + let keep = !(0xffffu64 << shift); + let old = reg(d); + let masked = b.eval(Op::IntAnd, size, &[old, Varnode::constant(keep, size)]); + let inserted = b.eval( + Op::IntOr, + size, + &[masked, Varnode::constant(v << shift, size)], + ); + write_reg(&mut b, d, inserted); + b.finish(true) + } + + "sbfiz" | "ubfiz" | "bfi" | "bfxil" => bitfield(b, i, ops, size), + + "ccmp" | "ccmn" => { + // A conditional compare either performs the comparison or writes + // the flags the instruction carries. Both sides are computed and + // selected, so the IR stays straight-line. + let (Some(a), Some(second), Some(Operand::Imm(nzcv)), Some(Operand::Cond(c))) = + (ops.first(), ops.get(1), ops.get(2), ops.get(3)) + else { + return b.unimplemented(); + }; + let size = match a { + Operand::Reg(r) => reg(*r).size, + _ => size, + }; + let (Some(x), Some(y)) = (source(&mut b, a, size), source(&mut b, second, size)) else { + return b.unimplemented(); + }; + let taken = condition(&mut b, c.0); + let sub = i.mnemonic == "ccmp"; + let r = b.eval(if sub { Op::IntSub } else { Op::IntAdd }, size, &[x, y]); + set_flags(&mut b, size, r, x, y, sub); + // When the condition failed, the carried flags win instead. + let not_taken = b.eval(Op::BoolNot, 1, &[taken]); + let flags = *nzcv as u64; + for (n, f) in [flag_n(), flag_z(), flag_c(), flag_v()] + .into_iter() + .enumerate() + { + let bit = (flags >> (3 - n)) & 1; + let carried = Varnode::constant(bit, 1); + let keep = b.eval(Op::IntAnd, 1, &[f, taken]); + let replace = b.eval(Op::IntAnd, 1, &[carried, not_taken]); + let merged = b.eval(Op::IntOr, 1, &[keep, replace]); + b.emit(Op::Copy, Some(f), &[merged]); + } + b.finish(true) + } + + "adc" | "adcs" | "sbc" | "sbcs" => { + let (Some(d), Some(a), Some(c)) = (dest, ops.get(1), ops.get(2)) else { + return b.unimplemented(); + }; + let (Some(x), Some(y)) = (source(&mut b, a, size), source(&mut b, c, size)) else { + return b.unimplemented(); + }; + let carry = b.eval(Op::IntZExt, size, &[flag_c()]); + let sub = i.mnemonic.starts_with("sbc"); + let r = if sub { + // With borrow: x - y - 1 + carry. + let d1 = b.eval(Op::IntSub, size, &[x, y]); + let d2 = b.eval(Op::IntAdd, size, &[d1, carry]); + b.eval(Op::IntSub, size, &[d2, Varnode::constant(1, size)]) + } else { + let s1 = b.eval(Op::IntAdd, size, &[x, y]); + b.eval(Op::IntAdd, size, &[s1, carry]) + }; + if i.mnemonic.ends_with('s') { + set_flags(&mut b, size, r, x, y, sub); + } + write_reg(&mut b, d, r); + b.finish(true) + } + + "smulh" | "umulh" => { + let (Some(d), Some(x), Some(y)) = (dest, ops.get(1), ops.get(2)) else { + return b.unimplemented(); + }; + let (Some(a), Some(c)) = (source(&mut b, x, 8), source(&mut b, y, 8)) else { + return b.unimplemented(); + }; + let op = if i.mnemonic == "smulh" { + Op::IntSMulHigh + } else { + Op::IntMulHigh + }; + let r = b.eval(op, 8, &[a, c]); + write_reg(&mut b, d, r); + b.finish(true) + } + + // The widening multiply-accumulates: two 32-bit sources, a 64-bit + // accumulator, and a product that needs the full width. + "umaddl" | "smaddl" | "umsubl" | "smsubl" | "umnegl" | "smnegl" => { + let (Some(d), Some(Operand::Reg(x)), Some(Operand::Reg(y))) = + (dest, ops.get(1), ops.get(2)) + else { + return b.unimplemented(); + }; + let signed = i.mnemonic.starts_with('s'); + let ext = if signed { Op::IntSExt } else { Op::IntZExt }; + let a = b.eval(ext, 8, &[Varnode { size: 4, ..reg(*x) }]); + let c = b.eval(ext, 8, &[Varnode { size: 4, ..reg(*y) }]); + let product = b.eval(Op::IntMul, 8, &[a, c]); + let r = match ops.get(3) { + Some(acc) => { + let Some(base) = source(&mut b, acc, 8) else { + return b.unimplemented(); + }; + let op = if i.mnemonic.ends_with("subl") { + Op::IntSub + } else { + Op::IntAdd + }; + b.eval(op, 8, &[base, product]) + } + // The negating forms have no accumulator. + None if i.mnemonic.ends_with("negl") => b.eval(Op::IntNegate, 8, &[product]), + None => product, + }; + write_reg(&mut b, d, r); + b.finish(true) + } + + "clz" | "rbit" | "rev" | "rev16" | "rev32" | "cls" => { + let (Some(d), Some(Operand::Reg(s))) = (dest, ops.get(1)) else { + return b.unimplemented(); + }; + let x = reg(*s); + match i.mnemonic { + "clz" => { + let r = b.eval(Op::LzCount, size, &[x]); + write_reg(&mut b, d, r); + b.finish(true) + } + // No single IR operation reverses bytes or bits, but both are + // exactly expressible in the ones there are, so they are + // written out rather than left unmodelled. A lane is the width + // the mnemonic names: `rev` reverses the whole register, + // `rev32` each word of a 64-bit one, `rev16` each halfword. + "rev" | "rev16" | "rev32" => { + let lane = match i.mnemonic { + "rev16" => 2, + "rev32" => 4, + _ => size, + }; + let r = swap_bytes(&mut b, x, size, lane); + write_reg(&mut b, d, r); + b.finish(true) + } + "rbit" => { + let r = reverse_bits(&mut b, x, size); + write_reg(&mut b, d, r); + b.finish(true) + } + _ => b.unimplemented(), + } + } + + // A system register holds something the processor knows and the + // program did not compute, so the destination becomes undefined: that + // is exactly what the operation does to the dataflow, and it is + // complete rather than a gap. A write to one leaves this model's state + // alone, so there is nothing to say about it. + "mrs" => { + let Some(d) = dest else { + return b.unimplemented(); + }; + b.emit(Op::Undefine, Some(reg(d)), &[]); + b.finish(true) + } + + "extr" | "ror_extr" => { + let (Some(d), Some(a), Some(c), Some(Operand::Count(lsb))) = + (dest, ops.get(1), ops.get(2), ops.get(3)) + else { + return b.unimplemented(); + }; + let (Some(hi), Some(lo)) = (source(&mut b, a, size), source(&mut b, c, size)) else { + return b.unimplemented(); + }; + let bits = size as u64 * 8; + let right = b.eval(Op::IntRight, size, &[lo, Varnode::constant(*lsb as u64, 1)]); + let r = if *lsb as u64 == 0 { + right + } else { + let left = b.eval( + Op::IntLeft, + size, + &[hi, Varnode::constant(bits - *lsb as u64, 1)], + ); + b.eval(Op::IntOr, size, &[right, left]) + }; + write_reg(&mut b, d, r); + b.finish(true) + } + + _ => b.unimplemented(), + } +} + +/// The insert and extract bitfield forms. +fn bitfield(mut b: Builder, i: &Insn, ops: &[Operand], size: u8) -> Lifted { + let (Some(Operand::Reg(d)), Some(src), Some(Operand::Count(a)), Some(Operand::Count(width))) = + (ops.first(), ops.get(1), ops.get(2), ops.get(3)) + else { + return b.unimplemented(); + }; + let Some(x) = source(&mut b, src, size) else { + return b.unimplemented(); + }; + let bits = size as u64 * 8; + let w = (*width as u64).min(bits); + let field_mask = if w >= 64 { u64::MAX } else { (1u64 << w) - 1 }; + + match i.mnemonic { + // Take the low `width` bits and place them at `lsb`, zeroing the rest. + "sbfiz" | "ubfiz" => { + let lsb = *a as u64; + let masked = b.eval(Op::IntAnd, size, &[x, Varnode::constant(field_mask, size)]); + let placed = b.eval(Op::IntLeft, size, &[masked, Varnode::constant(lsb, 1)]); + let r = if i.mnemonic == "sbfiz" { + // Sign-extend from the top of the placed field. + let up = bits.saturating_sub(lsb + w); + let left = b.eval(Op::IntLeft, size, &[placed, Varnode::constant(up, 1)]); + b.eval(Op::IntSRight, size, &[left, Varnode::constant(up, 1)]) + } else { + placed + }; + write_reg(&mut b, *d, r); + b.finish(true) + } + // Place the field into the destination, leaving the other bits alone. + "bfi" => { + let lsb = *a as u64; + let masked = b.eval(Op::IntAnd, size, &[x, Varnode::constant(field_mask, size)]); + let placed = b.eval(Op::IntLeft, size, &[masked, Varnode::constant(lsb, 1)]); + let hole = Varnode::constant(!(field_mask << lsb), size); + let old = reg(*d); + let cleared = b.eval(Op::IntAnd, size, &[old, hole]); + let r = b.eval(Op::IntOr, size, &[cleared, placed]); + write_reg(&mut b, *d, r); + b.finish(true) + } + // Take a field from the source's low bits into the destination's. + "bfxil" => { + let lsb = *a as u64; + let shifted = b.eval(Op::IntRight, size, &[x, Varnode::constant(lsb, 1)]); + let masked = b.eval( + Op::IntAnd, + size, + &[shifted, Varnode::constant(field_mask, size)], + ); + let old = reg(*d); + let cleared = b.eval( + Op::IntAnd, + size, + &[old, Varnode::constant(!field_mask, size)], + ); + let r = b.eval(Op::IntOr, size, &[cleared, masked]); + write_reg(&mut b, *d, r); + b.finish(true) + } + _ => b.unimplemented(), + } +} + +/// The operand size, taken from the destination when there is one and from the +/// first source when the result is discarded. +fn operand_size(dst: Option, a: &Operand, fallback: u8) -> u8 { + match (dst, a) { + (Some(d), _) => reg(d).size, + (None, Operand::Reg(r)) => reg(*r).size, + _ => fallback, + } +} + +fn cond_branch(mut b: Builder, i: &Insn, ops: &[Operand], target: Addr) -> Lifted { + let cond = match i.mnemonic { + m if m.starts_with("b.") => { + let Some(c) = cond_number(&m[2..]) else { + return b.unimplemented(); + }; + condition(&mut b, c) + } + "cbz" | "cbnz" => { + let Some(Operand::Reg(r)) = ops.first() else { + return b.unimplemented(); + }; + let v = reg(*r); + let zero = Varnode::constant(0, v.size); + let op = if i.mnemonic == "cbz" { + Op::IntEqual + } else { + Op::IntNotEqual + }; + b.eval(op, 1, &[v, zero]) + } + "tbz" | "tbnz" => { + let (Some(Operand::Reg(r)), Some(Operand::Count(bit))) = (ops.first(), ops.get(1)) + else { + return b.unimplemented(); + }; + let v = reg(*r); + let shifted = b.eval( + Op::IntRight, + v.size, + &[v, Varnode::constant(*bit as u64, 1)], + ); + let masked = b.eval(Op::IntAnd, v.size, &[shifted, Varnode::constant(1, v.size)]); + let zero = Varnode::constant(0, v.size); + let op = if i.mnemonic == "tbz" { + Op::IntEqual + } else { + Op::IntNotEqual + }; + b.eval(op, 1, &[masked, zero]) + } + _ => return b.unimplemented(), + }; + b.emit( + Op::CBranch, + None, + &[Varnode::constant(target.get(), 8), cond], + ); + b.finish(true) +} + +/// A conditional select, lifted without a branch: both values are masked and +/// combined, so the IR stays straight-line and dataflow sees one definition. +fn conditional_select(mut b: Builder, i: &Insn, ops: &[Operand], size: u8) -> Lifted { + let Some(Operand::Reg(d)) = ops.first() else { + return b.unimplemented(); + }; + let Some(cond_num) = ops.iter().rev().find_map(|o| match o { + Operand::Cond(c) => Some(c.0), + _ => None, + }) else { + return b.unimplemented(); + }; + let taken = condition(&mut b, cond_num); + + // The three-operand forms select the plain value when the condition holds + // and the transformed one when it does not. The two-operand aliases are + // the other way round: `cneg Rd, Rn, lt` negates *when* it is less than, + // because the alias hides an inverted condition. Getting this backwards + // turns an absolute value into a negation, which is what it did. + let alias = matches!(i.mnemonic, "cinc" | "cinv" | "cneg"); + let (plain, to_transform) = match i.mnemonic { + "cset" | "csetm" => ( + Varnode::constant(0, size), + Varnode::constant(if i.mnemonic == "cset" { 1 } else { u64::MAX }, size), + ), + _ if alias => { + let Some(src) = ops.get(1).and_then(|o| source(&mut b, o, size)) else { + return b.unimplemented(); + }; + (src, src) + } + _ => { + let (Some(x), Some(y)) = ( + ops.get(1).and_then(|o| source(&mut b, o, size)), + ops.get(2).and_then(|o| source(&mut b, o, size)), + ) else { + return b.unimplemented(); + }; + (x, y) + } + }; + + let transformed = match i.mnemonic { + "csinc" | "cinc" => b.eval( + Op::IntAdd, + size, + &[to_transform, Varnode::constant(1, size)], + ), + "csinv" | "cinv" => b.eval(Op::IntNot, size, &[to_transform]), + "csneg" | "cneg" => { + let zero = Varnode::constant(0, size); + b.eval(Op::IntSub, size, &[zero, to_transform]) + } + // cset and csetm need no transform: the constant is already the value. + "cset" | "csetm" => to_transform, + _ => to_transform, + }; + + // Which side the condition picks. + let (yes, otherwise) = if alias || matches!(i.mnemonic, "cset" | "csetm") { + (transformed, plain) + } else { + (plain, transformed) + }; + + let zero = Varnode::constant(0, size); + let t = b.eval(Op::IntZExt, size, &[taken]); + let mask = b.eval(Op::IntSub, size, &[zero, t]); + let inv = b.eval(Op::IntNot, size, &[mask]); + let a = b.eval(Op::IntAnd, size, &[yes, mask]); + let c = b.eval(Op::IntAnd, size, &[otherwise, inv]); + let r = b.eval(Op::IntOr, size, &[a, c]); + write_reg(&mut b, *d, r); + b.finish(true) +} + +fn load(mut b: Builder, i: &Insn, ops: &[Operand]) -> Lifted { + let (Some(Operand::Reg(d)), Some(second)) = (ops.first(), ops.get(1)) else { + return b.unimplemented(); + }; + // A value wider than the IR carries is moved in eight-byte pieces rather + // than truncated. + if d.class == RegClass::Vec && width_bytes(d.width) > 8 { + let addr = match second { + Operand::Addr(a) => Varnode::constant(a.get(), 8), + Operand::Mem(m) => address(&mut b, m, i.addr).0, + _ => return b.unimplemented(), + }; + crate::lift::neon::wide_transfer(&mut b, addr, d.num, 16, false); + if let Operand::Mem(m) = second { + writeback(&mut b, m); + } + return b.finish(true); + } + let out_size = reg(*d).size; + let (addr, access) = match second { + Operand::Addr(a) => (Varnode::constant(a.get(), 8), out_size), + Operand::Mem(m) => address(&mut b, m, i.addr), + _ => return b.unimplemented(), + }; + let loaded = b.temp(access); + b.emit(Op::Load, Some(loaded), &[addr]); + let signed = matches!( + i.mnemonic, + "ldrsb" | "ldrsh" | "ldrsw" | "ldursb" | "ldursh" | "ldursw" + ); + let value = if access == out_size { + loaded + } else { + b.eval( + if signed { Op::IntSExt } else { Op::IntZExt }, + out_size, + &[loaded], + ) + }; + write_reg(&mut b, *d, value); + if let Operand::Mem(m) = second { + writeback(&mut b, m); + } + b.finish(true) +} + +fn store(mut b: Builder, i: &Insn, ops: &[Operand]) -> Lifted { + let (Some(Operand::Reg(s)), Some(Operand::Mem(m))) = (ops.first(), ops.get(1)) else { + return b.unimplemented(); + }; + if s.class == RegClass::Vec && width_bytes(s.width) > 8 { + let (addr, _) = address(&mut b, m, i.addr); + crate::lift::neon::wide_transfer(&mut b, addr, s.num, 16, true); + writeback(&mut b, m); + return b.finish(true); + } + let (addr, access) = address(&mut b, m, i.addr); + let src = reg(*s); + // A narrowing store writes the low bytes of the register. + let value = narrow(&mut b, src, access.min(src.size)); + b.emit(Op::Store, None, &[addr, value]); + writeback(&mut b, m); + b.finish(true) +} + +fn pair(mut b: Builder, i: &Insn, ops: &[Operand]) -> Lifted { + let (Some(Operand::Reg(a)), Some(Operand::Reg(c)), Some(Operand::Mem(m))) = + (ops.first(), ops.get(1), ops.get(2)) + else { + return b.unimplemented(); + }; + if width_bytes(a.width) > 8 || width_bytes(c.width) > 8 { + // A pair of vector registers, moved in eight-byte pieces. + if a.class != RegClass::Vec || c.class != RegClass::Vec { + return b.unimplemented(); + } + let (addr, _) = address(&mut b, m, i.addr); + let storing = i.mnemonic.starts_with('s'); + crate::lift::neon::wide_transfer(&mut b, addr, a.num, 16, storing); + let second = b.eval(Op::IntAdd, 8, &[addr, Varnode::constant(16, 8)]); + crate::lift::neon::wide_transfer(&mut b, second, c.num, 16, storing); + writeback(&mut b, m); + return b.finish(true); + } + let (addr, total) = address(&mut b, m, i.addr); + let each = (total / 2).max(1); + let second = b.eval(Op::IntAdd, 8, &[addr, Varnode::constant(each as u64, 8)]); + // Every loading form starts with `ld`: `ldpsw` and `ldnp` are loads too, + // and treating them as stores would write memory the program only reads. + if i.mnemonic.starts_with("ld") { + // `ldpsw` loads two words and sign-extends each to a doubleword. + let widen = i.mnemonic == "ldpsw"; + for (slot, at) in [(*a, addr), (*c, second)] { + let loaded = b.temp(each); + b.emit(Op::Load, Some(loaded), &[at]); + let value = if widen { + b.eval(Op::IntSExt, 8, &[loaded]) + } else { + loaded + }; + write_reg(&mut b, slot, value); + } + } else { + b.emit(Op::Store, None, &[addr, reg(*a)]); + b.emit(Op::Store, None, &[second, reg(*c)]); + } + writeback(&mut b, m); + b.finish(true) +} diff --git a/crates/e5r-ir/src/lift/arm.rs b/crates/e5r-ir/src/lift/arm.rs new file mode 100644 index 0000000..7fc2e67 --- /dev/null +++ b/crates/e5r-ir/src/lift/arm.rs @@ -0,0 +1,1318 @@ +//! ARM A32 and Thumb-2 lifting. +//! +//! Covers the integer instructions a compiler emits: data processing through +//! the barrel shifter with its flag effects, multiplies, loads and stores in +//! every addressing mode, the load and store multiple forms, extensions, +//! bitfields and branches. Floating point and SIMD are not modelled and say so +//! rather than approximating. +//! +//! Two things this architecture does that need saying explicitly. +//! +//! Almost every instruction is predicated, and the condition is part of the +//! mnemonic rather than an operand: `sublt` is one spelling of `sub`. The IR +//! has no branch within an instruction, so a predicated instruction that +//! writes a register computes its result unconditionally and then selects +//! between it and the old value, which is exactly what the machine's result +//! is. A predicated store cannot be written that way -- a store that must not +//! happen is not a store of the old value, because the address may not be +//! readable -- so those are declined rather than approximated. +//! +//! The barrel shifter sets the carry flag on its way past, and the value it +//! sets it to is not the carry out of the arithmetic. `movs r0, r1, lsl #2` +//! leaves C holding bit 30 of `r1`. Code that chains `adcs` reads that, so it +//! is emitted rather than left alone. + +use e5r_arch::{AddrMode, Flow, Insn, Mem, Operand, Reg, RegClass, Shift, Width}; +use e5r_core::Addr; + +use crate::lift::{Builder, Lifted}; +use crate::op::{Op, Space, Varnode}; + +/// Byte offset of `r0`. +/// +/// The registers are four bytes on this machine and are spaced eight apart +/// anyway, because dataflow versions the register file in eight-byte units: +/// two registers sharing a slot would let a write to one clobber the other. +/// The upper half of each slot is not storage the architecture has, so every +/// write zeroes it and every read takes the low four bytes -- the same shape +/// AArch64's `w` registers already have. +const R_BASE: u64 = 0; +/// `sp`, which is register 13. +const SP: u64 = R_BASE + 13 * 8; +/// `lr`, which is register 14. +const LR: u64 = R_BASE + 14 * 8; +/// The four condition flags, one byte each, above the registers. +const N: u64 = 16 * 8; +const Z: u64 = N + 1; +const C: u64 = N + 2; +const V: u64 = N + 3; +/// Byte offset of `d0`; the VFP registers are eight bytes each. +const V_BASE: u64 = 18 * 8; + +/// The byte offset of a core register, for a caller setting up a machine +/// before running lifted code. +pub fn gpr_offset(n: u8) -> u64 { + R_BASE + (n & 15) as u64 * 8 +} + +/// The stack pointer's offset. +pub fn sp_offset() -> u64 { + SP +} + +/// The link register's offset. +pub fn lr_offset() -> u64 { + LR +} + +/// The program counter, which is not storage here: it is only ever read, and +/// the decoder has already resolved what it reads as. +pub fn pc_offset() -> u64 { + R_BASE + 15 * 8 +} + +/// The byte offset of a VFP register. +pub fn vec_offset(n: u8) -> u64 { + V_BASE + n as u64 * 8 +} + +/// The negative flag. +pub fn flag_n() -> Varnode { + Varnode::register(N, 1) +} +/// The zero flag. +pub fn flag_z() -> Varnode { + Varnode::register(Z, 1) +} +/// The carry flag. +pub fn flag_c() -> Varnode { + Varnode::register(C, 1) +} +/// The overflow flag. +pub fn flag_v() -> Varnode { + Varnode::register(V, 1) +} + +/// The condition codes, in encoding order, as they are spelled. +/// +/// `al` is index 14 and prints as nothing at all, which is why a mnemonic with +/// no suffix is unconditional rather than unparsed. +const CONDS: [&str; 15] = [ + "eq", "ne", "hs", "lo", "mi", "pl", "vs", "vc", "hi", "ls", "ge", "lt", "gt", "le", "al", +]; + +/// Every base mnemonic this lifter knows, longest first. +/// +/// The condition is glued onto the end of the mnemonic and some base names end +/// in the same two letters a condition does -- `muls` ends in `ls`, `movs` in +/// `vs`, `teq` in `eq`. Splitting is therefore not "strip two characters": a +/// split is accepted only when what is left is a name on this list, which is +/// what makes it unambiguous. +const BASES: &[&str] = &[ + "adc", "add", "and", "asr", "bfc", "bfi", "bic", "bkpt", "bl", "blx", "bx", "b", "cbnz", "cbz", + "clz", "cmn", "cmp", "eor", "lsl", "lsr", "mla", "mls", "mov", "movt", "movw", "mul", "mvn", + "neg", "nop", "orn", "orr", "pop", "push", "rbit", "rev", "rev16", "revsh", "ror", "rrx", + "rsb", "rsc", "sbc", "sbfx", "sdiv", "smlal", "smull", "sub", "sxtb", "sxth", "teq", "tst", + "ubfx", "udiv", "umlal", "umull", "uxtb", "uxtb16", "uxth", + // The loads and stores, which are the rest of what a compiler emits. + "ldr", "ldrb", "ldrd", "ldrh", "ldrsb", "ldrsh", "str", "strb", "strd", "strh", + // The load and store multiple forms, whose suffix is the direction the + // address moves and whether it moves before or after each transfer. + "ldm", "ldmda", "ldmdb", "ldmib", "stm", "stmda", "stmdb", "stmib", +]; + +/// A mnemonic split into what it does, whether it sets the flags, and when it +/// runs. +struct Parts { + base: &'static str, + flags: bool, + /// The condition's encoding, 14 for `al`. + cond: u8, +} + +/// Split a mnemonic into its base, its `s` and its condition. +/// +/// Returns `None` for anything not on [`BASES`], which is how an instruction +/// this lifter does not model reaches [`Builder::unimplemented`] rather than +/// being guessed at. +fn parts(mnemonic: &str) -> Option { + // Thumb spells the wide and narrow encodings of one instruction `add.w` + // and `add.n`. Which encoding it is changes nothing this lifter models -- + // the operands say the rest -- so the qualifier comes off first. + let mnemonic = mnemonic + .strip_suffix(".w") + .or_else(|| mnemonic.strip_suffix(".n")) + .unwrap_or(mnemonic); + // `it` through `itttt`: the letters after the first two say how many of + // the instructions that follow run under the condition and how many under + // its inverse, and there are fifteen spellings. The shape is the rule, so + // it is written as the rule rather than as fifteen table entries. + if let Some(rest) = mnemonic.strip_prefix("it") + && rest.len() < 4 + && rest.bytes().all(|b| b == b't' || b == b'e') + { + return Some(Parts { + base: "it", + flags: false, + cond: 14, + }); + } + // Every candidate split, unconditional first so `teq` is `teq` and not + // `t` + `eq`. + for (cond, tail) in + std::iter::once((14u8, "")).chain(CONDS.iter().enumerate().map(|(n, c)| (n as u8, *c))) + { + let Some(head) = mnemonic.strip_suffix(tail) else { + continue; + }; + for (flags, base) in [(false, head), (true, head.strip_suffix('s').unwrap_or(""))] { + if base.is_empty() { + continue; + } + if let Some(found) = BASES.iter().find(|b| **b == base) { + return Some(Parts { + base: found, + flags, + cond, + }); + } + } + } + None +} + +/// The varnode for a register operand. +fn reg(r: Reg) -> Varnode { + match r.class { + RegClass::Gpr => Varnode::register(gpr_offset(r.num), 4), + RegClass::Sp => Varnode::register(SP, 4), + RegClass::Vec => Varnode::register(vec_offset(r.num), width_bytes(r.width)), + // The program counter is read through `Operand::Addr` and `Mem` with a + // pc base, both of which the decoder has already resolved. A bare `pc` + // operand elsewhere is a form this lifter does not model. + _ => Varnode::register(pc_offset(), 4), + } +} + +fn width_bytes(w: Width) -> u8 { + match w { + Width::W8 => 1, + Width::W16 => 2, + Width::W32 => 4, + Width::W64 => 8, + Width::W128 => 16, + } +} + +/// True when an operand names a register this lifter can write. +fn writable(op: &Operand) -> Option { + match op { + Operand::Reg(r) if matches!(r.class, RegClass::Gpr | RegClass::Sp) => Some(*r), + _ => None, + } +} + +/// Apply the barrel shifter, returning the value and the carry it leaves. +/// +/// The carry is `None` when the shifter does not touch it, which is a shift of +/// zero and only that: every other amount writes it, including the `lsr #32` +/// and `asr #32` the encoding spells as zero. +fn shifter(b: &mut Builder, v: Varnode, sh: Shift, amount: u8) -> (Varnode, Option) { + let bits = Varnode::constant(amount as u64, 1); + match sh { + Shift::Lsl if amount == 0 => (v, None), + Shift::Lsl => { + // The bit shifted out last, which is bit 32-amount. + let out = b.eval( + Op::IntRight, + 4, + &[v, Varnode::constant(32 - amount as u64, 1)], + ); + let c = b.eval(Op::IntAnd, 4, &[out, Varnode::constant(1, 4)]); + let c = b.eval(Op::IntNotEqual, 1, &[c, Varnode::constant(0, 4)]); + (b.eval(Op::IntLeft, 4, &[v, bits]), Some(c)) + } + Shift::Lsr | Shift::Asr => { + let op = if sh == Shift::Lsr { + Op::IntRight + } else { + Op::IntSRight + }; + // The last bit shifted out is bit amount-1. + let out = b.eval( + Op::IntRight, + 4, + &[v, Varnode::constant(amount as u64 - 1, 1)], + ); + let c = b.eval(Op::IntAnd, 4, &[out, Varnode::constant(1, 4)]); + let c = b.eval(Op::IntNotEqual, 1, &[c, Varnode::constant(0, 4)]); + // A shift of 32 is out of range for the IR's shift, and the answer + // is zero or the sign, so it is written as what it means. + let value = if amount >= 32 { + if sh == Shift::Lsr { + Varnode::constant(0, 4) + } else { + b.eval(Op::IntSRight, 4, &[v, Varnode::constant(31, 1)]) + } + } else { + b.eval(op, 4, &[v, bits]) + }; + (value, Some(c)) + } + Shift::Ror if amount == 0 => { + // `ror #0` is `rrx`: a 33-bit rotate through the carry flag. + let low = b.eval(Op::IntRight, 4, &[v, Varnode::constant(1, 1)]); + let cin = b.eval(Op::IntZExt, 4, &[flag_c()]); + let top = b.eval(Op::IntLeft, 4, &[cin, Varnode::constant(31, 1)]); + let value = b.eval(Op::IntOr, 4, &[low, top]); + let c = b.eval(Op::IntAnd, 4, &[v, Varnode::constant(1, 4)]); + let c = b.eval(Op::IntNotEqual, 1, &[c, Varnode::constant(0, 4)]); + (value, Some(c)) + } + Shift::Ror => { + let n = amount as u64 % 32; + let right = b.eval(Op::IntRight, 4, &[v, Varnode::constant(n, 1)]); + let left = b.eval(Op::IntLeft, 4, &[v, Varnode::constant(32 - n, 1)]); + let value = b.eval(Op::IntOr, 4, &[right, left]); + let c = b.eval(Op::IntRight, 4, &[value, Varnode::constant(31, 1)]); + let c = b.eval(Op::IntNotEqual, 1, &[c, Varnode::constant(0, 4)]); + (value, Some(c)) + } + Shift::Msl => (v, None), + } +} + +/// Materialize a source operand, applying any shift. +/// +/// The second return is the carry the barrel shifter left, which a logical +/// operation with `s` writes and an arithmetic one ignores. +fn source(b: &mut Builder, op: &Operand) -> Option<(Varnode, Option)> { + Some(match op { + Operand::Reg(r) => (reg(*r), None), + Operand::Imm(v) => (Varnode::constant(*v as u64, 4), None), + Operand::UImm(v) => (Varnode::constant(*v, 4), None), + Operand::Count(v) => (Varnode::constant(*v as u64, 4), None), + Operand::Addr(a) => (Varnode::constant(a.get(), 4), None), + Operand::Shifted(r, sh, n) => { + let v = reg(*r); + let (value, c) = shifter(b, v, *sh, *n); + (value, c) + } + _ => return None, + }) +} + +/// Set the flags an arithmetic operation produces. +fn set_flags(b: &mut Builder, result: Varnode, x: Varnode, y: Varnode, sub: bool) { + set_nz(b, result); + // For a subtraction the carry flag means "no borrow", which is an unsigned + // comparison rather than a carry out. + let c = if sub { + b.eval(Op::IntLessEqual, 1, &[y, x]) + } else { + b.eval(Op::IntCarry, 1, &[x, y]) + }; + b.emit(Op::Copy, Some(flag_c()), &[c]); + let v = b.eval(if sub { Op::IntSBorrow } else { Op::IntSCarry }, 1, &[x, y]); + b.emit(Op::Copy, Some(flag_v()), &[v]); +} + +/// Set N and Z from a result, which every flag-setting form does the same way. +fn set_nz(b: &mut Builder, result: Varnode) { + let sign = b.eval(Op::IntRight, 4, &[result, Varnode::constant(31, 1)]); + let n = b.eval(Op::IntNotEqual, 1, &[sign, Varnode::constant(0, 4)]); + b.emit(Op::Copy, Some(flag_n()), &[n]); + let z = b.eval(Op::IntEqual, 1, &[result, Varnode::constant(0, 4)]); + b.emit(Op::Copy, Some(flag_z()), &[z]); +} + +/// Set the flags a logical operation produces: N and Z from the result, C from +/// the barrel shifter if it moved, and V untouched. +fn set_logical_flags(b: &mut Builder, result: Varnode, carry: Option) { + set_nz(b, result); + if let Some(c) = carry { + b.emit(Op::Copy, Some(flag_c()), &[c]); + } +} + +/// The condition a predicated instruction tests, as one byte. +/// +/// The same four-bit encoding AArch64 uses, and the same reading of it: the +/// low bit inverts the test the upper three select. +fn condition(b: &mut Builder, cond: u8) -> Varnode { + let (n, z, c, v) = (flag_n(), flag_z(), flag_c(), flag_v()); + let base = match cond >> 1 { + 0b000 => z, + 0b001 => c, + 0b010 => n, + 0b011 => v, + 0b100 => { + let nz = b.eval(Op::BoolNot, 1, &[z]); + b.eval(Op::BoolAnd, 1, &[c, nz]) + } + 0b101 => b.eval(Op::IntEqual, 1, &[n, v]), + 0b110 => { + let nz = b.eval(Op::BoolNot, 1, &[z]); + let ge = b.eval(Op::IntEqual, 1, &[n, v]); + b.eval(Op::BoolAnd, 1, &[nz, ge]) + } + _ => Varnode::constant(1, 1), + }; + if cond & 1 == 1 && cond >> 1 != 0b111 { + b.eval(Op::BoolNot, 1, &[base]) + } else { + base + } +} + +/// Write a register, selecting against its old value when predicated. +/// +/// The machine either writes the result or leaves the register alone, and +/// `old` is what it holds either way, so the select is the result and not an +/// approximation of it. +fn put(b: &mut Builder, p: &Parts, dest: Varnode, value: Varnode) { + if p.cond == 14 { + write_reg(b, dest, value); + return; + } + let cond = condition(b, p.cond); + let chosen = select(b, cond, value, dest, dest.size); + write_reg(b, dest, chosen); +} + +/// Write a core register, including the upper half that is not storage. +/// +/// Emitting the zero rather than assuming it is what makes the write cover the +/// whole eight-byte location: a four-byte write into it is a partial write, +/// and dataflow would then merge it with whatever the slot held. +fn write_reg(b: &mut Builder, dest: Varnode, value: Varnode) { + b.emit(Op::Copy, Some(dest), &[value]); + if dest.space == Space::Register && dest.size == 4 { + b.emit( + Op::Copy, + Some(Varnode::register(dest.offset + 4, 4)), + &[Varnode::constant(0, 4)], + ); + } +} + +/// `cond ? t : f`, which the IR has no opcode for. +/// +/// Negating a zero-or-one gives all zeroes or all ones, which is the mask. +fn select(b: &mut Builder, cond: Varnode, t: Varnode, f: Varnode, size: u8) -> Varnode { + let wide = if size == 1 { + cond + } else { + b.eval(Op::IntZExt, size, &[cond]) + }; + let mask = b.eval(Op::IntNegate, size, &[wide]); + let keep = b.eval(Op::IntAnd, size, &[t, mask]); + let inverse = b.eval(Op::IntNot, size, &[mask]); + let drop = b.eval(Op::IntAnd, size, &[f, inverse]); + b.eval(Op::IntOr, size, &[keep, drop]) +} + +/// The address a memory operand names, and the value the base ends up with. +/// +/// The second is `None` when the instruction has no writeback. +fn address(b: &mut Builder, m: &Mem, at: Addr, thumb: bool) -> Option<(Varnode, Option)> { + let base = match m.base { + // Reading the program counter gives the instruction's address plus + // eight in A32 and plus four in Thumb, and Thumb aligns it down to a + // word. The decoder leaves this to the lifter because the value + // depends on which instruction set the address holds. + Some(r) if r.class == RegClass::Pc => { + let pc = if thumb { + (at.get() + 4) & !3 + } else { + at.get() + 8 + }; + Varnode::constant(pc, 4) + } + Some(r) => reg(r), + None => Varnode::constant(0, 4), + }; + let mut offset = Varnode::constant(m.disp as u64, 4); + if let Some((ix, _, shift)) = m.index { + let v = reg(ix); + offset = if shift == 0 { + v + } else { + b.eval(Op::IntLeft, 4, &[v, Varnode::constant(shift as u64, 1)]) + }; + // A negative index is spelled by the displacement's sign, which the + // decoder keeps separate from the register. + if m.disp < 0 { + offset = b.eval(Op::IntSub, 4, &[Varnode::constant(0, 4), offset]); + } + } + let moved = b.eval(Op::IntAdd, 4, &[base, offset]); + Some(match m.mode { + AddrMode::Offset => (moved, None), + AddrMode::PreIndex => (moved, Some(moved)), + AddrMode::PostIndex => (base, Some(moved)), + }) +} + +/// Apply a memory operand's writeback to its base register. +fn writeback(b: &mut Builder, m: &Mem, updated: Option) { + let (Some(v), Some(r)) = (updated, m.base) else { + return; + }; + if matches!(r.class, RegClass::Gpr | RegClass::Sp) { + write_reg(b, reg(r), v); + } +} + +/// Undefine what a call is allowed to change. +/// +/// AAPCS: r0 to r3 and r12 are caller-saved, and so are the flags. The result +/// registers are written by the call operation itself. +fn clobber(b: &mut Builder) { + for n in [1u8, 2, 3, 12] { + b.emit(Op::Undefine, Some(Varnode::register(gpr_offset(n), 4)), &[]); + } + for f in [flag_n(), flag_z(), flag_c(), flag_v()] { + b.emit(Op::Undefine, Some(f), &[]); + } +} + +/// Lift one ARM instruction. +/// +/// `thumb` says which instruction set the address holds, which changes what +/// reading the program counter gives and nothing else this lifter models. +pub fn lift(i: &Insn) -> Lifted { + lift_mode(i, i.len == 2) +} + +/// The same, told explicitly which instruction set this is. +pub fn lift_mode(i: &Insn, thumb: bool) -> Lifted { + let mut b = Builder::sized(i.addr, 4); + let ops = i.operands(); + let Some(p) = parts(i.mnemonic) else { + return b.unimplemented(); + }; + + match i.flow { + Flow::Branch(t) => { + if p.cond == 14 { + b.emit(Op::Branch, None, &[Varnode::constant(t.get(), 4)]); + } else { + let cond = condition(&mut b, p.cond); + b.emit(Op::CBranch, None, &[Varnode::constant(t.get(), 4), cond]); + } + return b.finish(true); + } + Flow::CondBranch(t) => { + // `cbz` and `cbnz` test a register rather than the flags, and they + // are the only Thumb branches that do. + let cond = match p.base { + "cbz" | "cbnz" => { + let Some((v, _)) = ops.first().and_then(|o| source(&mut b, o)) else { + return b.unimplemented(); + }; + let op = if p.base == "cbz" { + Op::IntEqual + } else { + Op::IntNotEqual + }; + b.eval(op, 1, &[v, Varnode::constant(0, 4)]) + } + _ => condition(&mut b, p.cond), + }; + b.emit(Op::CBranch, None, &[Varnode::constant(t.get(), 4), cond]); + return b.finish(true); + } + Flow::Call(t) => { + // The return address, which `bl` and `blx` put in the link + // register. A Thumb return address has its low bit set, and the + // decoder's `next` is the plain address. + let ret = i.next().get() | u64::from(thumb); + write_reg(&mut b, Varnode::register(LR, 4), Varnode::constant(ret, 4)); + b.emit( + Op::Call, + Some(Varnode::register(gpr_offset(0), 4)), + &[Varnode::constant(t.get(), 4)], + ); + clobber(&mut b); + return b.finish(true); + } + Flow::IndirectCall => { + let Some((target, _)) = ops.first().and_then(|o| source(&mut b, o)) else { + return b.unimplemented(); + }; + let ret = i.next().get() | u64::from(thumb); + write_reg(&mut b, Varnode::register(LR, 4), Varnode::constant(ret, 4)); + b.emit( + Op::CallInd, + Some(Varnode::register(gpr_offset(0), 4)), + &[target], + ); + clobber(&mut b); + return b.finish(true); + } + Flow::Return => { + // Three spellings. `bx lr` returns through a register, + // `pop {.., pc}` restores the frame and returns through the last + // word it loads, and `ldr pc, [sp], #4` is that for one register. + // Each is the whole instruction and not just its flow. + if p.base == "pop" { + return stack_multiple(b, &p, ops); + } + if p.base.starts_with("ldr") { + return load_to_pc(b, i, &p, ops, thumb, true); + } + let Some((target, _)) = ops.first().and_then(|o| source(&mut b, o)) else { + return b.unimplemented(); + }; + b.emit(Op::Return, None, &[target]); + return b.finish(true); + } + Flow::IndirectBranch => { + // `ldr pc, [...]` branches to what it reads, not to the program + // counter: the operand names where the target goes, not what it is. + if p.base.starts_with("ldr") { + return load_to_pc(b, i, &p, ops, thumb, false); + } + let Some((target, _)) = ops.first().and_then(|o| source(&mut b, o)) else { + return b.unimplemented(); + }; + b.emit(Op::BranchInd, None, &[target]); + return b.finish(true); + } + Flow::Trap => return b.finish(true), + Flow::Syscall => { + // What the kernel does is not knowable from here, so the result + // and the caller-saved registers are undefined rather than + // guessed. The flow still stops the interpreter. + b.emit(Op::Undefine, Some(Varnode::register(gpr_offset(0), 4)), &[]); + return b.finish(true); + } + Flow::Next => {} + } + + match p.base { + // `it` says how the next few instructions are conditional and does + // nothing else. The decoder has already folded that into each of their + // mnemonics, which is where this lifter reads it, so there is nothing + // left for the instruction itself to do. + "nop" => b.finish(true), + "it" => b.finish(true), + "push" | "pop" => stack_multiple(b, &p, ops), + m if m.starts_with("ldm") || m.starts_with("stm") => block_transfer(b, &p, ops), + "add" | "sub" | "rsb" | "adc" | "sbc" | "rsc" | "and" | "eor" | "orr" | "orn" | "bic" + | "mul" | "sdiv" | "udiv" => arithmetic(b, &p, ops), + "mov" | "mvn" | "neg" | "movw" | "lsl" | "lsr" | "asr" | "ror" | "rrx" => moves(b, &p, ops), + "movt" => movt(b, &p, ops), + "cmp" | "cmn" | "tst" | "teq" => compare(b, &p, ops), + "mla" | "mls" => multiply_accumulate(b, &p, ops), + "umull" | "smull" | "umlal" | "smlal" => long_multiply(b, &p, ops), + "uxtb" | "uxth" | "sxtb" | "sxth" => extend(b, &p, ops), + "uxtb16" => uxtb16(b, &p, ops), + "clz" | "rev" | "rev16" | "revsh" | "rbit" => reverses(b, &p, ops), + "bfi" | "bfc" | "ubfx" | "sbfx" => bitfield(b, &p, ops), + _ => memory(b, i, &p, ops, thumb), + } +} + +/// `add`, `sub` and the rest of the two-source data-processing forms. +fn arithmetic(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let Some(dest) = ops.first().and_then(writable) else { + return b.unimplemented(); + }; + // Thumb's narrow encodings write back into the first source and name it + // once: `adds r0, r1` is `r0 = r0 + r1`. A32 always names three. + let (first, second) = match ops.len() { + 0 | 1 => return b.unimplemented(), + 2 => (ops.first(), ops.get(1)), + _ => (ops.get(1), ops.get(2)), + }; + let (Some(x), Some(y)) = ( + first.and_then(|o| source(&mut b, o)), + second.and_then(|o| source(&mut b, o)), + ) else { + return b.unimplemented(); + }; + let (x, _) = x; + let (y, carry) = y; + let logical = matches!(p.base, "and" | "eor" | "orr" | "orn" | "bic"); + let (value, kind) = match p.base { + "add" => (b.eval(Op::IntAdd, 4, &[x, y]), Some((x, y, false))), + "sub" => (b.eval(Op::IntSub, 4, &[x, y]), Some((x, y, true))), + "rsb" => (b.eval(Op::IntSub, 4, &[y, x]), Some((y, x, true))), + "and" => (b.eval(Op::IntAnd, 4, &[x, y]), None), + "eor" => (b.eval(Op::IntXor, 4, &[x, y]), None), + "orr" => (b.eval(Op::IntOr, 4, &[x, y]), None), + "orn" => { + let ny = b.eval(Op::IntNot, 4, &[y]); + (b.eval(Op::IntOr, 4, &[x, ny]), None) + } + "bic" => { + let ny = b.eval(Op::IntNot, 4, &[y]); + (b.eval(Op::IntAnd, 4, &[x, ny]), None) + } + "mul" => (b.eval(Op::IntMul, 4, &[x, y]), None), + "sdiv" => (b.eval(Op::IntSDiv, 4, &[x, y]), None), + "udiv" => (b.eval(Op::IntDiv, 4, &[x, y]), None), + // `adc` and `sbc` carry the flag in, so the flags they set are the + // flags of the whole three-input sum and not of the first addition. + // Writing that exactly needs a carry out of two additions, which this + // does not model, so the flag-setting forms are declined. + "adc" | "sbc" | "rsc" if p.flags => return b.unimplemented(), + "adc" => { + let cin = b.eval(Op::IntZExt, 4, &[flag_c()]); + let sum = b.eval(Op::IntAdd, 4, &[x, y]); + (b.eval(Op::IntAdd, 4, &[sum, cin]), None) + } + "sbc" | "rsc" => { + let (l, r) = if p.base == "sbc" { (x, y) } else { (y, x) }; + let cin = b.eval(Op::IntZExt, 4, &[flag_c()]); + let borrow = b.eval(Op::IntSub, 4, &[Varnode::constant(1, 4), cin]); + let diff = b.eval(Op::IntSub, 4, &[l, r]); + (b.eval(Op::IntSub, 4, &[diff, borrow]), None) + } + _ => return b.unimplemented(), + }; + if p.flags { + match kind { + Some((l, r, sub)) => set_flags(&mut b, value, l, r, sub), + None if logical => set_logical_flags(&mut b, value, carry), + // A multiply or a divide with `s` sets only N and Z. + None => set_nz(&mut b, value), + } + } + put(&mut b, p, reg(dest), value); + b.finish(true) +} + +/// `mov`, `mvn` and the shifts, which ARM spells as moves. +fn moves(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let Some(dest) = ops.first().and_then(writable) else { + return b.unimplemented(); + }; + let (value, carry) = match p.base { + "lsl" | "lsr" | "asr" | "ror" => { + // Thumb's narrow encoding shifts the destination in place and + // names it once: `rors r0, r1` is `r0 = r0 ror r1`. + let (value_at, amount_at) = if ops.len() > 2 { (1, 2) } else { (0, 1) }; + let Some((x, _)) = ops.get(value_at).and_then(|o| source(&mut b, o)) else { + return b.unimplemented(); + }; + let sh = match p.base { + "lsl" => Shift::Lsl, + "lsr" => Shift::Lsr, + "asr" => Shift::Asr, + _ => Shift::Ror, + }; + match ops.get(amount_at).and_then(shift_amount) { + Some(amount) => shifter(&mut b, x, sh, amount), + // A shift by a register. The amount is the low byte of it, and + // an amount of 32 or more gives zero for the logical shifts + // and the sign for the arithmetic one -- a rule the IR's shift + // does not have, so it is written as the mask it is. + None => { + let Some((rs, _)) = ops.get(amount_at).and_then(|o| source(&mut b, o)) else { + return b.unimplemented(); + }; + let value = register_shift(&mut b, x, sh, rs); + let carry = p + .flags + .then(|| register_shift_carry(&mut b, x, value, sh, rs)); + (value, carry) + } + } + } + "rrx" => { + let Some((x, _)) = ops.get(1).and_then(|o| source(&mut b, o)) else { + return b.unimplemented(); + }; + shifter(&mut b, x, Shift::Ror, 0) + } + _ => { + let Some((x, carry)) = ops.get(1).and_then(|o| source(&mut b, o)) else { + return b.unimplemented(); + }; + match p.base { + "mvn" => (b.eval(Op::IntNot, 4, &[x]), carry), + "neg" => (b.eval(Op::IntSub, 4, &[Varnode::constant(0, 4), x]), carry), + _ => (x, carry), + } + } + }; + if p.flags { + set_logical_flags(&mut b, value, carry); + } + put(&mut b, p, reg(dest), value); + b.finish(true) +} + +/// A shift by an amount held in a register. +/// +/// ARM takes the low byte of the amount and, unlike the IR's shift, defines +/// what happens past the width: the logical shifts give zero and the +/// arithmetic one gives the sign. `ror` takes the amount modulo 32 instead, +/// which needs no guard. +fn register_shift(b: &mut Builder, x: Varnode, sh: Shift, rs: Varnode) -> Varnode { + let amount = b.eval(Op::IntAnd, 4, &[rs, Varnode::constant(0xff, 4)]); + if sh == Shift::Ror { + let n = b.eval(Op::IntAnd, 4, &[amount, Varnode::constant(31, 4)]); + let right = b.eval(Op::IntRight, 4, &[x, n]); + let back = b.eval(Op::IntSub, 4, &[Varnode::constant(32, 4), n]); + // A rotate by zero would shift left by 32, which is out of range, so + // the second half is masked away in exactly that case. + let left = b.eval(Op::IntLeft, 4, &[x, back]); + let zero = b.eval(Op::IntEqual, 1, &[n, Varnode::constant(0, 4)]); + let left = select(b, zero, Varnode::constant(0, 4), left, 4); + return b.eval(Op::IntOr, 4, &[right, left]); + } + let wide = b.eval(Op::IntLessEqual, 1, &[Varnode::constant(32, 4), amount]); + let capped = select(b, wide, Varnode::constant(31, 4), amount, 4); + let value = b.eval( + match sh { + Shift::Lsl => Op::IntLeft, + Shift::Asr => Op::IntSRight, + _ => Op::IntRight, + }, + 4, + &[x, capped], + ); + // Past the width the logical shifts give zero; the arithmetic one has + // already given the sign, because a shift by 31 does. + if sh == Shift::Asr { + return value; + } + select(b, wide, Varnode::constant(0, 4), value, 4) +} + +/// The carry a shift by a register leaves. +/// +/// The bit that left the register last, which is at a position the amount +/// decides, so it is computed rather than selected from a table. An amount of +/// zero leaves the flag alone, which is the one case with no bit to report. +fn register_shift_carry( + b: &mut Builder, + x: Varnode, + value: Varnode, + sh: Shift, + rs: Varnode, +) -> Varnode { + let amount = b.eval(Op::IntAnd, 4, &[rs, Varnode::constant(0xff, 4)]); + let zero = b.eval(Op::IntEqual, 1, &[amount, Varnode::constant(0, 4)]); + if sh == Shift::Ror { + // A rotate keeps every bit, so the last one out is the top one in. + let top = b.eval(Op::IntRight, 4, &[value, Varnode::constant(31, 1)]); + let bit = b.eval(Op::IntNotEqual, 1, &[top, Varnode::constant(0, 4)]); + return select(b, zero, flag_c(), bit, 1); + } + // `lsl` pushes bit 32-n out of the top; the right shifts push bit n-1 out + // of the bottom. Past the width the logical shifts have pushed everything + // out and leave zero, and the arithmetic one leaves the sign. + let wide = b.eval(Op::IntLessEqual, 1, &[Varnode::constant(32, 4), amount]); + let at = match sh { + Shift::Lsl => b.eval(Op::IntSub, 4, &[Varnode::constant(32, 4), amount]), + _ => b.eval(Op::IntSub, 4, &[amount, Varnode::constant(1, 4)]), + }; + let at = select(b, wide, Varnode::constant(31, 4), at, 4); + let shifted = b.eval( + if sh == Shift::Asr { + Op::IntSRight + } else { + Op::IntRight + }, + 4, + &[x, at], + ); + let bit = b.eval(Op::IntAnd, 4, &[shifted, Varnode::constant(1, 4)]); + let bit = b.eval(Op::IntNotEqual, 1, &[bit, Varnode::constant(0, 4)]); + let past = if sh == Shift::Asr { + // The sign, which is what an arithmetic shift keeps producing. + bit + } else { + Varnode::constant(0, 1) + }; + let bit = select(b, wide, past, bit, 1); + select(b, zero, flag_c(), bit, 1) +} + +/// A shift amount, which is a count and not a register. +/// +/// A shift by a register reaches the lifter as a name rather than an operand, +/// so it is not modelled; a compiler emits it rarely and guessing would be +/// worse than saying so. +fn shift_amount(op: &Operand) -> Option { + match op { + Operand::Count(v) | Operand::Imm(v) => u8::try_from(*v).ok(), + Operand::UImm(v) => u8::try_from(*v).ok(), + _ => None, + } +} + +/// `movt`, which replaces the top halfword and keeps the bottom one. +fn movt(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let (Some(dest), Some((v, _))) = ( + ops.first().and_then(writable), + ops.get(1).and_then(|o| source(&mut b, o)), + ) else { + return b.unimplemented(); + }; + let d = reg(dest); + let kept = b.eval(Op::IntAnd, 4, &[d, Varnode::constant(0xffff, 4)]); + let top = b.eval(Op::IntLeft, 4, &[v, Varnode::constant(16, 1)]); + let value = b.eval(Op::IntOr, 4, &[kept, top]); + put(&mut b, p, d, value); + b.finish(true) +} + +/// `cmp`, `cmn`, `tst` and `teq`, which set the flags and write nothing. +fn compare(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let (Some((x, _)), Some((y, carry))) = ( + ops.first().and_then(|o| source(&mut b, o)), + ops.get(1).and_then(|o| source(&mut b, o)), + ) else { + return b.unimplemented(); + }; + // A predicated compare that does not run leaves the flags alone, and the + // select that would express that is four selects on four flags. Declined + // rather than written out, because nothing a compiler emits needs it. + if p.cond != 14 { + return b.unimplemented(); + } + match p.base { + "cmp" => { + let r = b.eval(Op::IntSub, 4, &[x, y]); + set_flags(&mut b, r, x, y, true); + } + "cmn" => { + let r = b.eval(Op::IntAdd, 4, &[x, y]); + set_flags(&mut b, r, x, y, false); + } + "tst" => { + let r = b.eval(Op::IntAnd, 4, &[x, y]); + set_logical_flags(&mut b, r, carry); + } + _ => { + let r = b.eval(Op::IntXor, 4, &[x, y]); + set_logical_flags(&mut b, r, carry); + } + } + b.finish(true) +} + +/// `mla` and `mls`: a multiply with an addend. +fn multiply_accumulate(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let (Some(dest), Some((x, _)), Some((y, _)), Some((a, _))) = ( + ops.first().and_then(writable), + ops.get(1).and_then(|o| source(&mut b, o)), + ops.get(2).and_then(|o| source(&mut b, o)), + ops.get(3).and_then(|o| source(&mut b, o)), + ) else { + return b.unimplemented(); + }; + let product = b.eval(Op::IntMul, 4, &[x, y]); + let value = if p.base == "mla" { + b.eval(Op::IntAdd, 4, &[a, product]) + } else { + b.eval(Op::IntSub, 4, &[a, product]) + }; + if p.flags { + set_nz(&mut b, value); + } + put(&mut b, p, reg(dest), value); + b.finish(true) +} + +/// `umull`, `smull` and the accumulating forms, which write two registers. +fn long_multiply(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let (Some(lo), Some(hi), Some((x, _)), Some((y, _))) = ( + ops.first().and_then(writable), + ops.get(1).and_then(writable), + ops.get(2).and_then(|o| source(&mut b, o)), + ops.get(3).and_then(|o| source(&mut b, o)), + ) else { + return b.unimplemented(); + }; + let signed = p.base.starts_with('s'); + let ext = if signed { Op::IntSExt } else { Op::IntZExt }; + let wx = b.eval(ext, 8, &[x]); + let wy = b.eval(ext, 8, &[y]); + let mut wide = b.eval(Op::IntMul, 8, &[wx, wy]); + if p.base.ends_with("lal") { + // The accumulating forms add the two destination registers, read as + // one 64-bit value, to the product. + let l = b.eval(Op::IntZExt, 8, &[reg(lo)]); + let h = b.eval(Op::IntZExt, 8, &[reg(hi)]); + let h = b.eval(Op::IntLeft, 8, &[h, Varnode::constant(32, 1)]); + let acc = b.eval(Op::IntOr, 8, &[l, h]); + wide = b.eval(Op::IntAdd, 8, &[wide, acc]); + } + let low = b.eval(Op::SubPiece, 4, &[wide, Varnode::constant(0, 1)]); + let high = b.eval(Op::SubPiece, 4, &[wide, Varnode::constant(4, 1)]); + if p.flags { + set_nz(&mut b, high); + } + put(&mut b, p, reg(lo), low); + put(&mut b, p, reg(hi), high); + b.finish(true) +} + +/// `uxtb` and friends, which widen a byte or a halfword with an optional +/// rotate first and an optional addend. +fn extend(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let (Some(dest), Some((x, _))) = ( + ops.first().and_then(writable), + ops.get(1).and_then(|o| source(&mut b, o)), + ) else { + return b.unimplemented(); + }; + let bytes = if p.base.ends_with('b') { 1 } else { 2 }; + let mask = if bytes == 1 { 0xffu64 } else { 0xffff }; + let low = b.eval(Op::IntAnd, 4, &[x, Varnode::constant(mask, 4)]); + let value = if p.base.starts_with('s') { + // Sign-extend by shifting the sign bit to the top and back. + let up = b.eval( + Op::IntLeft, + 4, + &[low, Varnode::constant(32 - bytes as u64 * 8, 1)], + ); + b.eval( + Op::IntSRight, + 4, + &[up, Varnode::constant(32 - bytes as u64 * 8, 1)], + ) + } else { + low + }; + put(&mut b, p, reg(dest), value); + b.finish(true) +} + +/// `uxtb16`, which keeps the low byte of each halfword and clears the rest. +fn uxtb16(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let (Some(dest), Some((x, _))) = ( + ops.first().and_then(writable), + ops.get(1).and_then(|o| source(&mut b, o)), + ) else { + return b.unimplemented(); + }; + let value = b.eval(Op::IntAnd, 4, &[x, Varnode::constant(0x00ff_00ff, 4)]); + put(&mut b, p, reg(dest), value); + b.finish(true) +} + +/// `bfi`, `bfc`, `ubfx` and `sbfx`. +fn bitfield(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let Some(dest) = ops.first().and_then(writable) else { + return b.unimplemented(); + }; + let d = reg(dest); + let value = match p.base { + "bfc" => { + let (Some(lsb), Some(width)) = ( + ops.get(1).and_then(shift_amount), + ops.get(2).and_then(shift_amount), + ) else { + return b.unimplemented(); + }; + let mask = field_mask(lsb, width); + b.eval(Op::IntAnd, 4, &[d, Varnode::constant(!mask, 4)]) + } + "bfi" => { + let (Some((x, _)), Some(lsb), Some(width)) = ( + ops.get(1).and_then(|o| source(&mut b, o)), + ops.get(2).and_then(shift_amount), + ops.get(3).and_then(shift_amount), + ) else { + return b.unimplemented(); + }; + let mask = field_mask(lsb, width); + let kept = b.eval(Op::IntAnd, 4, &[d, Varnode::constant(!mask, 4)]); + let moved = b.eval(Op::IntLeft, 4, &[x, Varnode::constant(lsb as u64, 1)]); + let cut = b.eval(Op::IntAnd, 4, &[moved, Varnode::constant(mask, 4)]); + b.eval(Op::IntOr, 4, &[kept, cut]) + } + _ => { + let (Some((x, _)), Some(lsb), Some(width)) = ( + ops.get(1).and_then(|o| source(&mut b, o)), + ops.get(2).and_then(shift_amount), + ops.get(3).and_then(shift_amount), + ) else { + return b.unimplemented(); + }; + // Shift the field to the top and back down, which is what makes + // the signed and unsigned forms one line apart. + let up = b.eval( + Op::IntLeft, + 4, + &[x, Varnode::constant(32 - (lsb + width) as u64, 1)], + ); + let down = if p.base == "sbfx" { + Op::IntSRight + } else { + Op::IntRight + }; + b.eval(down, 4, &[up, Varnode::constant(32 - width as u64, 1)]) + } + }; + put(&mut b, p, d, value); + b.finish(true) +} + +/// `clz` and the byte and bit reversals. +/// +/// No single IR operation reverses bytes or bits, but both are exactly +/// expressible in the ones there are, so they are written out rather than left +/// unmodelled. The helpers are the AArch64 lifter's, because the computation +/// is the same one and having it twice would be two chances to get it wrong. +fn reverses(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let (Some(dest), Some((x, _))) = ( + ops.first().and_then(writable), + ops.get(1).and_then(|o| source(&mut b, o)), + ) else { + return b.unimplemented(); + }; + let value = match p.base { + "clz" => b.eval(Op::LzCount, 4, &[x]), + "rev" => super::aarch64::swap_bytes(&mut b, x, 4, 4), + "rev16" => super::aarch64::swap_bytes(&mut b, x, 4, 2), + "rbit" => super::aarch64::reverse_bits(&mut b, x, 4), + // `revsh` reverses the low halfword's bytes and sign-extends it. + _ => { + let swapped = super::aarch64::swap_bytes(&mut b, x, 4, 2); + let low = b.eval(Op::IntAnd, 4, &[swapped, Varnode::constant(0xffff, 4)]); + let up = b.eval(Op::IntLeft, 4, &[low, Varnode::constant(16, 1)]); + b.eval(Op::IntSRight, 4, &[up, Varnode::constant(16, 1)]) + } + }; + put(&mut b, p, reg(dest), value); + b.finish(true) +} + +/// The mask of a bitfield `width` bits wide starting at `lsb`. +fn field_mask(lsb: u8, width: u8) -> u64 { + if width >= 32 { + return u64::MAX >> 32 << lsb; + } + ((1u64 << width) - 1) << lsb +} + +/// `push` and `pop`, which are the load and store multiple forms a compiler +/// writes for a frame. +fn stack_multiple(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let Some(Operand::Sys(mask)) = ops.first() else { + return b.unimplemented(); + }; + // Predicated, and there is no value to select against: declined. + if p.cond != 14 { + return b.unimplemented(); + } + let regs: Vec = (0..16u8).filter(|n| mask & (1 << n) != 0).collect(); + if regs.is_empty() { + return b.unimplemented(); + } + let sp = Varnode::register(SP, 4); + let total = regs.len() as u64 * 4; + if p.base == "push" { + // Registers go to descending addresses, lowest numbered lowest. + let base = b.eval(Op::IntSub, 4, &[sp, Varnode::constant(total, 4)]); + for (n, r) in regs.iter().enumerate() { + let at = b.eval(Op::IntAdd, 4, &[base, Varnode::constant(n as u64 * 4, 4)]); + b.emit(Op::Store, None, &[at, Varnode::register(gpr_offset(*r), 4)]); + } + write_reg(&mut b, sp, base); + return b.finish(true); + } + let mut returned = None; + for (n, r) in regs.iter().enumerate() { + let at = b.eval(Op::IntAdd, 4, &[sp, Varnode::constant(n as u64 * 4, 4)]); + let v = b.eval(Op::Load, 4, &[at]); + if *r == 15 { + // Popping the program counter is the return, and it is the last + // register in the list because the list is in register order. + returned = Some(v); + } else { + write_reg(&mut b, Varnode::register(gpr_offset(*r), 4), v); + } + } + let raised = b.eval(Op::IntAdd, 4, &[sp, Varnode::constant(total, 4)]); + write_reg(&mut b, sp, raised); + if let Some(target) = returned { + b.emit(Op::Return, None, &[target]); + } + b.finish(true) +} + +/// `ldm` and `stm`: a run of registers moved to or from consecutive addresses. +/// +/// The suffix says where the first one goes relative to the base: `ia` (which +/// is the bare name) counts up from it, `ib` up from one word past, `da` down +/// and `db` down from one word before. Whichever it is, the lowest numbered +/// register takes the lowest address. +fn block_transfer(mut b: Builder, p: &Parts, ops: &[Operand]) -> Lifted { + let Some(Operand::Sys(mask)) = ops.get(1) else { + return b.unimplemented(); + }; + if p.cond != 14 { + return b.unimplemented(); + } + // Writeback is spelled as a name, `r4!`, rather than as an operand kind. + let (base_num, back) = match ops.first() { + Some(Operand::Reg(r)) if matches!(r.class, RegClass::Gpr | RegClass::Sp) => (r.num, false), + Some(Operand::Name(n)) => match n.strip_suffix('!').and_then(register_named) { + Some(num) => (num, true), + None => return b.unimplemented(), + }, + _ => return b.unimplemented(), + }; + let regs: Vec = (0..16u8).filter(|n| mask & (1 << n) != 0).collect(); + if regs.is_empty() { + return b.unimplemented(); + } + let load = p.base.starts_with("ldm"); + let total = regs.len() as i64 * 4; + let down = p.base.ends_with("da") || p.base.ends_with("db"); + // Where the lowest numbered register goes, as a displacement from the + // base, and where the base ends up. + let (first, end) = match (down, p.base.ends_with('b')) { + (false, false) => (0, total), + (false, true) => (4, total), + (true, false) => (-total + 4, -total), + (true, true) => (-total, -total), + }; + let base = Varnode::register(gpr_offset(base_num), 4); + let mut returned = None; + for (n, r) in regs.iter().enumerate() { + let at = b.eval( + Op::IntAdd, + 4, + &[base, Varnode::constant((first + n as i64 * 4) as u64, 4)], + ); + if load { + let v = b.eval(Op::Load, 4, &[at]); + if *r == 15 { + returned = Some(v); + } else { + write_reg(&mut b, Varnode::register(gpr_offset(*r), 4), v); + } + } else { + b.emit(Op::Store, None, &[at, Varnode::register(gpr_offset(*r), 4)]); + } + } + if back { + let moved = b.eval(Op::IntAdd, 4, &[base, Varnode::constant(end as u64, 4)]); + write_reg(&mut b, base, moved); + } + if let Some(target) = returned { + b.emit(Op::Return, None, &[target]); + } + b.finish(true) +} + +/// The number of a core register named the way a listing spells it. +fn register_named(name: &str) -> Option { + match name { + "sp" => Some(13), + "lr" => Some(14), + "pc" => Some(15), + _ => name + .strip_prefix('r')? + .parse::() + .ok() + .filter(|n| *n < 16), + } +} + +/// `ldr pc, [...]`: a load whose destination is the program counter. +/// +/// The value it reads is where control goes, so the instruction is a branch +/// and the word it loads is the target -- not the program counter, which is +/// only where the encoding puts the destination. Off the stack with a +/// post-increment it is the return idiom and returns; anywhere else it is an +/// indirect branch. The base still gets its writeback either way. +fn load_to_pc( + mut b: Builder, + i: &Insn, + p: &Parts, + ops: &[Operand], + thumb: bool, + returns: bool, +) -> Lifted { + if p.cond != 14 { + return b.unimplemented(); + } + let Some(Operand::Mem(m)) = ops.get(1) else { + return b.unimplemented(); + }; + let Some((at, updated)) = address(&mut b, m, i.addr, thumb) else { + return b.unimplemented(); + }; + let target = b.eval(Op::Load, 4, &[at]); + writeback(&mut b, m, updated); + // The low bit of a loaded code address says which instruction set to + // switch to and is not part of the address. + let target = b.eval(Op::IntAnd, 4, &[target, Varnode::constant(!1u64, 4)]); + b.emit( + if returns { Op::Return } else { Op::BranchInd }, + None, + &[target], + ); + b.finish(true) +} + +/// The loads and stores. +fn memory(mut b: Builder, i: &Insn, p: &Parts, ops: &[Operand], thumb: bool) -> Lifted { + let bytes = match p.base { + b if b.starts_with("ldr") || b.starts_with("str") => match b { + "ldrb" | "ldrsb" | "strb" => 1u8, + "ldrh" | "ldrsh" | "strh" => 2, + "ldrd" | "strd" => 8, + _ => 4, + }, + _ => return b.unimplemented(), + }; + let signed = p.base.starts_with("ldrs"); + let load = p.base.starts_with("ldr"); + // A predicated access either happens or does not, and the IR has no branch + // inside an instruction to say which. A conditional load could be written + // as a select, but a conditional *store* could not: not storing is not the + // same as storing what was already there, because the address need not be + // readable. Both are declined, so the two are treated alike. + if p.cond != 14 { + return b.unimplemented(); + } + let Some(Operand::Mem(m)) = ops.get(1) else { + return b.unimplemented(); + }; + let Some((at, updated)) = address(&mut b, m, i.addr, thumb) else { + return b.unimplemented(); + }; + if bytes == 8 { + // `ldrd` and `strd` name two registers and move eight bytes. + return b.unimplemented(); + } + if load { + let Some(dest) = ops.first().and_then(writable) else { + return b.unimplemented(); + }; + let raw = b.eval(Op::Load, bytes, &[at]); + let value = if bytes == 4 { + raw + } else { + b.eval(if signed { Op::IntSExt } else { Op::IntZExt }, 4, &[raw]) + }; + write_reg(&mut b, reg(dest), value); + } else { + let Some((v, _)) = ops.first().and_then(|o| source(&mut b, o)) else { + return b.unimplemented(); + }; + let narrow = if bytes == 4 { + v + } else if v.space == Space::Register { + b.eval(Op::SubPiece, bytes, &[v, Varnode::constant(0, 1)]) + } else { + Varnode { size: bytes, ..v } + }; + b.emit(Op::Store, None, &[at, narrow]); + } + writeback(&mut b, m, updated); + b.finish(true) +} diff --git a/crates/e5r-ir/src/lift/neon.rs b/crates/e5r-ir/src/lift/neon.rs new file mode 100644 index 0000000..d29b756 --- /dev/null +++ b/crates/e5r-ir/src/lift/neon.rs @@ -0,0 +1,949 @@ +//! AArch64 SIMD lifting. +//! +//! A vector register is sixteen bytes of the register file and a lane is a +//! varnode inside it, so a lane operation is an ordinary operation on an +//! ordinary varnode. Nothing here needs values wider than the IR has: the +//! width lives in the addressing. +//! +//! A write through a 64-bit view clears the upper half of the register, which +//! is emitted rather than assumed, the same as the general registers. + +use e5r_arch::{Insn, Lanes, Operand, RegClass, Width}; + +use crate::lift::aarch64::{flag_c, flag_n, flag_v, flag_z, gpr_offset, sp_offset, vec_offset}; +use crate::lift::{Builder, Lifted}; +use crate::op::{Op, Varnode}; + +/// Lane count and lane size in bytes. +fn shape(l: Lanes) -> (u64, u8) { + match l { + Lanes::B8 => (8, 1), + Lanes::B16 => (16, 1), + Lanes::H4 => (4, 2), + Lanes::H8 => (8, 2), + Lanes::S2 => (2, 4), + Lanes::S4 => (4, 4), + Lanes::D1 => (1, 8), + Lanes::D2 => (2, 8), + Lanes::Q1 => (1, 16), + } +} + +/// One lane of a vector register. +fn lane(num: u8, index: u64, size: u8) -> Varnode { + Varnode::register(vec_offset(num) + index * size as u64, size) +} + +/// The register a vector operand names, with its shape. +fn vector(op: &Operand) -> Option<(u8, u64, u8)> { + match op { + Operand::Vector(n, l) => { + let (count, size) = shape(*l); + Some((*n, count, size)) + } + Operand::Reg(r) if r.class == RegClass::Vec => { + let size = match r.width { + Width::W8 => 1, + Width::W16 => 2, + Width::W32 => 4, + Width::W64 => 8, + Width::W128 => 16, + }; + Some((r.num, 1, size)) + } + _ => None, + } +} + +/// Clear the bytes of a vector register above `bytes`, which a write through a +/// narrower view performs. +fn clear_above(b: &mut Builder, num: u8, bytes: u64) { + let mut at = bytes; + while at < 16 { + // Aligned pieces only: a write that crosses an eight-byte boundary is + // two writes as far as the dataflow is concerned. + let chunk = aligned_chunk(at, 16); + b.emit( + Op::Copy, + Some(Varnode::register(vec_offset(num) + at, chunk)), + &[Varnode::constant(0, chunk)], + ); + at += chunk as u64; + } +} + +/// True when this instruction is one the SIMD lifter handles. +/// +/// Memory transfers are not among them: a vector load is still a load, and the +/// ordinary path handles it by moving the register in pieces. +pub fn handles(i: &Insn) -> bool { + let m = i.mnemonic; + if m.starts_with("ld") || m.starts_with("st") || m.starts_with("prf") { + return false; + } + i.operands().iter().any(|o| { + matches!( + o, + Operand::Vector(..) | Operand::VectorLane(..) | Operand::VectorList(..) + ) || matches!(o, Operand::Reg(r) if r.class == RegClass::Vec) + }) +} + +/// Lift one SIMD instruction, or say it is not modelled. +pub fn lift(mut b: Builder, i: &Insn) -> Lifted { + let ops = i.operands(); + match i.mnemonic { + "movi" | "mvni" => { + let Some((d, count, size)) = ops.first().and_then(vector) else { + return b.unimplemented(); + }; + let Some(raw) = immediate(ops) else { + return b.unimplemented(); + }; + let value = if i.mnemonic == "mvni" { !raw } else { raw }; + for n in 0..count { + b.emit( + Op::Copy, + Some(lane(d, n, size)), + &[Varnode::constant(value, size)], + ); + } + if count * size as u64 == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + "add" | "sub" | "mul" | "and" | "orr" | "eor" | "bic" | "orn" | "umax" | "umin" + | "smax" | "smin" => { + let (Some((d, count, size)), Some((x, ..)), Some((y, ..))) = ( + ops.first().and_then(vector), + ops.get(1).and_then(vector), + ops.get(2).and_then(vector), + ) else { + return b.unimplemented(); + }; + for n in 0..count { + let a = lane(x, n, size); + let c = lane(y, n, size); + let r = match i.mnemonic { + "add" => b.eval(Op::IntAdd, size, &[a, c]), + "sub" => b.eval(Op::IntSub, size, &[a, c]), + "mul" => b.eval(Op::IntMul, size, &[a, c]), + "and" => b.eval(Op::IntAnd, size, &[a, c]), + "orr" => b.eval(Op::IntOr, size, &[a, c]), + "eor" => b.eval(Op::IntXor, size, &[a, c]), + "bic" => { + let inverted = b.eval(Op::IntNot, size, &[c]); + b.eval(Op::IntAnd, size, &[a, inverted]) + } + "orn" => { + let inverted = b.eval(Op::IntNot, size, &[c]); + b.eval(Op::IntOr, size, &[a, inverted]) + } + // The maxima and minima pick a lane, which is a select on + // the comparison rather than an operation of its own. + _ => { + let op = match i.mnemonic { + "umax" => Op::IntLess, + "umin" => Op::IntLess, + "smax" => Op::IntSLess, + _ => Op::IntSLess, + }; + let takes_second = matches!(i.mnemonic, "umax" | "smax"); + let less = b.eval(op, 1, &[a, c]); + let (t, f) = if takes_second { (c, a) } else { (a, c) }; + select(&mut b, less, t, f, size) + } + }; + b.emit(Op::Copy, Some(lane(d, n, size)), &[r]); + } + if count * size as u64 == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + "mla" | "mls" => { + let (Some((d, count, size)), Some((x, ..)), Some((y, ..))) = ( + ops.first().and_then(vector), + ops.get(1).and_then(vector), + ops.get(2).and_then(vector), + ) else { + return b.unimplemented(); + }; + for n in 0..count { + let product = b.eval(Op::IntMul, size, &[lane(x, n, size), lane(y, n, size)]); + let acc = lane(d, n, size); + let op = if i.mnemonic == "mla" { + Op::IntAdd + } else { + Op::IntSub + }; + let r = b.eval(op, size, &[acc, product]); + b.emit(Op::Copy, Some(acc), &[r]); + } + if count * size as u64 == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + "neg" | "not" | "mvn" | "abs" => { + let (Some((d, count, size)), Some((x, xc, xs))) = + (ops.first().and_then(vector), ops.get(1).and_then(vector)) + else { + return b.unimplemented(); + }; + let (count, size) = if count == 1 && xc > 1 { + (xc, xs) + } else { + (count, size) + }; + for n in 0..count { + let a = lane(x, n, size); + let r = match i.mnemonic { + "neg" => b.eval(Op::IntNegate, size, &[a]), + "abs" => { + let negated = b.eval(Op::IntNegate, size, &[a]); + let negative = b.eval(Op::IntSLess, 1, &[a, Varnode::constant(0, size)]); + select(&mut b, negative, negated, a, size) + } + _ => b.eval(Op::IntNot, size, &[a]), + }; + b.emit(Op::Copy, Some(lane(d, n, size)), &[r]); + } + if count * size as u64 == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + // The bitwise selects: each bit of the destination comes from one of + // two registers according to a mask. + "bsl" | "bit" | "bif" => { + let (Some((d, count, size)), Some((x, ..)), Some((y, ..))) = ( + ops.first().and_then(vector), + ops.get(1).and_then(vector), + ops.get(2).and_then(vector), + ) else { + return b.unimplemented(); + }; + let bytes = count * size as u64; + for at in (0..bytes).step_by(8) { + let chunk = if bytes - at >= 8 { + 8u8 + } else { + (bytes - at) as u8 + }; + let dst = Varnode::register(vec_offset(d) + at, chunk); + let a = Varnode::register(vec_offset(x) + at, chunk); + let c = Varnode::register(vec_offset(y) + at, chunk); + // `bsl` selects with the destination as the mask; `bit` and + // `bif` use the third register, inverted for `bif`. + let (mask, from_mask, other) = match i.mnemonic { + "bsl" => (dst, a, c), + "bit" => (c, a, dst), + _ => { + let inverted = b.eval(Op::IntNot, chunk, &[c]); + (inverted, a, dst) + } + }; + let keep = b.eval(Op::IntAnd, chunk, &[from_mask, mask]); + let inverse = b.eval(Op::IntNot, chunk, &[mask]); + let rest = b.eval(Op::IntAnd, chunk, &[other, inverse]); + let r = b.eval(Op::IntOr, chunk, &[keep, rest]); + b.emit(Op::Copy, Some(dst), &[r]); + } + if bytes == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + "cmeq" | "cmgt" | "cmge" | "cmhi" | "cmhs" => { + let (Some((d, count, size)), Some((x, ..)), Some((y, ..))) = ( + ops.first().and_then(vector), + ops.get(1).and_then(vector), + ops.get(2).and_then(vector), + ) else { + return b.unimplemented(); + }; + for n in 0..count { + let a = lane(x, n, size); + let c = lane(y, n, size); + let truth = match i.mnemonic { + "cmeq" => b.eval(Op::IntEqual, 1, &[a, c]), + "cmgt" => b.eval(Op::IntSLess, 1, &[c, a]), + "cmge" => b.eval(Op::IntSLessEqual, 1, &[c, a]), + "cmhi" => b.eval(Op::IntLess, 1, &[c, a]), + _ => b.eval(Op::IntLessEqual, 1, &[c, a]), + }; + // A true comparison sets every bit of the lane. + let wide = if size == 1 { + truth + } else { + b.eval(Op::IntZExt, size, &[truth]) + }; + let r = b.eval(Op::IntNegate, size, &[wide]); + b.emit(Op::Copy, Some(lane(d, n, size)), &[r]); + } + if count * size as u64 == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + "addv" | "uaddlv" | "saddlv" => { + let Some((x, count, size)) = ops.get(1).and_then(vector) else { + return b.unimplemented(); + }; + let Some(Operand::Reg(dest)) = ops.first() else { + return b.unimplemented(); + }; + let wide = if i.mnemonic == "addv" { size } else { size * 2 }; + let mut acc = if wide == size { + lane(x, 0, size) + } else { + let op = if i.mnemonic == "saddlv" { + Op::IntSExt + } else { + Op::IntZExt + }; + b.eval(op, wide, &[lane(x, 0, size)]) + }; + for n in 1..count { + let next = if wide == size { + lane(x, n, size) + } else { + let op = if i.mnemonic == "saddlv" { + Op::IntSExt + } else { + Op::IntZExt + }; + b.eval(op, wide, &[lane(x, n, size)]) + }; + acc = b.eval(Op::IntAdd, wide, &[acc, next]); + } + b.emit(Op::Copy, Some(lane(dest.num, 0, wide)), &[acc]); + clear_above(&mut b, dest.num, wide as u64); + b.finish(true) + } + "addp" => { + // The scalar form folds a pair; the vector form interleaves two + // registers pairwise. + match (ops.first(), ops.get(1), ops.get(2)) { + (Some(Operand::Reg(d)), Some(src), None) => { + let Some((x, count, size)) = vector(src) else { + return b.unimplemented(); + }; + if count != 2 { + return b.unimplemented(); + } + let sum = b.eval(Op::IntAdd, size, &[lane(x, 0, size), lane(x, 1, size)]); + b.emit(Op::Copy, Some(lane(d.num, 0, size)), &[sum]); + clear_above(&mut b, d.num, size as u64); + b.finish(true) + } + (Some(dst), Some(a), Some(c)) => { + let (Some((d, count, size)), Some((x, ..)), Some((y, ..))) = + (vector(dst), vector(a), vector(c)) + else { + return b.unimplemented(); + }; + let half = count / 2; + let mut values = Vec::new(); + for n in 0..half { + values.push(b.eval( + Op::IntAdd, + size, + &[lane(x, n * 2, size), lane(x, n * 2 + 1, size)], + )); + } + for n in 0..half { + values.push(b.eval( + Op::IntAdd, + size, + &[lane(y, n * 2, size), lane(y, n * 2 + 1, size)], + )); + } + for (n, v) in values.into_iter().enumerate() { + b.emit(Op::Copy, Some(lane(d, n as u64, size)), &[v]); + } + if count * size as u64 == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + _ => b.unimplemented(), + } + } + "dup" => { + let Some((d, count, size)) = ops.first().and_then(vector) else { + return b.unimplemented(); + }; + let value = match ops.get(1) { + Some(Operand::Reg(r)) if r.class != RegClass::Vec => { + Varnode::register(gpr_offset(r.num), size) + } + Some(Operand::VectorLane(n, _, index)) => lane(*n, *index as u64, size), + _ => return b.unimplemented(), + }; + for n in 0..count { + b.emit(Op::Copy, Some(lane(d, n, size)), &[value]); + } + if count * size as u64 == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + "umov" | "smov" => { + let (Some(Operand::Reg(d)), Some(Operand::VectorLane(n, w, index))) = + (ops.first(), ops.get(1)) + else { + return b.unimplemented(); + }; + let size = match w { + Width::W8 => 1, + Width::W16 => 2, + Width::W32 => 4, + _ => 8, + }; + let value = lane(*n, *index as u64, size); + let dest_size = if d.width == Width::W64 { 8 } else { 4 }; + let widened = if size == dest_size { + value + } else if i.mnemonic == "smov" { + b.eval(Op::IntSExt, dest_size, &[value]) + } else { + b.eval(Op::IntZExt, dest_size, &[value]) + }; + b.emit( + Op::Copy, + Some(Varnode::register(gpr_offset(d.num), dest_size)), + &[widened], + ); + if dest_size == 4 { + b.emit( + Op::Copy, + Some(Varnode::register(gpr_offset(d.num) + 4, 4)), + &[Varnode::constant(0, 4)], + ); + } + b.finish(true) + } + "fmov" => { + // Only the forms that move bits between banks; the arithmetic ones + // are floating point and not modelled. + match (ops.first(), ops.get(1)) { + (Some(Operand::Reg(d)), Some(Operand::Reg(s))) + if d.class != RegClass::Vec && s.class == RegClass::Vec => + { + let size = if d.width == Width::W64 { 8 } else { 4 }; + let value = lane(s.num, 0, size); + b.emit( + Op::Copy, + Some(Varnode::register(gpr_offset(d.num), size)), + &[value], + ); + if size == 4 { + b.emit( + Op::Copy, + Some(Varnode::register(gpr_offset(d.num) + 4, 4)), + &[Varnode::constant(0, 4)], + ); + } + b.finish(true) + } + (Some(Operand::Reg(d)), Some(Operand::Reg(s))) + if d.class == RegClass::Vec && s.class != RegClass::Vec => + { + let size = if s.width == Width::W64 { 8 } else { 4 }; + let src = if s.class == RegClass::Zr { + Varnode::constant(0, size) + } else { + Varnode::register(gpr_offset(s.num), size) + }; + b.emit(Op::Copy, Some(lane(d.num, 0, size)), &[src]); + clear_above(&mut b, d.num, size as u64); + b.finish(true) + } + // A literal, which the encoding spells as a small set of + // representable values. + (Some(Operand::Reg(d)), Some(Operand::FpImm(bits))) if d.class == RegClass::Vec => { + let size = if d.width == Width::W64 { 8 } else { 4 }; + let value = if size == 4 { + (f64::from_bits(*bits) as f32).to_bits() as u64 + } else { + *bits + }; + b.emit( + Op::Copy, + Some(lane(d.num, 0, size)), + &[Varnode::constant(value, size)], + ); + clear_above(&mut b, d.num, size as u64); + b.finish(true) + } + (Some(Operand::Reg(d)), Some(Operand::Reg(s))) + if d.class == RegClass::Vec && s.class == RegClass::Vec => + { + let size = if d.width == Width::W64 { 8 } else { 4 }; + let value = lane(s.num, 0, size); + b.emit(Op::Copy, Some(lane(d.num, 0, size)), &[value]); + clear_above(&mut b, d.num, size as u64); + b.finish(true) + } + _ => b.unimplemented(), + } + } + "mov" | "ins" => { + match (ops.first(), ops.get(1)) { + // Inserting one lane, from a general register or another lane. + (Some(Operand::VectorLane(d, w, index)), Some(src)) => { + let size = match w { + Width::W8 => 1, + Width::W16 => 2, + Width::W32 => 4, + _ => 8, + }; + let value = match src { + Operand::Reg(r) if r.class == RegClass::Zr => Varnode::constant(0, size), + Operand::Reg(r) if r.class != RegClass::Vec => { + Varnode::register(gpr_offset(r.num), size) + } + Operand::VectorLane(n, _, from) => lane(*n, *from as u64, size), + _ => return b.unimplemented(), + }; + // The rest of the register keeps what it held. + b.emit(Op::Copy, Some(lane(*d, *index as u64, size)), &[value]); + b.finish(true) + } + // A whole-register move between vector registers. + (Some(a), Some(c)) if vector(a).is_some() && vector(c).is_some() => { + let ((d, count, size), (s, ..)) = (vector(a).unwrap(), vector(c).unwrap()); + let bytes = count * size as u64; + let mut at = 0; + while at < bytes { + let chunk = if bytes - at >= 8 { + 8 + } else { + (bytes - at) as u8 + }; + b.emit( + Op::Copy, + Some(Varnode::register(vec_offset(d) + at, chunk)), + &[Varnode::register(vec_offset(s) + at, chunk)], + ); + at += chunk as u64; + } + if bytes == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + _ => b.unimplemented(), + } + } + "ext" => { + let (Some((d, count, size)), Some((x, ..)), Some((y, ..)), Some(Operand::Count(n))) = ( + ops.first().and_then(vector), + ops.get(1).and_then(vector), + ops.get(2).and_then(vector), + ops.get(3), + ) else { + return b.unimplemented(); + }; + // Bytes taken from the top of the first register then the bottom of + // the second, which is a rotation when the two are the same. + let total = count * size as u64; + let start = *n as u64; + let mut bytes = Vec::new(); + for k in 0..total { + let at = start + k; + let source = if at < total { + Varnode::register(vec_offset(x) + at, 1) + } else { + Varnode::register(vec_offset(y) + (at - total), 1) + }; + bytes.push(b.eval(Op::Copy, 1, &[source])); + } + for (k, v) in bytes.into_iter().enumerate() { + b.emit( + Op::Copy, + Some(Varnode::register(vec_offset(d) + k as u64, 1)), + &[v], + ); + } + if total == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + "zip1" | "zip2" | "uzp1" | "uzp2" | "trn1" | "trn2" => { + let (Some((d, count, size)), Some((x, ..)), Some((y, ..))) = ( + ops.first().and_then(vector), + ops.get(1).and_then(vector), + ops.get(2).and_then(vector), + ) else { + return b.unimplemented(); + }; + let mut picked: Vec = Vec::new(); + for n in 0..count { + let (from, index) = match i.mnemonic { + "zip1" | "zip2" => { + let base = if i.mnemonic == "zip2" { count / 2 } else { 0 }; + let half = base + n / 2; + if n % 2 == 0 { (x, half) } else { (y, half) } + } + "uzp1" | "uzp2" => { + let odd = u64::from(i.mnemonic == "uzp2"); + if n < count / 2 { + (x, n * 2 + odd) + } else { + (y, (n - count / 2) * 2 + odd) + } + } + _ => { + let odd = u64::from(i.mnemonic == "trn2"); + if n % 2 == 0 { + (x, n + odd) + } else { + (y, n - 1 + odd) + } + } + }; + picked.push(b.eval(Op::Copy, size, &[lane(from, index, size)])); + } + for (n, v) in picked.into_iter().enumerate() { + b.emit(Op::Copy, Some(lane(d, n as u64, size)), &[v]); + } + if count * size as u64 == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + "shl" | "ushr" | "sshr" | "sli" | "sri" => { + let (Some((d, count, size)), Some((x, ..)), Some(Operand::Count(n))) = ( + ops.first().and_then(vector), + ops.get(1).and_then(vector), + ops.get(2), + ) else { + return b.unimplemented(); + }; + let amount = Varnode::constant(*n as u64, 1); + for k in 0..count { + let a = lane(x, k, size); + let r = match i.mnemonic { + "shl" => b.eval(Op::IntLeft, size, &[a, amount]), + "ushr" => b.eval(Op::IntRight, size, &[a, amount]), + "sshr" => b.eval(Op::IntSRight, size, &[a, amount]), + _ => return b.unimplemented(), + }; + b.emit(Op::Copy, Some(lane(d, k, size)), &[r]); + } + if count * size as u64 == 8 { + clear_above(&mut b, d, 8); + } + b.finish(true) + } + _ => float(b, i), + } +} + +/// The floating point instructions, scalar and packed. +/// +/// The lane size comes from the register width for a scalar and from the +/// arrangement for a vector, and the operations are the IR's own: nothing here +/// approximates a rounding rule with integer arithmetic. +fn float(mut b: Builder, i: &Insn) -> Lifted { + let ops = i.operands(); + let m = i.mnemonic; + // The conversions from integers do not start with `f`, and everything + // else here does. + if !m.starts_with('f') && !matches!(m, "scvtf" | "ucvtf") { + return b.unimplemented(); + } + + // The forms whose destination is a general register, or none at all. + match m { + "fcmp" | "fcmpe" => { + let Some((x, _, size)) = ops.first().and_then(vector) else { + return b.unimplemented(); + }; + let a = lane(x, 0, size); + // The second operand is either a register or the literal zero. + let c = match ops.get(1) { + Some(o) if vector(o).is_some() => { + let (y, ..) = vector(o).unwrap(); + lane(y, 0, size) + } + // The compare-with-zero form, which the decoder spells out. + Some(Operand::FpImm(0)) | Some(Operand::Imm(0)) | Some(Operand::Name(_)) | None => { + Varnode::constant(0, size) + } + _ => return b.unimplemented(), + }; + let less = b.eval(Op::FloatLess, 1, &[a, c]); + let equal = b.eval(Op::FloatEqual, 1, &[a, c]); + let nan_a = b.eval(Op::FloatNan, 1, &[a]); + let nan_c = b.eval(Op::FloatNan, 1, &[c]); + let unordered = b.eval(Op::BoolOr, 1, &[nan_a, nan_c]); + let carry = b.eval(Op::BoolNot, 1, &[less]); + b.emit(Op::Copy, Some(flag_n()), &[less]); + b.emit(Op::Copy, Some(flag_z()), &[equal]); + b.emit(Op::Copy, Some(flag_c()), &[carry]); + b.emit(Op::Copy, Some(flag_v()), &[unordered]); + return b.finish(true); + } + "fcvtzs" | "fcvtzu" => { + let (Some(Operand::Reg(dest)), Some(src)) = (ops.first(), ops.get(1)) else { + return b.unimplemented(); + }; + let Some((x, _, from)) = vector(src) else { + return b.unimplemented(); + }; + let to = if dest.width == Width::W64 { 8u8 } else { 4 }; + let op = if m == "fcvtzs" { + Op::FloatToInt + } else { + Op::FloatToUInt + }; + let v = b.eval(op, to, &[lane(x, 0, from)]); + b.emit( + Op::Copy, + Some(Varnode::register(gpr_offset(dest.num), to)), + &[v], + ); + if to == 4 { + b.emit( + Op::Copy, + Some(Varnode::register(gpr_offset(dest.num) + 4, 4)), + &[Varnode::constant(0, 4)], + ); + } + return b.finish(true); + } + "scvtf" | "ucvtf" => { + let (Some(dst), Some(Operand::Reg(src))) = (ops.first(), ops.get(1)) else { + return b.unimplemented(); + }; + let Some((d, _, to)) = vector(dst) else { + return b.unimplemented(); + }; + let from = if src.width == Width::W64 { 8u8 } else { 4 }; + let value = if src.class == RegClass::Zr { + Varnode::constant(0, from) + } else if src.class == RegClass::Vec { + lane(src.num, 0, from) + } else { + Varnode::register(gpr_offset(src.num), from) + }; + let op = if m == "scvtf" { + Op::IntToFloat + } else { + Op::UIntToFloat + }; + let v = b.eval(op, to, &[value]); + b.emit(Op::Copy, Some(lane(d, 0, to)), &[v]); + clear_above(&mut b, d, to as u64); + return b.finish(true); + } + "fcvt" => { + let (Some(dst), Some(src)) = (ops.first(), ops.get(1)) else { + return b.unimplemented(); + }; + let (Some((d, _, to)), Some((x, _, from))) = (vector(dst), vector(src)) else { + return b.unimplemented(); + }; + if !matches!(to, 4 | 8) || !matches!(from, 4 | 8) { + return b.unimplemented(); + } + let v = b.eval(Op::FloatConvert, to, &[lane(x, 0, from)]); + b.emit(Op::Copy, Some(lane(d, 0, to)), &[v]); + clear_above(&mut b, d, to as u64); + return b.finish(true); + } + "fcsel" => { + let (Some(dst), Some(a), Some(c), Some(Operand::Cond(cond))) = + (ops.first(), ops.get(1), ops.get(2), ops.get(3)) + else { + return b.unimplemented(); + }; + let (Some((d, _, size)), Some((x, ..)), Some((y, ..))) = + (vector(dst), vector(a), vector(c)) + else { + return b.unimplemented(); + }; + let taken = crate::lift::aarch64::condition_value(&mut b, cond.0); + let r = select(&mut b, taken, lane(x, 0, size), lane(y, 0, size), size); + b.emit(Op::Copy, Some(lane(d, 0, size)), &[r]); + clear_above(&mut b, d, size as u64); + return b.finish(true); + } + _ => {} + } + + // The shape of the destination decides scalar or packed. + let (d, count, size) = match ops.first().and_then(vector) { + Some(v) => v, + None => return b.unimplemented(), + }; + if size != 4 && size != 8 { + return b.unimplemented(); + } + + let binary = |m: &str| -> Option { + Some(match m { + "fadd" => Op::FloatAdd, + "fsub" => Op::FloatSub, + "fmul" => Op::FloatMul, + "fdiv" => Op::FloatDiv, + "fmax" | "fmaxnm" => Op::FloatMax, + "fmin" | "fminnm" => Op::FloatMin, + _ => return None, + }) + }; + + if let Some(op) = binary(m) { + let (Some((x, ..)), Some((y, ..))) = + (ops.get(1).and_then(vector), ops.get(2).and_then(vector)) + else { + return b.unimplemented(); + }; + for n in 0..count { + let r = b.eval(op, size, &[lane(x, n, size), lane(y, n, size)]); + b.emit(Op::Copy, Some(lane(d, n, size)), &[r]); + } + clear_tail(&mut b, d, count * size as u64); + return b.finish(true); + } + + match m { + "fneg" | "fabs" | "fsqrt" | "frintz" | "frintn" | "frintp" | "frintm" | "frinta" => { + let Some((x, ..)) = ops.get(1).and_then(vector) else { + return b.unimplemented(); + }; + let op = match m { + "fneg" => Op::FloatNeg, + "fabs" => Op::FloatAbs, + "fsqrt" => Op::FloatSqrt, + "frintz" => Op::FloatTrunc, + "frintp" => Op::FloatCeil, + "frintm" => Op::FloatFloor, + _ => Op::FloatRound, + }; + for n in 0..count { + let r = b.eval(op, size, &[lane(x, n, size)]); + b.emit(Op::Copy, Some(lane(d, n, size)), &[r]); + } + clear_tail(&mut b, d, count * size as u64); + b.finish(true) + } + // The fused multiply-adds, which round once: `fmadd` is `a + n * m`, + // and the other three negate one side or the other. + "fmadd" | "fmsub" | "fnmadd" | "fnmsub" => { + let (Some((x, ..)), Some((y, ..)), Some((a, ..))) = ( + ops.get(1).and_then(vector), + ops.get(2).and_then(vector), + ops.get(3).and_then(vector), + ) else { + return b.unimplemented(); + }; + let mut multiplicand = lane(x, 0, size); + if matches!(m, "fmsub" | "fnmadd") { + multiplicand = b.eval(Op::FloatNeg, size, &[multiplicand]); + } + let mut addend = lane(a, 0, size); + if matches!(m, "fnmadd" | "fnmsub") { + addend = b.eval(Op::FloatNeg, size, &[addend]); + } + let r = b.eval( + Op::FloatMulAdd, + size, + &[multiplicand, lane(y, 0, size), addend], + ); + b.emit(Op::Copy, Some(lane(d, 0, size)), &[r]); + clear_tail(&mut b, d, size as u64); + b.finish(true) + } + _ => b.unimplemented(), + } +} + +/// Clear whatever sits above a write of this many bytes. +fn clear_tail(b: &mut Builder, num: u8, bytes: u64) { + if bytes < 16 { + clear_above(b, num, bytes); + } +} + +/// A vector immediate, with any shift the encoding carries applied. +fn immediate(ops: &[Operand]) -> Option { + let mut value = match ops.get(1)? { + Operand::UImm(v) => *v, + Operand::Imm(v) => *v as u64, + _ => return None, + }; + if let Some(Operand::ShiftOp(shift, amount)) = ops.get(2) { + use e5r_arch::Shift; + value = match shift { + Shift::Lsl => value << amount, + // Shifting in ones, which the modified immediate encoding uses. + Shift::Msl => (value << amount) | ((1u64 << amount) - 1), + _ => return None, + }; + } + Some(value) +} + +/// A branch-free choice between two lanes. +fn select(b: &mut Builder, cond: Varnode, t: Varnode, f: Varnode, size: u8) -> Varnode { + let wide = if size == 1 { + cond + } else { + b.eval(Op::IntZExt, size, &[cond]) + }; + let mask = b.eval(Op::IntNegate, size, &[wide]); + let keep = b.eval(Op::IntAnd, size, &[t, mask]); + let inverse = b.eval(Op::IntNot, size, &[mask]); + let drop = b.eval(Op::IntAnd, size, &[f, inverse]); + b.eval(Op::IntOr, size, &[keep, drop]) +} + +/// The largest aligned piece that starts at `at` and ends by `limit`. +pub fn aligned_chunk(at: u64, limit: u64) -> u8 { + let mut chunk = 8u64; + while chunk > 1 && (at % chunk != 0 || at + chunk > limit) { + chunk /= 2; + } + chunk as u8 +} + +/// A sixteen-byte load or store, which the ordinary path cannot express +/// because the IR carries no value that wide. +pub fn wide_transfer(b: &mut Builder, addr: Varnode, num: u8, bytes: u64, store: bool) { + let mut at = 0; + while at < bytes { + let chunk = if bytes - at >= 8 { + 8 + } else { + (bytes - at) as u8 + }; + let here = if at == 0 { + addr + } else { + b.eval(Op::IntAdd, 8, &[addr, Varnode::constant(at, 8)]) + }; + let slot = Varnode::register(vec_offset(num) + at, chunk); + if store { + b.emit(Op::Store, None, &[here, slot]); + } else { + let value = b.eval(Op::Load, chunk, &[here]); + b.emit(Op::Copy, Some(slot), &[value]); + } + at += chunk as u64; + } + if !store && bytes == 8 { + clear_above(b, num, 8); + } +} + +/// The stack pointer, re-exported so the loads can reach it. +pub fn stack() -> Varnode { + Varnode::register(sp_offset(), 8) +} diff --git a/crates/e5r-ir/src/lift/sse.rs b/crates/e5r-ir/src/lift/sse.rs new file mode 100644 index 0000000..ea636e8 --- /dev/null +++ b/crates/e5r-ir/src/lift/sse.rs @@ -0,0 +1,896 @@ +//! x86-64 SIMD lifting, the integer part. +//! +//! Same idea as the NEON lifter: a vector register is sixteen bytes of the +//! register file and a lane is a varnode inside it. The packed integer +//! operations a compiler's vectorizer emits are all lane-wise, so they need +//! nothing the IR does not already have. +//! +//! The floating point instructions are a different matter and are not modelled +//! here, because the IR has no floating point operations and approximating +//! them with integer ones would produce answers that look right. + +use e5r_arch::{Insn, Operand, RegClass}; +use e5r_core::Addr; + +use crate::lift::x86::{address, vec_offset}; +use crate::lift::{Builder, Lifted}; +use crate::op::{Op, Varnode}; + +/// One lane of a vector register. +fn lane(num: u8, index: u64, size: u8) -> Varnode { + Varnode::register(vec_offset(num) + index * size as u64, size) +} + +/// The vector register an operand names. +fn vector(op: &Operand) -> Option { + match op { + Operand::Reg(r) if r.class == RegClass::Vec => Some(r.num), + _ => None, + } +} + +/// True when the SIMD lifter handles this instruction. +/// +/// A conversion can name no vector register at all — `cvttsd2si rax, [rbp-8]` +/// reads memory and writes a general register — so the mnemonic decides for +/// those. +pub fn handles(i: &Insn) -> bool { + i.operands() + .iter() + .any(|o| matches!(o, Operand::Reg(r) if r.class == RegClass::Vec)) + || i.mnemonic.starts_with("cvt") +} + +/// Read sixteen bytes of a source operand into two temporaries, whichever +/// kind of operand it is. +fn read_wide(b: &mut Builder, op: &Operand, at: Addr) -> Option<[Varnode; 2]> { + match op { + Operand::Reg(r) if r.class == RegClass::Vec => Some([ + Varnode::register(vec_offset(r.num), 8), + Varnode::register(vec_offset(r.num) + 8, 8), + ]), + Operand::Mem(m) => { + let addr = address(b, m, at)?; + let low = b.eval(Op::Load, 8, &[addr]); + let next = b.eval(Op::IntAdd, 8, &[addr, Varnode::constant(8, 8)]); + let high = b.eval(Op::Load, 8, &[next]); + Some([low, high]) + } + _ => None, + } +} + +/// The lane size a packed mnemonic works at, from its suffix letter. +fn lane_size(m: &str) -> Option { + Some(match m.chars().last()? { + 'b' => 1, + 'w' => 2, + 'd' => 4, + 'q' => 8, + _ => return None, + }) +} + +/// Lift one SIMD instruction. +pub fn lift(mut b: Builder, i: &Insn) -> Lifted { + let ops = i.operands(); + let at = i.next(); + let m = i.mnemonic; + + if let Some(l) = scalar_float(&mut b, i) { + return l; + } + + match m { + // Whole-register moves, aligned or not: the IR does not model the + // fault an unaligned access to the aligned form would take. + "movdqa" | "movdqu" | "movaps" | "movups" | "movapd" | "movupd" | "lddqu" => { + match (ops.first(), ops.get(1)) { + (Some(dst), Some(src)) => { + let Some(value) = read_wide(&mut b, src, at) else { + return b.unimplemented(); + }; + match dst { + Operand::Reg(r) if r.class == RegClass::Vec => { + b.emit(Op::Copy, Some(lane(r.num, 0, 8)), &[value[0]]); + b.emit(Op::Copy, Some(lane(r.num, 1, 8)), &[value[1]]); + } + Operand::Mem(mem) => { + let Some(addr) = address(&mut b, mem, at) else { + return b.unimplemented(); + }; + b.emit(Op::Store, None, &[addr, value[0]]); + let next = b.eval(Op::IntAdd, 8, &[addr, Varnode::constant(8, 8)]); + b.emit(Op::Store, None, &[next, value[1]]); + } + _ => return b.unimplemented(), + } + b.finish(true) + } + _ => b.unimplemented(), + } + } + // The narrow moves, which clear the rest of the register when the + // destination is a vector one. + "movd" | "movq" => { + let size = if m == "movd" { 4 } else { 8 }; + match (ops.first(), ops.get(1)) { + (Some(Operand::Reg(d)), Some(src)) if d.class == RegClass::Vec => { + let value = match src { + Operand::Reg(s) if s.class == RegClass::Vec => lane(s.num, 0, size), + Operand::Reg(s) => { + Varnode::register(crate::lift::x86::gpr_offset(s.num), size) + } + Operand::Mem(mem) => { + let Some(addr) = address(&mut b, mem, at) else { + return b.unimplemented(); + }; + b.eval(Op::Load, size, &[addr]) + } + _ => return b.unimplemented(), + }; + b.emit(Op::Copy, Some(lane(d.num, 0, size)), &[value]); + // Everything above the moved bytes becomes zero. + let mut off = size as u64; + while off < 16 { + let chunk = crate::lift::neon::aligned_chunk(off, 16); + b.emit( + Op::Copy, + Some(Varnode::register(vec_offset(d.num) + off, chunk)), + &[Varnode::constant(0, chunk)], + ); + off += chunk as u64; + } + b.finish(true) + } + (Some(dst), Some(Operand::Reg(s))) if s.class == RegClass::Vec => { + let value = lane(s.num, 0, size); + match dst { + Operand::Reg(d) => { + b.emit( + Op::Copy, + Some(Varnode::register(crate::lift::x86::gpr_offset(d.num), size)), + &[value], + ); + if size == 4 { + b.emit( + Op::Copy, + Some(Varnode::register( + crate::lift::x86::gpr_offset(d.num) + 4, + 4, + )), + &[Varnode::constant(0, 4)], + ); + } + } + Operand::Mem(mem) => { + let Some(addr) = address(&mut b, mem, at) else { + return b.unimplemented(); + }; + b.emit(Op::Store, None, &[addr, value]); + } + _ => return b.unimplemented(), + } + b.finish(true) + } + _ => b.unimplemented(), + } + } + // The bitwise operations, which have no lanes. + "pxor" | "pand" | "por" | "pandn" | "xorps" | "xorpd" | "andps" | "andpd" | "orps" + | "orpd" | "andnps" | "andnpd" => { + let (Some(d), Some(src)) = (ops.first().and_then(vector), ops.get(1)) else { + return b.unimplemented(); + }; + let Some(y) = read_wide(&mut b, src, at) else { + return b.unimplemented(); + }; + for half in 0..2u64 { + let x = lane(d, half, 8); + let r = match m { + "pxor" | "xorps" | "xorpd" => b.eval(Op::IntXor, 8, &[x, y[half as usize]]), + "por" | "orps" | "orpd" => b.eval(Op::IntOr, 8, &[x, y[half as usize]]), + "pandn" | "andnps" | "andnpd" => { + let inverted = b.eval(Op::IntNot, 8, &[x]); + b.eval(Op::IntAnd, 8, &[inverted, y[half as usize]]) + } + _ => b.eval(Op::IntAnd, 8, &[x, y[half as usize]]), + }; + b.emit(Op::Copy, Some(lane(d, half, 8)), &[r]); + } + b.finish(true) + } + // Lane-wise arithmetic. + "paddb" | "paddw" | "paddd" | "paddq" | "psubb" | "psubw" | "psubd" | "psubq" + | "pmullw" | "pmulld" | "pminub" | "pmaxub" | "pminsd" | "pmaxsd" | "pcmpeqb" + | "pcmpeqw" | "pcmpeqd" | "pcmpeqq" | "pcmpgtb" | "pcmpgtw" | "pcmpgtd" => { + let (Some(d), Some(src)) = (ops.first().and_then(vector), ops.get(1)) else { + return b.unimplemented(); + }; + let Some(size) = lane_size(m) else { + return b.unimplemented(); + }; + let Some(y) = read_wide(&mut b, src, at) else { + return b.unimplemented(); + }; + // Name the source halves so a lane can be cut out of them. + let source = [b.eval(Op::Copy, 8, &[y[0]]), b.eval(Op::Copy, 8, &[y[1]])]; + let count = 16 / size as u64; + for n in 0..count { + let x = lane(d, n, size); + let c = piece(&mut b, source, n, size); + let r = match m { + _ if m.starts_with("padd") => b.eval(Op::IntAdd, size, &[x, c]), + _ if m.starts_with("psub") => b.eval(Op::IntSub, size, &[x, c]), + _ if m.starts_with("pmul") => b.eval(Op::IntMul, size, &[x, c]), + _ if m.starts_with("pcmpeq") => { + let equal = b.eval(Op::IntEqual, 1, &[x, c]); + all_bits(&mut b, equal, size) + } + _ if m.starts_with("pcmpgt") => { + let greater = b.eval(Op::IntSLess, 1, &[c, x]); + all_bits(&mut b, greater, size) + } + "pminub" | "pminsd" => { + let op = if m == "pminub" { + Op::IntLess + } else { + Op::IntSLess + }; + let less = b.eval(op, 1, &[x, c]); + select(&mut b, less, x, c, size) + } + _ => { + let op = if m == "pmaxub" { + Op::IntLess + } else { + Op::IntSLess + }; + let less = b.eval(op, 1, &[x, c]); + select(&mut b, less, c, x, size) + } + }; + b.emit(Op::Copy, Some(lane(d, n, size)), &[r]); + } + b.finish(true) + } + // The even 32-bit lanes multiplied into 64-bit results. + "pmuludq" => { + let (Some(d), Some(src)) = (ops.first().and_then(vector), ops.get(1)) else { + return b.unimplemented(); + }; + let Some(y) = read_wide(&mut b, src, at) else { + return b.unimplemented(); + }; + let source = [b.eval(Op::Copy, 8, &[y[0]]), b.eval(Op::Copy, 8, &[y[1]])]; + let mut results = Vec::new(); + for half in 0..2u64 { + let x = lane(d, half * 2, 4); + let c = piece(&mut b, source, half * 2, 4); + let wx = b.eval(Op::IntZExt, 8, &[x]); + let wc = b.eval(Op::IntZExt, 8, &[c]); + results.push(b.eval(Op::IntMul, 8, &[wx, wc])); + } + for (half, r) in results.into_iter().enumerate() { + b.emit(Op::Copy, Some(lane(d, half as u64, 8)), &[r]); + } + b.finish(true) + } + // Lane permutation by an immediate. + "pshufd" => { + let (Some(d), Some(src), Some(order)) = + (ops.first().and_then(vector), ops.get(1), ops.get(2)) + else { + return b.unimplemented(); + }; + let control = match order { + Operand::Imm(v) => *v as u64, + Operand::UImm(v) => *v, + Operand::Count(v) => *v as u64, + _ => return b.unimplemented(), + }; + let Some(y) = read_wide(&mut b, src, at) else { + return b.unimplemented(); + }; + let source = [b.eval(Op::Copy, 8, &[y[0]]), b.eval(Op::Copy, 8, &[y[1]])]; + let mut picked = Vec::new(); + for n in 0..4u64 { + let from = (control >> (n * 2)) & 3; + picked.push(piece(&mut b, source, from, 4)); + } + for (n, v) in picked.into_iter().enumerate() { + let named = b.eval(Op::Copy, 4, &[v]); + b.emit(Op::Copy, Some(lane(d, n as u64, 4)), &[named]); + } + b.finish(true) + } + // Interleaving the low or high halves. + "punpcklbw" | "punpcklwd" | "punpckldq" | "punpcklqdq" | "punpckhbw" | "punpckhwd" + | "punpckhdq" | "punpckhqdq" => { + let (Some(d), Some(src)) = (ops.first().and_then(vector), ops.get(1)) else { + return b.unimplemented(); + }; + let size: u8 = match m { + "punpcklbw" | "punpckhbw" => 1, + "punpcklwd" | "punpckhwd" => 2, + "punpckldq" | "punpckhdq" => 4, + _ => 8, + }; + let Some(y) = read_wide(&mut b, src, at) else { + return b.unimplemented(); + }; + let source = [b.eval(Op::Copy, 8, &[y[0]]), b.eval(Op::Copy, 8, &[y[1]])]; + let count = 16 / size as u64; + let base = if m.starts_with("punpckh") { + count / 2 + } else { + 0 + }; + let mut picked = Vec::new(); + for n in 0..count { + let index = base + n / 2; + let v = if n % 2 == 0 { + let l = lane(d, index, size); + b.eval(Op::Copy, size, &[l]) + } else { + let p = piece(&mut b, source, index, size); + b.eval(Op::Copy, size, &[p]) + }; + picked.push(v); + } + for (n, v) in picked.into_iter().enumerate() { + b.emit(Op::Copy, Some(lane(d, n as u64, size)), &[v]); + } + b.finish(true) + } + // Packing with saturation: two registers of wide lanes become one of + // narrow ones, each clamped to what the narrow lane can hold. + "packssdw" | "packsswb" | "packusdw" | "packuswb" => { + let (Some(d), Some(src)) = (ops.first().and_then(vector), ops.get(1)) else { + return b.unimplemented(); + }; + let wide: u8 = if m.ends_with("dw") { 4 } else { 2 }; + let narrow = wide / 2; + let signed = m.starts_with("packs"); + let Some(y) = read_wide(&mut b, src, at) else { + return b.unimplemented(); + }; + let source = [b.eval(Op::Copy, 8, &[y[0]]), b.eval(Op::Copy, 8, &[y[1]])]; + let per = 16 / wide as u64; + let mut packed = Vec::new(); + for n in 0..per { + let v = lane(d, n, wide); + packed.push(saturate(&mut b, v, wide, narrow, signed)); + } + for n in 0..per { + let v = piece(&mut b, source, n, wide); + packed.push(saturate(&mut b, v, wide, narrow, signed)); + } + for (n, v) in packed.into_iter().enumerate() { + b.emit(Op::Copy, Some(lane(d, n as u64, narrow)), &[v]); + } + b.finish(true) + } + // Shuffling one half's words, leaving the other half alone. + "pshuflw" | "pshufhw" => { + let (Some(d), Some(src), Some(order)) = + (ops.first().and_then(vector), ops.get(1), ops.get(2)) + else { + return b.unimplemented(); + }; + let control = match order { + Operand::Imm(v) => *v as u64, + Operand::UImm(v) => *v, + Operand::Count(v) => *v as u64, + _ => return b.unimplemented(), + }; + let Some(y) = read_wide(&mut b, src, at) else { + return b.unimplemented(); + }; + let source = [b.eval(Op::Copy, 8, &[y[0]]), b.eval(Op::Copy, 8, &[y[1]])]; + let base = if m == "pshufhw" { 4u64 } else { 0 }; + let mut picked = Vec::new(); + for n in 0..4u64 { + let from = base + ((control >> (n * 2)) & 3); + picked.push(piece(&mut b, source, from, 2)); + } + // The untouched half is copied across, because the source may be + // memory or another register. + let other = if m == "pshufhw" { 0u64 } else { 4 }; + let mut kept = Vec::new(); + for n in 0..4u64 { + kept.push(piece(&mut b, source, other + n, 2)); + } + for (n, v) in picked.into_iter().enumerate() { + let named = b.eval(Op::Copy, 2, &[v]); + b.emit(Op::Copy, Some(lane(d, base + n as u64, 2)), &[named]); + } + for (n, v) in kept.into_iter().enumerate() { + let named = b.eval(Op::Copy, 2, &[v]); + b.emit(Op::Copy, Some(lane(d, other + n as u64, 2)), &[named]); + } + b.finish(true) + } + // One lane out to a general register. + "pextrw" | "pextrb" | "pextrd" | "pextrq" => { + let size: u8 = match m { + "pextrb" => 1, + "pextrw" => 2, + "pextrd" => 4, + _ => 8, + }; + let (Some(dst), Some(s), Some(index)) = + (ops.first(), ops.get(1).and_then(vector), ops.get(2)) + else { + return b.unimplemented(); + }; + let n = match index { + Operand::Imm(v) => *v as u64, + Operand::UImm(v) => *v, + Operand::Count(v) => *v as u64, + _ => return b.unimplemented(), + } % (16 / size as u64); + let value = lane(s, n, size); + match dst { + Operand::Reg(r) if r.class != RegClass::Vec => { + // The destination is a full register and the lane is + // zero-extended into it. + let dest_size = if size == 8 { 8 } else { 4 }; + let widened = if size == dest_size { + value + } else { + b.eval(Op::IntZExt, dest_size, &[value]) + }; + b.emit( + Op::Copy, + Some(Varnode::register( + crate::lift::x86::gpr_offset(r.num), + dest_size, + )), + &[widened], + ); + if dest_size == 4 { + b.emit( + Op::Copy, + Some(Varnode::register( + crate::lift::x86::gpr_offset(r.num) + 4, + 4, + )), + &[Varnode::constant(0, 4)], + ); + } + b.finish(true) + } + Operand::Mem(mem) => { + let Some(addr) = address(&mut b, mem, at) else { + return b.unimplemented(); + }; + b.emit(Op::Store, None, &[addr, value]); + b.finish(true) + } + _ => b.unimplemented(), + } + } + // Shifts of whole lanes by an immediate. + "psllq" | "psrlq" | "pslld" | "psrld" | "psllw" | "psrlw" | "psrad" | "psraw" => { + let (Some(d), Some(amount)) = (ops.first().and_then(vector), ops.get(1)) else { + return b.unimplemented(); + }; + let Some(size) = lane_size(m) else { + return b.unimplemented(); + }; + let op = match &m[0..4] { + "psll" => Op::IntLeft, + "psrl" => Op::IntRight, + _ => Op::IntSRight, + }; + let lanes = 16 / size as u64; + // A count in a register, which is the low 64 bits of the source + // vector. A count at or above the lane's width gives zero for a + // logical shift, so the shift is forced into range and the result + // masked to nothing, which is exact. The arithmetic form + // saturates to the sign instead and is declined rather than + // approximated. + if let Some(src) = vector(amount) { + if op == Op::IntSRight { + return b.unimplemented(); + } + let raw = lane(src, 0, 8); + let bits = Varnode::constant(u64::from(size) * 8, 8); + let ok = b.eval(Op::IntLess, 1, &[raw, bits]); + let wide = b.eval(Op::IntZExt, size, &[ok]); + let mask = b.eval(Op::IntSub, size, &[Varnode::constant(0, size), wide]); + let narrow = b.eval(Op::SubPiece, size, &[raw, Varnode::constant(0, 1)]); + let count = b.eval(Op::IntAnd, size, &[narrow, mask]); + for k in 0..lanes { + let x = lane(d, k, size); + let r = b.eval(op, size, &[x, count]); + let r = b.eval(Op::IntAnd, size, &[r, mask]); + b.emit(Op::Copy, Some(lane(d, k, size)), &[r]); + } + return b.finish(true); + } + let n = match amount { + Operand::Imm(v) => *v as u64, + Operand::UImm(v) => *v, + Operand::Count(v) => *v as u64, + _ => return b.unimplemented(), + }; + for k in 0..lanes { + let x = lane(d, k, size); + let r = b.eval(op, size, &[x, Varnode::constant(n, 1)]); + b.emit(Op::Copy, Some(lane(d, k, size)), &[r]); + } + b.finish(true) + } + // Shifting the whole register by whole bytes. + "pslldq" | "psrldq" => { + let (Some(d), Some(amount)) = (ops.first().and_then(vector), ops.get(1)) else { + return b.unimplemented(); + }; + let n = match amount { + Operand::Imm(v) => (*v as u64).min(16), + Operand::UImm(v) => (*v).min(16), + Operand::Count(v) => (*v as u64).min(16), + _ => return b.unimplemented(), + }; + let mut bytes = Vec::new(); + for k in 0..16u64 { + let from = if m == "pslldq" { + k.checked_sub(n) + } else { + (k + n < 16).then_some(k + n) + }; + bytes.push(match from { + Some(index) => b.eval(Op::Copy, 1, &[lane(d, index, 1)]), + None => Varnode::constant(0, 1), + }); + } + for (k, v) in bytes.into_iter().enumerate() { + b.emit(Op::Copy, Some(lane(d, k as u64, 1)), &[v]); + } + b.finish(true) + } + _ => b.unimplemented(), + } +} + +/// The scalar floating point instructions, which operate on the low lane and +/// leave the rest of the register alone. +fn scalar_float(b: &mut Builder, i: &Insn) -> Option { + let m = i.mnemonic; + let ops = i.operands(); + let at = i.next(); + // The trailing `ss` or `sd` says single or double, scalar either way. + let size: u8 = if m.ends_with("ss") { + 4 + } else if m.ends_with("sd") { + 8 + } else if m.ends_with("ps") { + 4 + } else if m.ends_with("pd") { + 8 + } else { + 0 + }; + + // The scalar moves, which behave differently from register and from + // memory: from memory the rest of the register is cleared. + if matches!(m, "movss" | "movsd") { + let (dst, src) = (ops.first()?, ops.get(1)?); + let value = match src { + Operand::Reg(r) if r.class == RegClass::Vec => lane(r.num, 0, size), + Operand::Mem(mem) => { + let addr = address(b, mem, at)?; + b.eval(Op::Load, size, &[addr]) + } + _ => return None, + }; + match dst { + Operand::Reg(d) if d.class == RegClass::Vec => { + b.emit(Op::Copy, Some(lane(d.num, 0, size)), &[value]); + if matches!(src, Operand::Mem(_)) { + let mut off = size as u64; + while off < 16 { + let chunk = crate::lift::neon::aligned_chunk(off, 16); + b.emit( + Op::Copy, + Some(Varnode::register(vec_offset(d.num) + off, chunk)), + &[Varnode::constant(0, chunk)], + ); + off += chunk as u64; + } + } + } + Operand::Mem(mem) => { + let addr = address(b, mem, at)?; + b.emit(Op::Store, None, &[addr, value]); + } + _ => return None, + } + return Some(b.clone_finish()); + } + + let arithmetic = matches!( + &m[..m.len().saturating_sub(2)], + "add" | "sub" | "mul" | "div" | "min" | "max" | "sqrt" + ); + if arithmetic && size != 0 { + let d = match ops.first()? { + Operand::Reg(r) if r.class == RegClass::Vec => r.num, + _ => return None, + }; + let packed = m.ends_with("ps") || m.ends_with("pd"); + let count = if packed { 16 / size as u64 } else { 1 }; + let y = read_wide(b, ops.get(1)?, at)?; + let source = [b.eval(Op::Copy, 8, &[y[0]]), b.eval(Op::Copy, 8, &[y[1]])]; + for n in 0..count { + let x = lane(d, n, size); + let c = piece(b, source, n, size); + let op = match &m[..m.len() - 2] { + "add" => Op::FloatAdd, + "sub" => Op::FloatSub, + "mul" => Op::FloatMul, + "div" => Op::FloatDiv, + "min" => Op::FloatMin, + "max" => Op::FloatMax, + _ => Op::FloatSqrt, + }; + let r = if op == Op::FloatSqrt { + b.eval(op, size, &[c]) + } else { + b.eval(op, size, &[x, c]) + }; + b.emit(Op::Copy, Some(lane(d, n, size)), &[r]); + } + return Some(b.clone_finish()); + } + + // The ordered and unordered compares, which write the integer flags. + if matches!(m, "ucomiss" | "ucomisd" | "comiss" | "comisd") { + let width: u8 = if m.ends_with("ss") { 4 } else { 8 }; + let d = match ops.first()? { + Operand::Reg(r) if r.class == RegClass::Vec => r.num, + _ => return None, + }; + let x = lane(d, 0, width); + let c = match ops.get(1)? { + Operand::Reg(r) if r.class == RegClass::Vec => lane(r.num, 0, width), + Operand::Mem(mem) => { + let addr = address(b, mem, at)?; + b.eval(Op::Load, width, &[addr]) + } + _ => return None, + }; + use crate::lift::x86::{flag_af, flag_cf, flag_of, flag_pf, flag_sf, flag_zf}; + // Unordered sets all three of zero, parity and carry; otherwise they + // carry the comparison and the other three are cleared. + let nan_x = b.eval(Op::FloatNan, 1, &[x]); + let nan_y = b.eval(Op::FloatNan, 1, &[c]); + let unordered = b.eval(Op::BoolOr, 1, &[nan_x, nan_y]); + let less = b.eval(Op::FloatLess, 1, &[x, c]); + let equal = b.eval(Op::FloatEqual, 1, &[x, c]); + let zf = b.eval(Op::BoolOr, 1, &[unordered, equal]); + let cf = b.eval(Op::BoolOr, 1, &[unordered, less]); + b.emit(Op::Copy, Some(flag_zf()), &[zf]); + b.emit(Op::Copy, Some(flag_cf()), &[cf]); + b.emit(Op::Copy, Some(flag_pf()), &[unordered]); + for f in [flag_of(), flag_sf(), flag_af()] { + b.emit(Op::Copy, Some(f), &[Varnode::constant(0, 1)]); + } + return Some(b.clone_finish()); + } + + // The compares that write a mask rather than flags: the mnemonic carries + // the predicate. + if m.starts_with("cmp") && size != 0 && m.len() > 5 { + let predicate = &m[3..m.len() - 2]; + let packed = m.ends_with("ps") || m.ends_with("pd"); + let d = match ops.first()? { + Operand::Reg(r) if r.class == RegClass::Vec => r.num, + _ => return None, + }; + let y = read_wide(b, ops.get(1)?, at)?; + let source = [b.eval(Op::Copy, 8, &[y[0]]), b.eval(Op::Copy, 8, &[y[1]])]; + let count = if packed { 16 / size as u64 } else { 1 }; + for n in 0..count { + let x = lane(d, n, size); + let c = piece(b, source, n, size); + let truth = match predicate { + "eq" => b.eval(Op::FloatEqual, 1, &[x, c]), + "lt" => b.eval(Op::FloatLess, 1, &[x, c]), + "le" => b.eval(Op::FloatLessEqual, 1, &[x, c]), + "neq" => b.eval(Op::FloatNotEqual, 1, &[x, c]), + "nlt" => { + let less = b.eval(Op::FloatLess, 1, &[x, c]); + b.eval(Op::BoolNot, 1, &[less]) + } + "nle" => { + let le = b.eval(Op::FloatLessEqual, 1, &[x, c]); + b.eval(Op::BoolNot, 1, &[le]) + } + "unord" => { + let a = b.eval(Op::FloatNan, 1, &[x]); + let e = b.eval(Op::FloatNan, 1, &[c]); + b.eval(Op::BoolOr, 1, &[a, e]) + } + "ord" => { + let a = b.eval(Op::FloatNan, 1, &[x]); + let e = b.eval(Op::FloatNan, 1, &[c]); + let either = b.eval(Op::BoolOr, 1, &[a, e]); + b.eval(Op::BoolNot, 1, &[either]) + } + _ => return None, + }; + let mask = all_bits(b, truth, size); + b.emit(Op::Copy, Some(lane(d, n, size)), &[mask]); + } + return Some(b.clone_finish()); + } + + // The conversions. + let converted = match m { + "cvtsi2ss" | "cvtsi2sd" => { + let to: u8 = if m.ends_with("ss") { 4 } else { 8 }; + let d = match ops.first()? { + Operand::Reg(r) if r.class == RegClass::Vec => r.num, + _ => return None, + }; + let src = match ops.get(1)? { + Operand::Reg(r) => Varnode::register( + crate::lift::x86::gpr_offset(r.num), + if r.width == e5r_arch::Width::W64 { + 8 + } else { + 4 + }, + ), + Operand::Mem(mem) => { + let addr = address(b, mem, at)?; + let bytes = if mem.size == 0 { 4 } else { mem.size as u8 }; + b.eval(Op::Load, bytes, &[addr]) + } + _ => return None, + }; + let v = b.eval(Op::IntToFloat, to, &[src]); + b.emit(Op::Copy, Some(lane(d, 0, to)), &[v]); + true + } + "cvttss2si" | "cvttsd2si" | "cvtss2si" | "cvtsd2si" => { + let from: u8 = if m.contains("ss") { 4 } else { 8 }; + let (dest, dest_size) = match ops.first()? { + Operand::Reg(r) => ( + r.num, + if r.width == e5r_arch::Width::W64 { + 8u8 + } else { + 4 + }, + ), + _ => return None, + }; + let src = match ops.get(1)? { + Operand::Reg(r) if r.class == RegClass::Vec => lane(r.num, 0, from), + Operand::Mem(mem) => { + let addr = address(b, mem, at)?; + b.eval(Op::Load, from, &[addr]) + } + _ => return None, + }; + let v = b.eval(Op::FloatToInt, dest_size, &[src]); + b.emit( + Op::Copy, + Some(Varnode::register( + crate::lift::x86::gpr_offset(dest), + dest_size, + )), + &[v], + ); + if dest_size == 4 { + b.emit( + Op::Copy, + Some(Varnode::register(crate::lift::x86::gpr_offset(dest) + 4, 4)), + &[Varnode::constant(0, 4)], + ); + } + true + } + "cvtss2sd" | "cvtsd2ss" => { + let (from, to): (u8, u8) = if m == "cvtss2sd" { (4, 8) } else { (8, 4) }; + let d = match ops.first()? { + Operand::Reg(r) if r.class == RegClass::Vec => r.num, + _ => return None, + }; + let src = match ops.get(1)? { + Operand::Reg(r) if r.class == RegClass::Vec => lane(r.num, 0, from), + Operand::Mem(mem) => { + let addr = address(b, mem, at)?; + b.eval(Op::Load, from, &[addr]) + } + _ => return None, + }; + let v = b.eval(Op::FloatConvert, to, &[src]); + b.emit(Op::Copy, Some(lane(d, 0, to)), &[v]); + true + } + _ => false, + }; + if converted { + return Some(b.clone_finish()); + } + None +} + +/// One lane cut out of a pair of eight-byte temporaries. +fn piece(b: &mut Builder, source: [Varnode; 2], index: u64, size: u8) -> Varnode { + let per_half = 8 / size as u64; + let half = source[(index / per_half) as usize]; + let within = index % per_half; + if within == 0 && size == 8 { + return half; + } + let shifted = if within == 0 { + half + } else { + b.eval( + Op::IntRight, + 8, + &[half, Varnode::constant(within * size as u64 * 8, 1)], + ) + }; + b.eval(Op::SubPiece, size, &[shifted, Varnode::constant(0, 1)]) +} + +/// Clamp a wide lane into a narrow one, which is what packing does. +fn saturate(b: &mut Builder, v: Varnode, wide: u8, narrow: u8, signed: bool) -> Varnode { + let bits = narrow as u64 * 8; + let (low, high) = if signed { + ((!0u64 << (bits - 1)) & mask(wide), (1u64 << (bits - 1)) - 1) + } else { + (0, (1u64 << bits) - 1) + }; + let hi = Varnode::constant(high, wide); + let lo = Varnode::constant(low, wide); + let above = if signed { + b.eval(Op::IntSLess, 1, &[hi, v]) + } else { + b.eval(Op::IntLess, 1, &[hi, v]) + }; + let clamped_high = select(b, above, hi, v, wide); + let below = if signed { + b.eval(Op::IntSLess, 1, &[clamped_high, lo]) + } else { + b.eval(Op::IntLess, 1, &[clamped_high, lo]) + }; + let clamped = select(b, below, lo, clamped_high, wide); + b.eval(Op::SubPiece, narrow, &[clamped, Varnode::constant(0, 1)]) +} + +/// The mask of a value of this many bytes. +fn mask(size: u8) -> u64 { + if size >= 8 { + u64::MAX + } else { + (1u64 << (size as u64 * 8)) - 1 + } +} + +/// Spread a zero-or-one across a whole lane, which is what a packed compare +/// writes. +fn all_bits(b: &mut Builder, truth: Varnode, size: u8) -> Varnode { + let wide = if size == 1 { + truth + } else { + b.eval(Op::IntZExt, size, &[truth]) + }; + b.eval(Op::IntNegate, size, &[wide]) +} + +/// A branch-free choice between two lanes. +fn select(b: &mut Builder, cond: Varnode, t: Varnode, f: Varnode, size: u8) -> Varnode { + let mask = all_bits(b, cond, size); + let keep = b.eval(Op::IntAnd, size, &[t, mask]); + let inverse = b.eval(Op::IntNot, size, &[mask]); + let drop = b.eval(Op::IntAnd, size, &[f, inverse]); + b.eval(Op::IntOr, size, &[keep, drop]) +} diff --git a/crates/e5r-ir/src/lift/x86.rs b/crates/e5r-ir/src/lift/x86.rs new file mode 100644 index 0000000..ace1a67 --- /dev/null +++ b/crates/e5r-ir/src/lift/x86.rs @@ -0,0 +1,1260 @@ +//! x86-64 lifting. +//! +//! Covers the integer instruction set a compiler emits, with every flag the +//! instruction actually writes. The flags are the point: x86 code branches on +//! them constantly, and a lifter that sets only zero and sign turns a correct +//! comparison into a wrong one several instructions later. +//! +//! Two things the architecture does that need saying explicitly. A write to a +//! 32-bit register clears the upper half, and a write to an 8- or 16-bit one +//! does not, which the byte-addressed register file expresses directly. And a +//! shift by zero leaves the flags untouched rather than recomputing them, so +//! the flag updates are selected on the count rather than assumed. + +use e5r_arch::{Extend, Flow, Insn, Mem, Operand, Reg, RegClass, Width}; +use e5r_core::Addr; + +use crate::lift::{Builder, Lifted}; +use crate::op::{Op, Space, Varnode}; + +/// Byte offset of `rax`; the sixteen general registers are eight bytes each. +const R_BASE: u64 = 0; +/// `rsp`, which is register 4. +const RSP: u64 = 4 * 8; +/// The instruction pointer. +const RIP: u64 = 16 * 8; +/// The flags, one byte each. +const CF: u64 = 17 * 8; +const PF: u64 = CF + 1; +const AF: u64 = CF + 2; +const ZF: u64 = CF + 3; +const SF: u64 = CF + 4; +const OF: u64 = CF + 5; +const DF: u64 = CF + 6; +/// Byte offset of `xmm0`; vector registers are sixteen bytes each. +const XMM_BASE: u64 = 18 * 8; + +/// The byte offset of a vector register; they are sixteen bytes each. +pub fn vec_offset(n: u8) -> u64 { + XMM_BASE + n as u64 * 16 +} + +/// The byte offset of a general purpose register, in encoding order: `rax`, +/// `rcx`, `rdx`, `rbx`, `rsp`, `rbp`, `rsi`, `rdi`, then `r8` upwards. +pub fn gpr_offset(n: u8) -> u64 { + R_BASE + n as u64 * 8 +} + +/// The stack pointer's offset. +pub fn sp_offset() -> u64 { + RSP +} + +/// The instruction pointer's offset. +pub fn pc_offset() -> u64 { + RIP +} + +/// The carry flag. +pub fn flag_cf() -> Varnode { + Varnode::register(CF, 1) +} +/// The zero flag. +pub fn flag_zf() -> Varnode { + Varnode::register(ZF, 1) +} +/// The sign flag. +pub fn flag_sf() -> Varnode { + Varnode::register(SF, 1) +} +/// The overflow flag. +pub fn flag_of() -> Varnode { + Varnode::register(OF, 1) +} +/// The parity flag. +pub fn flag_pf() -> Varnode { + Varnode::register(PF, 1) +} +/// The adjust flag. +pub fn flag_af() -> Varnode { + Varnode::register(AF, 1) +} +/// The direction flag. +pub fn flag_df() -> Varnode { + Varnode::register(DF, 1) +} + +fn width_bytes(w: Width) -> u8 { + match w { + Width::W8 => 1, + Width::W16 => 2, + Width::W32 => 4, + Width::W64 => 8, + Width::W128 => 16, + } +} + +/// The varnode a register operand names. +/// +/// `ah` is byte one of `rax` rather than a register of its own, which the byte +/// addressing says without a special case anywhere else. +fn reg(r: Reg) -> Option { + let size = width_bytes(r.width); + Some(match r.class { + RegClass::Gpr => Varnode::register(gpr_offset(r.num), size), + RegClass::GprHigh => Varnode::register(gpr_offset(r.num & 3) + 1, 1), + RegClass::Vec => Varnode::register(XMM_BASE + r.num as u64 * 16, size.max(16)), + RegClass::Pc => return None, + _ => return None, + }) +} + +/// The whole register a one-byte operand sits in, and which byte of it that is. +/// +/// Dataflow versions the register file in eight-byte units and keeps one-byte +/// register locations for the flags, which sit above the registers. A varnode +/// naming `al` on its own is therefore storage no write to `rax` ever reaches: +/// the read misses its definition and the write is dead. +fn byte_slot(v: Varnode) -> Option<(Varnode, u64)> { + (v.space == Space::Register && v.size == 1 && v.offset < CF) + .then(|| (Varnode::register(v.offset & !7, 8), v.offset % 8)) +} + +/// Read a varnode, cutting a byte operand out of the register that holds it. +fn read(b: &mut Builder, v: Varnode) -> Varnode { + match byte_slot(v) { + Some((whole, index)) => b.eval(Op::SubPiece, 1, &[whole, Varnode::constant(index, 1)]), + None => v, + } +} + +/// Write a varnode, merging a byte operand back into the register that holds +/// it. The merge is what keeps the other seven bytes alive, which is the +/// machine's behaviour: a write to `al` leaves the rest of `rax` untouched. +fn write(b: &mut Builder, dest: Varnode, value: Varnode) { + let Some((whole, index)) = byte_slot(dest) else { + b.emit(Op::Copy, Some(dest), &[value]); + return; + }; + let byte = if value.size == 1 { + value + } else { + b.eval(Op::SubPiece, 1, &[value, Varnode::constant(0, 1)]) + }; + let shift = index * 8; + let kept = b.eval( + Op::IntAnd, + 8, + &[whole, Varnode::constant(!(0xffu64 << shift), 8)], + ); + let wide = b.eval(Op::IntZExt, 8, &[byte]); + let placed = if shift == 0 { + wide + } else { + b.eval(Op::IntLeft, 8, &[wide, Varnode::constant(shift, 1)]) + }; + b.emit(Op::IntOr, Some(whole), &[kept, placed]); +} + +/// Write a register, including the upper-half clear a 32-bit write performs. +fn write_reg(b: &mut Builder, r: Reg, value: Varnode) { + let Some(dest) = reg(r) else { return }; + write(b, dest, value); + if r.width == Width::W32 && r.class == RegClass::Gpr { + b.emit( + Op::Copy, + Some(Varnode::register(dest.offset + 4, 4)), + &[Varnode::constant(0, 4)], + ); + } +} + +/// The address a memory operand names. +/// +/// Returns `None` for a thread-local access through `fs` or `gs`, whose base is +/// not in the register file and cannot be invented. +pub(crate) fn address(b: &mut Builder, m: &Mem, at: Addr) -> Option { + if let Some(s) = m.seg { + // `fs` and `gs` are numbers 4 and 5 in the segment bank. + if matches!(s.num, 4 | 5) { + return None; + } + } + let p = b.ptr; + let mut addr = match m.base { + Some(r) if r.class == RegClass::Pc => Varnode::constant(at.get(), p), + Some(r) => Varnode { size: p, ..reg(r)? }, + None => Varnode::constant(0, p), + }; + if let Some((ix, ext, scale)) = m.index { + let base = reg(ix)?; + let narrow = if matches!(ext, Extend::Uxtw | Extend::Sxtw) { + Varnode { size: 4, ..base } + } else { + Varnode { size: p, ..base } + }; + let widened = if narrow.size == p { + narrow + } else { + let signed = matches!(ext, Extend::Sxtw); + b.eval(if signed { Op::IntSExt } else { Op::IntZExt }, p, &[narrow]) + }; + let scaled = if scale == 0 { + widened + } else { + b.eval( + Op::IntLeft, + p, + &[widened, Varnode::constant(scale as u64, 1)], + ) + }; + addr = if m.base.is_none() { + scaled + } else { + b.eval(Op::IntAdd, p, &[addr, scaled]) + }; + } + if m.disp != 0 { + addr = b.eval(Op::IntAdd, p, &[addr, Varnode::constant(m.disp as u64, p)]); + } + Some(addr) +} + +/// Read an operand, loading from memory when it names memory. +fn source(b: &mut Builder, op: &Operand, size: u8, at: Addr) -> Option { + Some(match op { + Operand::Reg(r) => read(b, reg(*r)?), + Operand::Imm(v) => Varnode::constant(*v as u64, size), + Operand::UImm(v) => Varnode::constant(*v, size), + Operand::Count(v) => Varnode::constant(*v as u64, size), + Operand::Addr(a) => Varnode::constant(a.get(), b.ptr), + Operand::Mem(m) => { + let addr = address(b, m, at)?; + let bytes = if m.size == 0 { + size + } else { + m.size.clamp(1, 16) as u8 + }; + b.eval(Op::Load, bytes, &[addr]) + } + _ => return None, + }) +} + +/// Write a value to whatever an operand names. +fn store(b: &mut Builder, op: &Operand, value: Varnode, at: Addr) -> Option<()> { + match op { + Operand::Reg(r) => write_reg(b, *r, value), + Operand::Mem(m) => { + let addr = address(b, m, at)?; + b.emit(Op::Store, None, &[addr, value]); + } + _ => return None, + } + Some(()) +} + +/// The size an instruction operates at, taken from its destination. +fn operand_size(ops: &[Operand]) -> Option { + for op in ops { + match op { + Operand::Reg(r) => return Some(width_bytes(r.width)), + Operand::Mem(m) if m.size > 0 => return Some(m.size.clamp(1, 16) as u8), + _ => {} + } + } + None +} + +/// Choose between two values without a branch: `cond ? t : f`. +/// +/// Used wherever the machine makes an update conditional, which is a shift by +/// a variable count leaving the flags alone, and `cmov`. +fn select(b: &mut Builder, cond: Varnode, t: Varnode, f: Varnode, size: u8) -> Varnode { + let wide = if size == 1 { + cond + } else { + b.eval(Op::IntZExt, size, &[cond]) + }; + // Negating a zero-or-one gives all zeroes or all ones, which is the mask. + let mask = b.eval(Op::IntNegate, size, &[wide]); + let keep = b.eval(Op::IntAnd, size, &[t, mask]); + let inverse = b.eval(Op::IntNot, size, &[mask]); + let drop = b.eval(Op::IntAnd, size, &[f, inverse]); + b.eval(Op::IntOr, size, &[keep, drop]) +} + +/// The sign bit of a value, as one byte. +fn sign_of(b: &mut Builder, v: Varnode) -> Varnode { + let shift = Varnode::constant(v.size as u64 * 8 - 1, 1); + let high = b.eval(Op::IntRight, v.size, &[v, shift]); + b.eval(Op::IntNotEqual, 1, &[high, Varnode::constant(0, v.size)]) +} + +/// Set sign, zero and parity from a result. Every arithmetic and logical +/// instruction writes these three. +fn set_result_flags(b: &mut Builder, result: Varnode) { + let sf = sign_of(b, result); + b.emit(Op::Copy, Some(flag_sf()), &[sf]); + let zf = b.eval( + Op::IntEqual, + 1, + &[result, Varnode::constant(0, result.size)], + ); + b.emit(Op::Copy, Some(flag_zf()), &[zf]); + // Parity is over the low byte only, on every width. + let low = b.eval( + Op::IntAnd, + result.size, + &[result, Varnode::constant(0xff, result.size)], + ); + let ones = b.eval(Op::PopCount, 1, &[low]); + let odd = b.eval(Op::IntAnd, 1, &[ones, Varnode::constant(1, 1)]); + let pf = b.eval(Op::IntEqual, 1, &[odd, Varnode::constant(0, 1)]); + b.emit(Op::Copy, Some(flag_pf()), &[pf]); +} + +/// The adjust flag: a carry out of bit three, which is the same bit of +/// `x ^ y ^ result` on an addition and on a subtraction. +fn set_adjust_flag(b: &mut Builder, x: Varnode, y: Varnode, result: Varnode) { + let size = result.size; + let xy = b.eval(Op::IntXor, size, &[x, y]); + let all = b.eval(Op::IntXor, size, &[xy, result]); + let bit = b.eval(Op::IntAnd, size, &[all, Varnode::constant(0x10, size)]); + let af = b.eval(Op::IntNotEqual, 1, &[bit, Varnode::constant(0, size)]); + b.emit(Op::Copy, Some(flag_af()), &[af]); +} + +/// Flags for an addition. +fn set_add_flags(b: &mut Builder, x: Varnode, y: Varnode, result: Varnode) { + let cf = b.eval(Op::IntCarry, 1, &[x, y]); + b.emit(Op::Copy, Some(flag_cf()), &[cf]); + let of = b.eval(Op::IntSCarry, 1, &[x, y]); + b.emit(Op::Copy, Some(flag_of()), &[of]); + set_adjust_flag(b, x, y, result); + set_result_flags(b, result); +} + +/// Flags for a subtraction. The carry flag means a borrow, which is an +/// unsigned comparison rather than a carry out. +fn set_sub_flags(b: &mut Builder, x: Varnode, y: Varnode, result: Varnode) { + let cf = b.eval(Op::IntLess, 1, &[x, y]); + b.emit(Op::Copy, Some(flag_cf()), &[cf]); + let of = b.eval(Op::IntSBorrow, 1, &[x, y]); + b.emit(Op::Copy, Some(flag_of()), &[of]); + set_adjust_flag(b, x, y, result); + set_result_flags(b, result); +} + +/// Flags for a logical operation: carry and overflow cleared, the rest from +/// the result. +fn set_logic_flags(b: &mut Builder, result: Varnode) { + b.emit(Op::Copy, Some(flag_cf()), &[Varnode::constant(0, 1)]); + b.emit(Op::Copy, Some(flag_of()), &[Varnode::constant(0, 1)]); + b.emit(Op::Copy, Some(flag_af()), &[Varnode::constant(0, 1)]); + set_result_flags(b, result); +} + +/// Overflow and carry from the sign bits, for the forms that carry an incoming +/// flag and so cannot use the two-input operations. +fn set_signed_overflow(b: &mut Builder, x: Varnode, y: Varnode, result: Varnode, sub: bool) { + let sx = sign_of(b, x); + let sy = sign_of(b, y); + let sr = sign_of(b, result); + // An addition overflows when the addends agree in sign and the result does + // not; a subtraction when they disagree and the result follows the second. + let same = b.eval(Op::IntEqual, 1, &[sx, sy]); + let differs = b.eval(Op::IntNotEqual, 1, &[sr, sx]); + let of = if sub { + let opposite = b.eval(Op::BoolNot, 1, &[same]); + b.eval(Op::BoolAnd, 1, &[opposite, differs]) + } else { + b.eval(Op::BoolAnd, 1, &[same, differs]) + }; + b.emit(Op::Copy, Some(flag_of()), &[of]); +} + +/// The condition a `jcc`, `setcc` or `cmovcc` tests, taken from the suffix. +fn condition(b: &mut Builder, suffix: &str) -> Option { + let (cf, zf, sf, of, pf) = (flag_cf(), flag_zf(), flag_sf(), flag_of(), flag_pf()); + let not = |b: &mut Builder, v: Varnode| b.eval(Op::BoolNot, 1, &[v]); + Some(match suffix { + "o" => of, + "no" => not(b, of), + "b" | "c" | "nae" => cf, + "ae" | "nb" | "nc" => not(b, cf), + "e" | "z" => zf, + "ne" | "nz" => not(b, zf), + "be" | "na" => b.eval(Op::BoolOr, 1, &[cf, zf]), + "a" | "nbe" => { + let ncf = not(b, cf); + let nzf = not(b, zf); + b.eval(Op::BoolAnd, 1, &[ncf, nzf]) + } + "s" => sf, + "ns" => not(b, sf), + "p" | "pe" => pf, + "np" | "po" => not(b, pf), + "l" | "nge" => b.eval(Op::IntNotEqual, 1, &[sf, of]), + "ge" | "nl" => b.eval(Op::IntEqual, 1, &[sf, of]), + "le" | "ng" => { + let lt = b.eval(Op::IntNotEqual, 1, &[sf, of]); + b.eval(Op::BoolOr, 1, &[zf, lt]) + } + "g" | "nle" => { + let ge = b.eval(Op::IntEqual, 1, &[sf, of]); + let nzf = not(b, zf); + b.eval(Op::BoolAnd, 1, &[nzf, ge]) + } + _ => return None, + }) +} + +/// The suffix of a mnemonic beginning with `prefix`. +fn suffix<'a>(mnemonic: &'a str, prefix: &str) -> Option<&'a str> { + mnemonic + .strip_prefix(prefix) + .filter(|s| !s.is_empty() && !s.contains(' ')) +} + +/// Write a pointer-wide value into a register that holds one. +/// +/// In protected mode that is four bytes of an eight-byte slot, and the rest of +/// the slot does not exist on the machine. Zeroing it rather than leaving it +/// makes the write a whole-register write, which is the same rule the +/// architecture already applies to every 32-bit destination. Without it every +/// stack adjustment reads back as `sp & 0xffffffff00000000 | ...`, which is +/// true of the model and not of the machine. +fn write_ptr(b: &mut Builder, offset: u64, value: Varnode) { + let p = b.ptr; + b.emit(Op::Copy, Some(Varnode::register(offset, p)), &[value]); + if p < 8 { + b.emit( + Op::Copy, + Some(Varnode::register(offset + p as u64, 8 - p)), + &[Varnode::constant(0, 8 - p)], + ); + } +} + +/// Push a value, which is one pointer wide: eight bytes in long mode, four in +/// protected mode. +fn push(b: &mut Builder, value: Varnode) { + let p = b.ptr; + let sp = Varnode::register(RSP, p); + let lowered = b.eval(Op::IntSub, p, &[sp, Varnode::constant(p as u64, p)]); + write_ptr(b, RSP, lowered); + b.emit(Op::Store, None, &[Varnode::register(RSP, p), value]); +} + +/// Pop into a varnode. +fn pop(b: &mut Builder, size: u8) -> Varnode { + let p = b.ptr; + let sp = Varnode::register(RSP, p); + let value = b.eval(Op::Load, size, &[sp]); + let raised = b.eval(Op::IntAdd, p, &[sp, Varnode::constant(p as u64, p)]); + write_ptr(b, RSP, raised); + value +} + +/// Lift one x86-64 instruction. +pub fn lift(i: &Insn) -> Lifted { + lift_sized(i, 8) +} + +/// Lift one i386 instruction. +/// +/// The same decoder and the same rules at half the pointer width. The two +/// modes differ in what a push moves the stack by, how wide a return address +/// is, and how wide an effective address is computed -- everything the +/// architecture calls the operand size is already carried by the operands. +pub fn lift32(i: &Insn) -> Lifted { + lift_sized(i, 4) +} + +fn lift_sized(i: &Insn, ptr: u8) -> Lifted { + let mut b = Builder::sized(i.addr, ptr); + let ops = i.operands(); + let next = Varnode::constant(i.next().get(), ptr); + let at = i.next(); + + // A repeated string operation is a loop, not one instruction, and is not + // modelled rather than modelled as a single pass. + if matches!(i.prefix, Some("rep") | Some("repne") | Some("repe")) { + return b.unimplemented(); + } + + match i.flow { + Flow::Branch(t) => { + b.emit(Op::Branch, None, &[Varnode::constant(t.get(), ptr)]); + return b.finish(true); + } + Flow::CondBranch(t) => { + let Some(s) = suffix(i.mnemonic, "j") else { + return b.unimplemented(); + }; + let Some(cond) = condition(&mut b, s) else { + return b.unimplemented(); + }; + b.emit(Op::CBranch, None, &[Varnode::constant(t.get(), ptr), cond]); + return b.finish(true); + } + Flow::Call(t) => { + push(&mut b, next); + b.emit( + Op::Call, + Some(Varnode::register(gpr_offset(0), ptr)), + &[Varnode::constant(t.get(), ptr)], + ); + clobber(&mut b); + return b.finish(true); + } + Flow::IndirectCall => { + let Some(target) = ops.first().and_then(|o| source(&mut b, o, ptr, at)) else { + return b.unimplemented(); + }; + push(&mut b, next); + b.emit( + Op::CallInd, + Some(Varnode::register(gpr_offset(0), ptr)), + &[target], + ); + clobber(&mut b); + return b.finish(true); + } + Flow::IndirectBranch => { + let Some(target) = ops.first().and_then(|o| source(&mut b, o, ptr, at)) else { + return b.unimplemented(); + }; + b.emit(Op::BranchInd, None, &[target]); + return b.finish(true); + } + Flow::Return => { + let target = pop(&mut b, ptr); + // `ret imm16` also drops the arguments the caller pushed. + if let Some(Operand::Imm(n)) = ops.first() { + let sp = Varnode::register(RSP, ptr); + let raised = b.eval(Op::IntAdd, ptr, &[sp, Varnode::constant(*n as u64, ptr)]); + write_ptr(&mut b, RSP, raised); + } + b.emit(Op::Return, None, &[target]); + return b.finish(true); + } + Flow::Trap => { + // A trap raises an exception and nothing after it runs, which the + // flow already says. It changes no register on the way, so there + // is nothing to model and nothing missing: complete, with no ops. + return b.finish(true); + } + Flow::Syscall => { + // What the instruction itself does to the register file is + // specified: RCX takes the return address and R11 the flags. What + // the kernel does is not knowable from here, so the result and + // everything the convention lets it change is undefined rather + // than guessed, and the flow still stops the interpreter. + // Only `syscall` does that to RCX and R11. An i386 image reaches + // the kernel through `int 0x80`, which leaves them alone, so the + // one thing both forms have in common is the result. + if ptr == 8 { + let next = Varnode::constant(i.next().get(), 8); + b.emit(Op::Copy, Some(Varnode::register(gpr_offset(1), 8)), &[next]); + b.emit( + Op::Undefine, + Some(Varnode::register(gpr_offset(11), 8)), + &[], + ); + } + b.emit(Op::Undefine, Some(Varnode::register(gpr_offset(0), 8)), &[]); + return b.finish(true); + } + Flow::Next => {} + } + + // Anything naming a vector register goes to the SIMD lifter. + if crate::lift::sse::handles(i) { + return crate::lift::sse::lift(b, i); + } + + let size = operand_size(ops).unwrap_or(8); + let m = i.mnemonic; + + match m { + "nop" | "endbr64" | "endbr32" | "hlt" | "pause" | "cld" | "std" | "lfence" | "mfence" + | "sfence" | "prefetch" | "prefetchnta" | "prefetcht0" | "prefetcht1" | "prefetcht2" => { + if m == "cld" { + b.emit(Op::Copy, Some(flag_df()), &[Varnode::constant(0, 1)]); + } + if m == "std" { + b.emit(Op::Copy, Some(flag_df()), &[Varnode::constant(1, 1)]); + } + b.finish(true) + } + "mov" | "movabs" => match two(&mut b, ops, size, at) { + Some((_, src)) => { + if store(&mut b, &ops[0], src, at).is_none() { + return b.unimplemented(); + } + b.finish(true) + } + None => b.unimplemented(), + }, + "lea" => { + let Some(Operand::Mem(mem)) = ops.get(1) else { + return b.unimplemented(); + }; + let Some(addr) = address(&mut b, mem, at) else { + return b.unimplemented(); + }; + // Narrowing by relabelling the varnode names a location nothing + // ever wrote: the address was computed at eight bytes, and its low + // four are a piece of it, not a value of their own. + let value = if size == addr.size { + addr + } else { + b.eval(Op::SubPiece, size, &[addr, Varnode::constant(0, 1)]) + }; + match store(&mut b, &ops[0], value, at) { + Some(()) => b.finish(true), + None => b.unimplemented(), + } + } + "movzx" | "movsx" | "movsxd" => { + let Some(dest) = ops.first() else { + return b.unimplemented(); + }; + let dest_size = match dest { + Operand::Reg(r) => width_bytes(r.width), + _ => return b.unimplemented(), + }; + let Some(src) = ops.get(1).and_then(|o| source(&mut b, o, dest_size, at)) else { + return b.unimplemented(); + }; + let widened = if src.size >= dest_size { + Varnode { + size: dest_size, + ..src + } + } else { + let op = if m == "movzx" { + Op::IntZExt + } else { + Op::IntSExt + }; + b.eval(op, dest_size, &[src]) + }; + match store(&mut b, dest, widened, at) { + Some(()) => b.finish(true), + None => b.unimplemented(), + } + } + "push" => { + let Some(v) = ops.first().and_then(|o| source(&mut b, o, ptr, at)) else { + return b.unimplemented(); + }; + let wide = if v.size == ptr { + v + } else { + b.eval(Op::IntSExt, ptr, &[v]) + }; + push(&mut b, wide); + b.finish(true) + } + "pop" => { + let v = pop(&mut b, ptr); + match store(&mut b, &ops[0], v, at) { + Some(()) => b.finish(true), + None => b.unimplemented(), + } + } + "leave" => { + // `mov rsp, rbp` then `pop rbp`. + let bp = Varnode::register(gpr_offset(5), ptr); + write_ptr(&mut b, RSP, bp); + let saved = pop(&mut b, ptr); + write_ptr(&mut b, gpr_offset(5), saved); + b.finish(true) + } + // Bit test: the carry flag takes the selected bit and the rest are + // left undefined, which is what the manual says and what the IR can + // say exactly. The offset is taken modulo the operand width for a + // register destination, which is the only form the decoder produces + // here; a memory destination addresses a bit string and is declined. + "bt" => { + let (Some(x), Some(count)) = ( + ops.first().and_then(|o| source(&mut b, o, size, at)), + ops.get(1).and_then(|o| source(&mut b, o, size, at)), + ) else { + return b.unimplemented(); + }; + if !matches!(ops.first(), Some(Operand::Reg(_))) { + return b.unimplemented(); + } + let bits = u64::from(size) * 8 - 1; + let which = b.eval(Op::IntAnd, size, &[count, Varnode::constant(bits, size)]); + let moved = b.eval(Op::IntRight, size, &[x, which]); + let bit = b.eval(Op::IntAnd, size, &[moved, Varnode::constant(1, size)]); + let one = b.eval(Op::IntNotEqual, 1, &[bit, Varnode::constant(0, size)]); + b.emit(Op::Copy, Some(flag_cf()), &[one]); + for f in [flag_of(), flag_sf(), flag_zf(), flag_af(), flag_pf()] { + b.emit(Op::Undefine, Some(f), &[]); + } + b.finish(true) + } + + "add" | "sub" | "and" | "or" | "xor" | "cmp" | "test" => { + let Some((x, y)) = two(&mut b, ops, size, at) else { + return b.unimplemented(); + }; + let (op, kind) = match m { + "add" => (Op::IntAdd, Kind::Add), + "sub" | "cmp" => (Op::IntSub, Kind::Sub), + "and" | "test" => (Op::IntAnd, Kind::Logic), + "or" => (Op::IntOr, Kind::Logic), + _ => (Op::IntXor, Kind::Logic), + }; + let result = b.eval(op, size, &[x, y]); + match kind { + Kind::Add => set_add_flags(&mut b, x, y, result), + Kind::Sub => set_sub_flags(&mut b, x, y, result), + Kind::Logic => set_logic_flags(&mut b, result), + } + // `cmp` and `test` discard the result and keep only the flags. + if m != "cmp" && m != "test" && store(&mut b, &ops[0], result, at).is_none() { + return b.unimplemented(); + } + b.finish(true) + } + "adc" | "sbb" => { + let Some((x, y)) = two(&mut b, ops, size, at) else { + return b.unimplemented(); + }; + let carry_in = b.eval(Op::IntZExt, size, &[flag_cf()]); + let (first, second) = if m == "adc" { + (Op::IntAdd, Op::IntAdd) + } else { + (Op::IntSub, Op::IntSub) + }; + let partial = b.eval(first, size, &[x, y]); + let result = b.eval(second, size, &[partial, carry_in]); + // The carry out happens in either step, so both are tested. + let (c1, c2) = if m == "adc" { + ( + b.eval(Op::IntCarry, 1, &[x, y]), + b.eval(Op::IntCarry, 1, &[partial, carry_in]), + ) + } else { + ( + b.eval(Op::IntLess, 1, &[x, y]), + b.eval(Op::IntLess, 1, &[partial, carry_in]), + ) + }; + let cf = b.eval(Op::BoolOr, 1, &[c1, c2]); + b.emit(Op::Copy, Some(flag_cf()), &[cf]); + set_signed_overflow(&mut b, x, y, result, m == "sbb"); + set_adjust_flag(&mut b, x, y, result); + set_result_flags(&mut b, result); + if store(&mut b, &ops[0], result, at).is_none() { + return b.unimplemented(); + } + b.finish(true) + } + "inc" | "dec" => { + let Some(x) = ops.first().and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + let one = Varnode::constant(1, size); + let op = if m == "inc" { Op::IntAdd } else { Op::IntSub }; + let result = b.eval(op, size, &[x, one]); + // These leave the carry flag alone, which is the whole reason a + // compiler prefers them to `add`. + let of = b.eval( + if m == "inc" { + Op::IntSCarry + } else { + Op::IntSBorrow + }, + 1, + &[x, one], + ); + b.emit(Op::Copy, Some(flag_of()), &[of]); + set_adjust_flag(&mut b, x, one, result); + set_result_flags(&mut b, result); + if store(&mut b, &ops[0], result, at).is_none() { + return b.unimplemented(); + } + b.finish(true) + } + "neg" => { + let Some(x) = ops.first().and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + let zero = Varnode::constant(0, size); + let result = b.eval(Op::IntNegate, size, &[x]); + // The carry flag is set unless the operand was zero. + let cf = b.eval(Op::IntNotEqual, 1, &[x, zero]); + b.emit(Op::Copy, Some(flag_cf()), &[cf]); + let of = b.eval(Op::IntSBorrow, 1, &[zero, x]); + b.emit(Op::Copy, Some(flag_of()), &[of]); + set_adjust_flag(&mut b, zero, x, result); + set_result_flags(&mut b, result); + if store(&mut b, &ops[0], result, at).is_none() { + return b.unimplemented(); + } + b.finish(true) + } + "not" => { + let Some(x) = ops.first().and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + let result = b.eval(Op::IntNot, size, &[x]); + // `not` writes no flags at all. + if store(&mut b, &ops[0], result, at).is_none() { + return b.unimplemented(); + } + b.finish(true) + } + "xchg" => { + let Some((x, y)) = two(&mut b, ops, size, at) else { + return b.unimplemented(); + }; + let tx = b.eval(Op::Copy, size, &[x]); + let ty = b.eval(Op::Copy, size, &[y]); + if store(&mut b, &ops[0], ty, at).is_none() || store(&mut b, &ops[1], tx, at).is_none() + { + return b.unimplemented(); + } + b.finish(true) + } + "bswap" => { + let Some(x) = ops.first().and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + let mut result: Option = None; + for byte in 0..size { + let from = Varnode::constant(byte as u64 * 8, 1); + let to = (size - 1 - byte) as u64 * 8; + let shifted = b.eval(Op::IntRight, size, &[x, from]); + let isolated = b.eval(Op::IntAnd, size, &[shifted, Varnode::constant(0xff, size)]); + let placed = b.eval(Op::IntLeft, size, &[isolated, Varnode::constant(to, 1)]); + result = Some(match result { + None => placed, + Some(acc) => b.eval(Op::IntOr, size, &[acc, placed]), + }); + } + match result.and_then(|r| store(&mut b, &ops[0], r, at)) { + Some(()) => b.finish(true), + None => b.unimplemented(), + } + } + "shl" | "sal" | "shr" | "sar" | "rol" | "ror" => shift(b, i, ops, size, at), + "imul" => imul(b, i, ops, size, at), + "mul" => mul_one(b, ops, size, at), + "div" | "idiv" => divide(b, ops, size, at, m == "idiv"), + "cdq" | "cqo" | "cwd" => { + // Sign-extend the accumulator into the high register. + let acc_size: u8 = match m { + "cdq" => 4, + "cwd" => 2, + _ => 8, + }; + let acc = read(&mut b, Varnode::register(gpr_offset(0), acc_size)); + let sign = sign_of(&mut b, acc); + let wide = if acc_size == 1 { + sign + } else { + b.eval(Op::IntZExt, acc_size, &[sign]) + }; + let mask = b.eval(Op::IntNegate, acc_size, &[wide]); + let dx = Varnode::register(gpr_offset(2), acc_size); + b.emit(Op::Copy, Some(dx), &[mask]); + if acc_size == 4 { + b.emit( + Op::Copy, + Some(Varnode::register(gpr_offset(2) + 4, 4)), + &[Varnode::constant(0, 4)], + ); + } + b.finish(true) + } + "cdqe" => { + let eax = Varnode::register(gpr_offset(0), 4); + let wide = b.eval(Op::IntSExt, 8, &[eax]); + b.emit(Op::Copy, Some(Varnode::register(gpr_offset(0), 8)), &[wide]); + b.finish(true) + } + "cwde" => { + let ax = Varnode::register(gpr_offset(0), 2); + let wide = b.eval(Op::IntSExt, 4, &[ax]); + write_reg(&mut b, Reg::gpr(0, Width::W32), wide); + b.finish(true) + } + "popcnt" => { + let Some(x) = ops.get(1).and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + let count = b.eval(Op::PopCount, size, &[x]); + let zf = b.eval(Op::IntEqual, 1, &[x, Varnode::constant(0, size)]); + b.emit(Op::Copy, Some(flag_zf()), &[zf]); + for f in [flag_cf(), flag_of(), flag_sf(), flag_af(), flag_pf()] { + b.emit(Op::Copy, Some(f), &[Varnode::constant(0, 1)]); + } + match store(&mut b, &ops[0], count, at) { + Some(()) => b.finish(true), + None => b.unimplemented(), + } + } + "lzcnt" | "tzcnt" | "bsf" | "bsr" => { + let Some(x) = ops.get(1).and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + let zf = b.eval(Op::IntEqual, 1, &[x, Varnode::constant(0, size)]); + let value = match m { + "lzcnt" => b.eval(Op::LzCount, size, &[x]), + "bsr" => { + // The index of the highest set bit: width minus one minus + // the leading zero count. + let lz = b.eval(Op::LzCount, size, &[x]); + b.eval( + Op::IntSub, + size, + &[Varnode::constant(size as u64 * 8 - 1, size), lz], + ) + } + _ => { + // Trailing zeroes: the leading zeroes of the lowest set bit + // isolated by `x & -x`, counted from the other end. + let neg = b.eval(Op::IntNegate, size, &[x]); + let lowest = b.eval(Op::IntAnd, size, &[x, neg]); + let lz = b.eval(Op::LzCount, size, &[lowest]); + b.eval( + Op::IntSub, + size, + &[Varnode::constant(size as u64 * 8 - 1, size), lz], + ) + } + }; + b.emit(Op::Copy, Some(flag_zf()), &[zf]); + // For a zero input the destination is undefined on `bsf`/`bsr` and + // the width on `lzcnt`/`tzcnt`; writing the computed value is + // within what the architecture allows in both cases. + match store(&mut b, &ops[0], value, at) { + Some(()) => b.finish(true), + None => b.unimplemented(), + } + } + _ if m.starts_with("set") => { + let Some(s) = suffix(m, "set").and_then(|s| condition(&mut b, s)) else { + return b.unimplemented(); + }; + match store(&mut b, &ops[0], s, at) { + Some(()) => b.finish(true), + None => b.unimplemented(), + } + } + _ if m.starts_with("cmov") => { + let Some((dst, src)) = two(&mut b, ops, size, at) else { + return b.unimplemented(); + }; + let Some(cond) = suffix(m, "cmov").and_then(|s| condition(&mut b, s)) else { + return b.unimplemented(); + }; + let chosen = select(&mut b, cond, src, dst, size); + match store(&mut b, &ops[0], chosen, at) { + Some(()) => b.finish(true), + None => b.unimplemented(), + } + } + _ => b.unimplemented(), + } +} + +/// Say that a call left the caller-saved registers holding anything. +fn clobber(b: &mut Builder) { + let abi = crate::abi::of(&e5r_core::Arch::X86_64); + for offset in &abi.caller_saved { + if *offset == gpr_offset(0) { + continue; + } + b.emit(Op::Undefine, Some(Varnode::register(*offset, 8)), &[]); + } + for flag in [ + flag_cf(), + flag_pf(), + flag_af(), + flag_zf(), + flag_sf(), + flag_of(), + ] { + b.emit(Op::Undefine, Some(flag), &[]); + } +} + +/// Which flag rule an arithmetic form follows. +enum Kind { + Add, + Sub, + Logic, +} + +/// The two operands of a binary form, destination first. +fn two(b: &mut Builder, ops: &[Operand], size: u8, at: Addr) -> Option<(Varnode, Varnode)> { + let x = source(b, ops.first()?, size, at)?; + let y = source(b, ops.get(1)?, size, at)?; + let y = size_matched(b, y, size); + Some((x, y)) +} + +/// Widen a source that is narrower than the operation, which happens for the +/// forms taking a byte immediate. +fn size_matched(b: &mut Builder, y: Varnode, size: u8) -> Varnode { + if y.size == size { + y + } else if y.is_const() { + Varnode { size, ..y } + } else { + b.eval(Op::IntSExt, size, &[y]) + } +} + +/// Shifts and rotates. +fn shift(mut b: Builder, i: &Insn, ops: &[Operand], size: u8, at: Addr) -> Lifted { + let Some(x) = ops.first().and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + // A one-operand form shifts by one. + let raw = match ops.get(1) { + Some(o) => match source(&mut b, o, 1, at) { + Some(v) => v, + None => return b.unimplemented(), + }, + None => Varnode::constant(1, 1), + }; + // The count is masked to the operand width, five bits below 64. + let limit = if size == 8 { 0x3f } else { 0x1f }; + let count = b.eval(Op::IntAnd, 1, &[raw, Varnode::constant(limit, 1)]); + let bits = size as u64 * 8; + let m = i.mnemonic; + + let result = match m { + "shl" | "sal" => b.eval(Op::IntLeft, size, &[x, count]), + "shr" => b.eval(Op::IntRight, size, &[x, count]), + "sar" => b.eval(Op::IntSRight, size, &[x, count]), + "rol" | "ror" => { + let other = b.eval(Op::IntSub, 1, &[Varnode::constant(bits, 1), count]); + let (a, c) = if m == "rol" { + ( + b.eval(Op::IntLeft, size, &[x, count]), + b.eval(Op::IntRight, size, &[x, other]), + ) + } else { + ( + b.eval(Op::IntRight, size, &[x, count]), + b.eval(Op::IntLeft, size, &[x, other]), + ) + }; + // A rotate by zero would shift by the full width on the other + // side, which this IR defines as zero, so the two halves still + // recombine into the original. + let joined = b.eval(Op::IntOr, size, &[a, c]); + let zero_count = b.eval(Op::IntEqual, 1, &[count, Varnode::constant(0, 1)]); + select(&mut b, zero_count, x, joined, size) + } + _ => return b.unimplemented(), + }; + + // The carry flag takes the last bit shifted out. + let cf_new = match m { + "shl" | "sal" | "rol" => { + let back = b.eval(Op::IntSub, 1, &[Varnode::constant(bits, 1), count]); + let moved = b.eval(Op::IntRight, size, &[x, back]); + b.eval( + Op::IntAnd, + 1, + &[Varnode { size: 1, ..moved }, Varnode::constant(1, 1)], + ) + } + _ => { + let back = b.eval(Op::IntSub, 1, &[count, Varnode::constant(1, 1)]); + let moved = b.eval(Op::IntRight, size, &[x, back]); + b.eval( + Op::IntAnd, + 1, + &[Varnode { size: 1, ..moved }, Varnode::constant(1, 1)], + ) + } + }; + // A shift by zero leaves every flag alone, so each update is selected on + // the count rather than written unconditionally. + let shifted = b.eval(Op::IntNotEqual, 1, &[count, Varnode::constant(0, 1)]); + let cf = select(&mut b, shifted, cf_new, flag_cf(), 1); + b.emit(Op::Copy, Some(flag_cf()), &[cf]); + + if matches!(m, "rol" | "ror") { + // The rotates write only the carry and overflow flags. + let top = sign_of(&mut b, result); + let of_new = b.eval(Op::BoolXor, 1, &[top, cf]); + let of = select(&mut b, shifted, of_new, flag_of(), 1); + b.emit(Op::Copy, Some(flag_of()), &[of]); + } else { + let sf_new = sign_of(&mut b, result); + let sf = select(&mut b, shifted, sf_new, flag_sf(), 1); + b.emit(Op::Copy, Some(flag_sf()), &[sf]); + let zf_new = b.eval(Op::IntEqual, 1, &[result, Varnode::constant(0, size)]); + let zf = select(&mut b, shifted, zf_new, flag_zf(), 1); + b.emit(Op::Copy, Some(flag_zf()), &[zf]); + let low = b.eval(Op::IntAnd, size, &[result, Varnode::constant(0xff, size)]); + let ones = b.eval(Op::PopCount, 1, &[low]); + let odd = b.eval(Op::IntAnd, 1, &[ones, Varnode::constant(1, 1)]); + let pf_new = b.eval(Op::IntEqual, 1, &[odd, Varnode::constant(0, 1)]); + let pf = select(&mut b, shifted, pf_new, flag_pf(), 1); + b.emit(Op::Copy, Some(flag_pf()), &[pf]); + // Overflow is defined only for a shift by one; any value is allowed + // otherwise, so the one-bit rule is used throughout. + let of_new = match m { + "shl" | "sal" => { + let top = sign_of(&mut b, result); + b.eval(Op::BoolXor, 1, &[top, cf]) + } + "shr" => sign_of(&mut b, x), + _ => Varnode::constant(0, 1), + }; + let of = select(&mut b, shifted, of_new, flag_of(), 1); + b.emit(Op::Copy, Some(flag_of()), &[of]); + } + + if store(&mut b, &ops[0], result, at).is_none() { + return b.unimplemented(); + } + b.finish(true) +} + +/// `imul` in its one, two and three operand forms. +fn imul(mut b: Builder, _i: &Insn, ops: &[Operand], size: u8, at: Addr) -> Lifted { + if ops.len() == 1 { + // The one-operand form multiplies the accumulator and writes both + // halves of the product. + let Some(y) = source(&mut b, &ops[0], size, at) else { + return b.unimplemented(); + }; + let acc = read(&mut b, Varnode::register(gpr_offset(0), size)); + let low = b.eval(Op::IntMul, size, &[acc, y]); + let high = b.eval(Op::IntSMulHigh, size, &[acc, y]); + let expected = b.eval( + Op::IntSRight, + size, + &[low, Varnode::constant(size as u64 * 8 - 1, 1)], + ); + let fits = b.eval(Op::IntEqual, 1, &[high, expected]); + let overflow = b.eval(Op::BoolNot, 1, &[fits]); + b.emit(Op::Copy, Some(flag_cf()), &[overflow]); + b.emit(Op::Copy, Some(flag_of()), &[overflow]); + if size == 1 { + // The byte form writes the whole product to `ax`. + let wide = b.eval(Op::IntSExt, 2, &[low]); + b.emit(Op::Copy, Some(Varnode::register(gpr_offset(0), 2)), &[wide]); + } else { + write_reg(&mut b, Reg::gpr(0, width_of(size)), low); + write_reg(&mut b, Reg::gpr(2, width_of(size)), high); + } + set_result_flags(&mut b, low); + return b.finish(true); + } + + // Two and three operand forms write only the low half. + let (x_op, y_op) = if ops.len() == 3 { (1, 2) } else { (0, 1) }; + let Some(x) = ops.get(x_op).and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + let Some(y) = ops.get(y_op).and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + let y = size_matched(&mut b, y, size); + let low = b.eval(Op::IntMul, size, &[x, y]); + let high = b.eval(Op::IntSMulHigh, size, &[x, y]); + let expected = b.eval( + Op::IntSRight, + size, + &[low, Varnode::constant(size as u64 * 8 - 1, 1)], + ); + let fits = b.eval(Op::IntEqual, 1, &[high, expected]); + let overflow = b.eval(Op::BoolNot, 1, &[fits]); + b.emit(Op::Copy, Some(flag_cf()), &[overflow]); + b.emit(Op::Copy, Some(flag_of()), &[overflow]); + set_result_flags(&mut b, low); + if store(&mut b, &ops[0], low, at).is_none() { + return b.unimplemented(); + } + b.finish(true) +} + +/// The unsigned one-operand multiply. +fn mul_one(mut b: Builder, ops: &[Operand], size: u8, at: Addr) -> Lifted { + let Some(y) = ops.first().and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + let acc = read(&mut b, Varnode::register(gpr_offset(0), size)); + let low = b.eval(Op::IntMul, size, &[acc, y]); + let high = b.eval(Op::IntMulHigh, size, &[acc, y]); + let overflow = b.eval(Op::IntNotEqual, 1, &[high, Varnode::constant(0, size)]); + b.emit(Op::Copy, Some(flag_cf()), &[overflow]); + b.emit(Op::Copy, Some(flag_of()), &[overflow]); + if size == 1 { + let wide = b.eval(Op::IntZExt, 2, &[low]); + b.emit(Op::Copy, Some(Varnode::register(gpr_offset(0), 2)), &[wide]); + } else { + write_reg(&mut b, Reg::gpr(0, width_of(size)), low); + write_reg(&mut b, Reg::gpr(2, width_of(size)), high); + } + set_result_flags(&mut b, low); + b.finish(true) +} + +/// The one-operand divides, whose dividend is twice the operand width. +fn divide(mut b: Builder, ops: &[Operand], size: u8, at: Addr, signed: bool) -> Lifted { + let Some(d) = ops.first().and_then(|o| source(&mut b, o, size, at)) else { + return b.unimplemented(); + }; + if size == 1 { + // The byte form divides `ax`, not a pair of registers. + let ax = Varnode::register(gpr_offset(0), 2); + let wide = if signed { + b.eval(Op::IntSExt, 2, &[d]) + } else { + b.eval(Op::IntZExt, 2, &[d]) + }; + let q = b.eval( + if signed { Op::IntSDiv } else { Op::IntDiv }, + 2, + &[ax, wide], + ); + let r = b.eval( + if signed { Op::IntSRem } else { Op::IntRem }, + 2, + &[ax, wide], + ); + write(&mut b, Varnode::register(gpr_offset(0), 1), q); + write(&mut b, Varnode::register(gpr_offset(0) + 1, 1), r); + return b.finish(true); + } + let low = Varnode::register(gpr_offset(0), size); + let high = Varnode::register(gpr_offset(2), size); + let (div, rem) = if signed { + (Op::IntSDiv128, Op::IntSRem128) + } else { + (Op::IntDiv128, Op::IntRem128) + }; + let q = b.eval(div, size, &[high, low, d]); + let r = b.eval(rem, size, &[high, low, d]); + write_reg(&mut b, Reg::gpr(0, width_of(size)), q); + write_reg(&mut b, Reg::gpr(2, width_of(size)), r); + b.finish(true) +} + +fn width_of(size: u8) -> Width { + match size { + 1 => Width::W8, + 2 => Width::W16, + 4 => Width::W32, + _ => Width::W64, + } +} diff --git a/crates/e5r-ir/src/op.rs b/crates/e5r-ir/src/op.rs new file mode 100644 index 0000000..1b0160d --- /dev/null +++ b/crates/e5r-ir/src/op.rs @@ -0,0 +1,538 @@ +//! The instruction set the IR is written in. +//! +//! Modelled on Ghidra's p-code: every operation reads typed storage locations +//! and writes at most one. Two properties matter and both are deliberate. +//! +//! Operations have no side effects beyond their output. A machine instruction +//! that sets flags becomes several IR operations, one per flag, so nothing is +//! implicit and dataflow does not have to know which instruction touches what. +//! +//! Operations are sized. `IntAdd` on four bytes and on eight are the same +//! opcode with different varnode sizes, which keeps the opcode list short +//! enough to implement completely. + +use std::fmt; + +use serde::Serialize; + +/// Where a value lives. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Space { + /// A literal. The offset is the value. + Const, + /// The machine's register file. The offset is a byte position in it, so + /// `eax` and `rax` overlap the way the hardware says they do. + Register, + /// Addressable memory. The offset is an address. + Ram, + /// A temporary introduced by lifting, invisible to the machine. + Unique, + /// A stack slot that has been proven to be accessed only directly. The + /// offset is from the stack pointer on entry, so it is usually negative. + Stack, +} + +/// A storage location: a space, an offset in it, and a size in bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +pub struct Varnode { + /// Which space. + pub space: Space, + /// Where in it. + pub offset: u64, + /// How many bytes. + pub size: u8, +} + +impl Varnode { + /// A literal value. + pub const fn constant(value: u64, size: u8) -> Varnode { + Varnode { + space: Space::Const, + offset: value, + size, + } + } + + /// A register at a byte offset in the register file. + pub const fn register(offset: u64, size: u8) -> Varnode { + Varnode { + space: Space::Register, + offset, + size, + } + } + + /// A lifting temporary. + pub const fn temp(id: u64, size: u8) -> Varnode { + Varnode { + space: Space::Unique, + offset: id, + size, + } + } + + /// True when this is a literal. + pub const fn is_const(self) -> bool { + matches!(self.space, Space::Const) + } + + /// The literal's value, if it is one. + pub const fn value(self) -> Option { + if self.is_const() { + Some(self.offset) + } else { + None + } + } + + /// A mask covering this varnode's bits. + pub const fn mask(self) -> u64 { + match self.size { + 0 => 0, + 1..=7 => (1u64 << (self.size as u32 * 8)) - 1, + _ => u64::MAX, + } + } + + /// True when two varnodes name overlapping storage. + pub fn overlaps(self, other: Varnode) -> bool { + if self.space != other.space || self.space == Space::Const { + return false; + } + let a = self.offset..self.offset.saturating_add(self.size as u64); + let b = other.offset..other.offset.saturating_add(other.size as u64); + a.start < b.end && b.start < a.end + } +} + +impl fmt::Display for Varnode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.space { + Space::Const => write!(f, "{:#x}:{}", self.offset, self.size), + Space::Register => write!(f, "r[{:#x}]:{}", self.offset, self.size), + Space::Ram => write!(f, "ram[{:#x}]:{}", self.offset, self.size), + Space::Unique => write!(f, "u{}:{}", self.offset, self.size), + Space::Stack => write!(f, "stack[{}]:{}", self.offset as i64, self.size), + } + } +} + +/// What an operation does. +/// +/// Every arithmetic opcode is unsigned unless its name says otherwise, because +/// the machine's registers hold bits and signedness is a property of the +/// operation rather than of the storage. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Op { + /// `out = in0`, with truncation or the identity. + Copy, + /// `out = *in0`, reading `out.size` bytes. + Load, + /// `*in0 = in1`. + Store, + + /// Unconditional transfer to `in0`, which is a constant address. + Branch, + /// Transfer to `in0` when `in1` is non-zero. + CBranch, + /// Transfer to the address in `in0`. + BranchInd, + /// Call `in0`. + Call, + /// Call the address in `in0`. + CallInd, + /// Return to the address in `in0`. + Return, + + /// Integer addition. + IntAdd, + /// Integer subtraction. + IntSub, + /// Integer multiplication, low half. + IntMul, + /// Unsigned division. + IntDiv, + /// Signed division. + IntSDiv, + /// Unsigned remainder. + IntRem, + /// Signed remainder. + IntSRem, + /// High half of an unsigned product, which no narrower operation can give. + IntMulHigh, + /// High half of a signed product. + IntSMulHigh, + /// Unsigned divide of a double-width dividend: `(hi:lo) / y`. + IntDiv128, + /// Signed divide of a double-width dividend. + IntSDiv128, + /// Unsigned remainder of a double-width dividend. + IntRem128, + /// Signed remainder of a double-width dividend. + IntSRem128, + /// Bitwise and. + IntAnd, + /// Bitwise or. + IntOr, + /// Bitwise exclusive or. + IntXor, + /// Bitwise complement. + IntNot, + /// Two's complement negation. + IntNegate, + /// Shift left. + IntLeft, + /// Logical shift right. + IntRight, + /// Arithmetic shift right. + IntSRight, + /// Equality, producing one byte. + IntEqual, + /// Inequality. + IntNotEqual, + /// Unsigned less than. + IntLess, + /// Signed less than. + IntSLess, + /// Unsigned less than or equal. + IntLessEqual, + /// Signed less than or equal. + IntSLessEqual, + /// Unsigned carry out of an addition. + IntCarry, + /// Signed overflow of an addition. + IntSCarry, + /// Signed overflow of a subtraction. + IntSBorrow, + /// Zero extension. + IntZExt, + /// Sign extension. + IntSExt, + /// Count set bits. + PopCount, + /// Count leading zeros. + LzCount, + + /// Logical and of one-byte booleans. + BoolAnd, + /// Logical or. + BoolOr, + /// Logical exclusive or. + BoolXor, + /// Logical not. + BoolNot, + + /// Floating point addition, at the width of its operands. + FloatAdd, + /// Floating point subtraction. + FloatSub, + /// Floating point multiplication. + FloatMul, + /// Floating point division. + FloatDiv, + /// Fused multiply-add: `in0 * in1 + in2`, rounded once. + FloatMulAdd, + /// Sign flip. + FloatNeg, + /// Magnitude. + FloatAbs, + /// Square root. + FloatSqrt, + /// Larger of two, the IEEE rule rather than a comparison. + FloatMax, + /// Smaller of two. + FloatMin, + /// Equality, false when either side is not a number. + FloatEqual, + /// Inequality, true when either side is not a number. + FloatNotEqual, + /// Ordered less than. + FloatLess, + /// Ordered less than or equal. + FloatLessEqual, + /// True when the value is not a number. + FloatNan, + /// Round toward zero, still floating point. + FloatTrunc, + /// Round to nearest, ties to even. + FloatRound, + /// Round up. + FloatCeil, + /// Round down. + FloatFloor, + /// Signed integer to floating point. + IntToFloat, + /// Unsigned integer to floating point. + UIntToFloat, + /// Floating point to signed integer, truncating toward zero. + FloatToInt, + /// Floating point to unsigned integer. + FloatToUInt, + /// Change of floating point width. + FloatConvert, + + /// `out` now holds something this function did not compute: what a call + /// leaves in the registers the convention lets it clobber. It is not a + /// value, it is the absence of one, and dataflow needs to see the + /// difference. + Undefine, + + /// Concatenate: `out = (in0 << (in1.size * 8)) | in1`. + Piece, + /// Extract: `out = in0 >> (in1 * 8)`, truncated to `out.size`. + SubPiece, + + /// Something the lifter does not model. The output, if any, becomes + /// unknown. Honest rather than wrong. + Unimplemented, +} + +impl Op { + /// True when this ends a block. + pub fn is_branch(self) -> bool { + matches!( + self, + Op::Branch | Op::CBranch | Op::BranchInd | Op::Return | Op::Call | Op::CallInd + ) + } + + /// How many inputs the operation takes. + pub fn arity(self) -> usize { + match self { + Op::Copy + | Op::Load + | Op::IntNot + | Op::IntNegate + | Op::IntZExt + | Op::IntSExt + | Op::FloatNeg + | Op::FloatAbs + | Op::FloatSqrt + | Op::FloatNan + | Op::FloatTrunc + | Op::FloatRound + | Op::FloatCeil + | Op::FloatFloor + | Op::IntToFloat + | Op::UIntToFloat + | Op::FloatToInt + | Op::FloatToUInt + | Op::FloatConvert + | Op::BoolNot + | Op::PopCount + | Op::LzCount + | Op::Branch + | Op::BranchInd + | Op::Call + | Op::CallInd + | Op::Return => 1, + Op::Unimplemented | Op::Undefine => 0, + Op::IntDiv128 | Op::IntSDiv128 | Op::IntRem128 | Op::IntSRem128 => 3, + Op::FloatMulAdd => 3, + _ => 2, + } + } + + /// The symbol shown when an operation is printed. + pub fn as_str(self) -> &'static str { + match self { + Op::Copy => "copy", + Op::Load => "load", + Op::Store => "store", + Op::Branch => "branch", + Op::CBranch => "cbranch", + Op::BranchInd => "branchind", + Op::Call => "call", + Op::CallInd => "callind", + Op::Return => "return", + Op::IntAdd => "+", + Op::IntSub => "-", + Op::IntMul => "*", + Op::IntDiv => "/u", + Op::IntSDiv => "/s", + Op::IntRem => "%u", + Op::IntSRem => "%s", + Op::FloatAdd => "f+", + Op::FloatSub => "f-", + Op::FloatMul => "f*", + Op::FloatDiv => "f/", + Op::FloatMulAdd => "fmuladd", + Op::FloatNeg => "fneg", + Op::FloatAbs => "fabs", + Op::FloatSqrt => "fsqrt", + Op::FloatMax => "fmax", + Op::FloatMin => "fmin", + Op::FloatEqual => "f==", + Op::FloatNotEqual => "f!=", + Op::FloatLess => "f<", + Op::FloatLessEqual => "f<=", + Op::FloatNan => "fnan", + Op::FloatTrunc => "ftrunc", + Op::FloatRound => "fround", + Op::FloatCeil => "fceil", + Op::FloatFloor => "ffloor", + Op::IntToFloat => "s2f", + Op::UIntToFloat => "u2f", + Op::FloatToInt => "f2s", + Op::FloatToUInt => "f2u", + Op::FloatConvert => "f2f", + Op::IntMulHigh => "*hu", + Op::IntSMulHigh => "*hs", + Op::IntDiv128 => "/u128", + Op::IntSDiv128 => "/s128", + Op::IntRem128 => "%u128", + Op::IntSRem128 => "%s128", + Op::IntAnd => "&", + Op::IntOr => "|", + Op::IntXor => "^", + Op::IntNot => "~", + Op::IntNegate => "neg", + Op::IntLeft => "<<", + Op::IntRight => ">>u", + Op::IntSRight => ">>s", + Op::IntEqual => "==", + Op::IntNotEqual => "!=", + Op::IntLess => " " "<=u", + Op::IntSLessEqual => "<=s", + Op::IntCarry => "carry", + Op::IntSCarry => "scarry", + Op::IntSBorrow => "sborrow", + Op::IntZExt => "zext", + Op::IntSExt => "sext", + Op::PopCount => "popcount", + Op::LzCount => "lzcount", + Op::BoolAnd => "&&", + Op::BoolOr => "||", + Op::BoolXor => "^^", + Op::BoolNot => "!", + Op::Piece => "piece", + Op::SubPiece => "subpiece", + Op::Undefine => "undefine", + Op::Unimplemented => "unimplemented", + } + } +} + +impl fmt::Display for Op { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// The most inputs any operation takes. +pub const MAX_INPUTS: usize = 3; + +/// One IR operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IrOp { + /// Which operation. + pub op: Op, + /// Where the result goes, if anywhere. + pub out: Option, + inputs: [Varnode; MAX_INPUTS], + n: u8, + /// The machine instruction this came from, so a result can be traced back. + pub addr: e5r_core::Addr, +} + +impl IrOp { + /// Build an operation with no inputs yet. + pub fn new(addr: e5r_core::Addr, op: Op, out: Option) -> IrOp { + IrOp { + op, + out, + inputs: [Varnode::constant(0, 1); MAX_INPUTS], + n: 0, + addr, + } + } + + /// Add an input. + pub fn with(mut self, v: Varnode) -> IrOp { + if (self.n as usize) < MAX_INPUTS { + self.inputs[self.n as usize] = v; + self.n += 1; + } + self + } + + /// The inputs. + pub fn inputs(&self) -> &[Varnode] { + &self.inputs[..self.n as usize] + } + + /// One input, or `None` if there are fewer. + pub fn input(&self, n: usize) -> Option { + self.inputs().get(n).copied() + } + + /// Rewrite every input in place. + pub fn map_inputs(&mut self, mut f: impl FnMut(Varnode) -> Varnode) { + for i in 0..self.n as usize { + self.inputs[i] = f(self.inputs[i]); + } + } + + /// Replace the inputs wholesale. + pub fn set_inputs(&mut self, vs: &[Varnode]) { + self.n = 0; + for v in vs.iter().take(MAX_INPUTS) { + self.inputs[self.n as usize] = *v; + self.n += 1; + } + } +} + +impl fmt::Display for IrOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(out) = self.out { + write!(f, "{out} = ")?; + } + write!(f, "{}", self.op)?; + for (n, i) in self.inputs().iter().enumerate() { + write!(f, "{}{i}", if n == 0 { " " } else { ", " })?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn overlapping_registers_are_detected() { + // eax and rax share storage; that is the whole reason the register + // space is byte-addressed rather than a list of names. + let rax = Varnode::register(0, 8); + let eax = Varnode::register(0, 4); + let rcx = Varnode::register(8, 8); + assert!(rax.overlaps(eax)); + assert!(eax.overlaps(rax)); + assert!(!rax.overlaps(rcx)); + } + + #[test] + fn constants_never_overlap() { + let a = Varnode::constant(5, 8); + assert!(!a.overlaps(a)); + } + + #[test] + fn masks_match_the_size() { + assert_eq!(Varnode::constant(0, 1).mask(), 0xff); + assert_eq!(Varnode::constant(0, 4).mask(), 0xffff_ffff); + assert_eq!(Varnode::constant(0, 8).mask(), u64::MAX); + } + + #[test] + fn arity_matches_what_the_interpreter_reads() { + assert_eq!(Op::IntAdd.arity(), 2); + assert_eq!(Op::Copy.arity(), 1); + assert_eq!(Op::Unimplemented.arity(), 0); + } +} diff --git a/crates/e5r-ir/src/opt.rs b/crates/e5r-ir/src/opt.rs new file mode 100644 index 0000000..32928ed --- /dev/null +++ b/crates/e5r-ir/src/opt.rs @@ -0,0 +1,1915 @@ +//! Dataflow over SSA: constant folding, copy propagation, dead code removal. +//! +//! The point is not speed. Lifting one machine instruction produces several IR +//! operations and most of them are bookkeeping: a flag nothing reads, a +//! temporary that holds a constant, a merge that puts a value back where it +//! already was. Removing them is what turns IR into something a person can +//! read, and it is the difference between a disassembler and the beginning of a +//! decompiler. +//! +//! Every pass runs to a fixed point and every pass is conservative: an +//! operation whose effect is not understood is kept. + +use std::cell::RefCell; +use std::collections::{BTreeMap, BTreeSet}; + +use crate::op::{Op, Space}; +use crate::ssa::{Location, Operand, SsaFunction, SsaKind, Value}; + +/// What a pass changed. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct Changes { + /// Operations whose result became a constant. + pub folded: usize, + /// Operands replaced with what they were copied from. + pub propagated: usize, + /// Operations removed because nothing read them. + pub removed: usize, +} + +impl Changes { + /// True when nothing changed. + pub fn is_empty(self) -> bool { + self == Changes::default() + } + + fn add(&mut self, other: Changes) { + self.folded += other.folded; + self.propagated += other.propagated; + self.removed += other.removed; + } +} + +/// Run every pass to a fixed point. +pub fn optimize(f: &mut SsaFunction) -> Changes { + let mut total = Changes::default(); + for _ in 0..32 { + let mut round = Changes::default(); + round.add(propagate(f)); + round.add(fold(f)); + round.add(simplify(f)); + round.add(cse(f)); + round.add(patterns(f)); + round.add(divisions(f)); + round.add(remove_dead(f)); + if round.is_empty() { + break; + } + total.add(round); + } + total +} + +/// Replace an operand with what it was copied from. +/// +/// A copy chain is followed to its source, and a phi whose inputs all agree is +/// itself a copy, which is what removes the phis lifting introduces for values +/// that never actually differ between paths. +pub fn propagate(f: &mut SsaFunction) -> Changes { + let mut source: BTreeMap = BTreeMap::new(); + for b in f.blocks.values() { + for op in &b.ops { + let Some(out) = op.out else { continue }; + match &op.kind { + SsaKind::Op(Op::Copy) if op.inputs.len() == 1 => { + // A copy that narrows or widens is not a copy. + if operand_size(&op.inputs[0]) == op.size { + source.insert(out, op.inputs[0]); + } + } + // A phi whose inputs are all the same value is that value. + SsaKind::Phi if !op.inputs.is_empty() => { + let first = op.inputs[0]; + if op.inputs.iter().all(|i| *i == first) && first != Operand::Value(out) { + source.insert(out, first); + } + } + _ => {} + } + } + } + if source.is_empty() { + return Changes::default(); + } + + // Follow chains, with a guard: a phi cycle would otherwise loop. + let resolve = |mut o: Operand| { + let mut guard = 0; + while let Operand::Value(v) = o { + match source.get(&v) { + Some(next) if *next != o && guard < 64 => { + o = *next; + guard += 1; + } + _ => break, + } + } + o + }; + + let mut changed = Changes::default(); + for b in f.blocks.values_mut() { + for op in b.ops.iter_mut() { + for i in op.inputs.iter_mut() { + let to = resolve(*i); + if to != *i { + *i = to; + changed.propagated += 1; + } + } + } + } + changed +} + +/// Evaluate operations whose inputs are all constants. +pub fn fold(f: &mut SsaFunction) -> Changes { + let mut changed = Changes::default(); + for b in f.blocks.values_mut() { + for op in b.ops.iter_mut() { + let SsaKind::Op(o) = op.kind else { continue }; + if op.out.is_none() || matches!(o, Op::Copy | Op::Load | Op::Store) { + continue; + } + let consts: Option> = op.inputs.iter().map(|i| i.as_const()).collect(); + let Some(consts) = consts else { continue }; + if consts.is_empty() { + continue; + } + let Some(value) = evaluate(o, &consts, op.size, &op.inputs) else { + continue; + }; + // Rewritten as a copy of the constant rather than deleted, so a + // later pass sees an ordinary copy and the definition survives. + op.kind = SsaKind::Op(Op::Copy); + op.inputs = vec![Operand::Const(value, op.size)]; + changed.folded += 1; + } + } + changed +} + +/// The size an operand is read at. +/// +/// The size the operand itself carries, with no clamping to the operation's +/// width. Clamping used to happen here, and it made a narrowing copy compare +/// equal to the operation's size, so only a widening copy was ever refused and +/// an eight-byte value under a four-byte copy propagated into four-byte +/// contexts. That is the shape that once turned every signed `<` in the corpus +/// into its complement, so what the caller wants is the honest size. +/// +/// Removing the clamp costs nothing measured: over 5,120 functions of the +/// fixture corpus there are 285,718 copies before optimization and 105,801 +/// after, and not one of them has an operand of a different size from the copy +/// in either direction. The whole fixture corpus decompiles to the same text +/// either way and the roundtrip gate does not move. It is dropped because the +/// guard should mean what it says when a lifter does emit such a copy, not +/// because anything here emits one today. +fn operand_size(o: &Operand) -> u8 { + match o { + Operand::Const(_, s) => *s, + Operand::Value(v) => v.location.size, + Operand::Undefined(l) => l.size, + } + .max(1) +} + +/// Evaluate a constant operation, or `None` when the result is not defined. +fn evaluate(op: Op, a: &[u64], size: u8, inputs: &[Operand]) -> Option { + let mask = match size { + 0 => return None, + 1..=7 => (1u64 << (size as u32 * 8)) - 1, + _ => u64::MAX, + }; + let in_size = |n: usize| match inputs.get(n) { + Some(Operand::Const(_, s)) => (*s).max(1), + _ => size.max(1), + }; + let sext = |v: u64, s: u8| -> i64 { + if s >= 8 { + v as i64 + } else { + let shift = 64 - s as u32 * 8; + ((v << shift) as i64) >> shift + } + }; + let x = *a.first()?; + let xs = sext(x, in_size(0)); + let y = a.get(1).copied().unwrap_or(0); + let ys = sext(y, in_size(1)); + let bits = in_size(0) as u64 * 8; + + let out = match op { + Op::IntAdd => x.wrapping_add(y), + Op::IntSub => x.wrapping_sub(y), + Op::IntMul => x.wrapping_mul(y), + Op::IntDiv | Op::IntRem | Op::IntSDiv | Op::IntSRem if y == 0 => return None, + Op::IntDiv => x / y, + Op::IntRem => x % y, + Op::IntSDiv => xs.wrapping_div(ys) as u64, + Op::IntSRem => xs.wrapping_rem(ys) as u64, + Op::IntAnd => x & y, + Op::IntOr => x | y, + Op::IntXor => x ^ y, + Op::IntNot => !x, + Op::IntNegate => x.wrapping_neg(), + Op::IntLeft => { + if y >= bits { + 0 + } else { + x << y + } + } + Op::IntRight => { + if y >= bits { + 0 + } else { + x >> y + } + } + Op::IntSRight => { + if y >= bits { + if xs < 0 { u64::MAX } else { 0 } + } else { + (xs >> y) as u64 + } + } + Op::IntEqual => (x == y) as u64, + Op::IntNotEqual => (x != y) as u64, + Op::IntLess => (x < y) as u64, + Op::IntSLess => (xs < ys) as u64, + Op::IntLessEqual => (x <= y) as u64, + Op::IntSLessEqual => (xs <= ys) as u64, + Op::IntZExt => x, + Op::IntSExt => xs as u64, + Op::PopCount => x.count_ones() as u64, + Op::BoolAnd => ((x != 0) && (y != 0)) as u64, + Op::BoolOr => ((x != 0) || (y != 0)) as u64, + Op::BoolXor => ((x != 0) ^ (y != 0)) as u64, + Op::BoolNot => (x == 0) as u64, + Op::SubPiece => x >> (y * 8), + // The carry and overflow operations depend on the operand width in a + // way the constant folder would have to duplicate; left alone. + _ => return None, + }; + Some(out & mask) +} + +/// Remove operations nothing reads. +/// +/// A store, a branch, a call and anything unmodelled are kept whatever their +/// output: their effect is not the value they produce. +/// The value a location holds at the end of a block, whether it was written +/// there or arrived from a predecessor. +fn last_definition( + f: &SsaFunction, + b: &crate::ssa::SsaBlock, + location: Location, + before: usize, +) -> Option { + if let Some(v) = b.ops[..before.min(b.ops.len())] + .iter() + .rev() + .filter_map(|op| op.out) + .find(|v| v.location == location) + { + return Some(v); + } + // Nothing in this block wrote it, so whatever the rest of the function + // last put there is what the caller sees. + f.blocks + .values() + .flat_map(|other| other.ops.iter()) + .filter_map(|op| op.out) + .filter(|v| v.location == location) + .max_by_key(|v| v.version) +} + +/// Remove operations nothing reads and nothing observes. +pub fn remove_dead(f: &mut SsaFunction) -> Changes { + let mut live: BTreeSet = BTreeSet::new(); + // Seed from the operations that must stay, then close over their inputs. + let mut work: Vec = Vec::new(); + + // The caller reads the result and expects the saved registers back, so the + // last value in each of those at a return is live even though nothing in + // this function reads it. Without this the result is dead by construction + // and every function optimizes down to its side effects. + let abi = crate::abi::of(&f.arch); + let live_out = abi.live_at_return(); + for b in f.blocks.values() { + let returns = b + .ops + .iter() + .any(|op| op.kind == SsaKind::Op(crate::op::Op::Return)); + if !returns { + continue; + } + for location in &live_out { + if let Some(v) = last_definition(f, b, *location, b.ops.len()) { + if live.insert(v) { + work.push(v); + } + } + } + } + + // A call reads its arguments out of registers the IR does not name as its + // inputs, so without this every instruction that sets one up looks dead. + // Deleting them does not just lose readability: it loses the argument. + let argument_locations: Vec = abi + .integer_arguments + .iter() + .chain(abi.float_arguments.iter()) + .map(|offset| Location { + space: Space::Register, + offset: *offset, + size: 8, + }) + .collect(); + for b in f.blocks.values() { + for (n, op) in b.ops.iter().enumerate() { + if !matches!( + op.kind, + SsaKind::Op(crate::op::Op::Call) | SsaKind::Op(crate::op::Op::CallInd) + ) { + continue; + } + for location in &argument_locations { + if let Some(v) = last_definition(f, b, *location, n) { + if live.insert(v) { + work.push(v); + } + } + } + } + } + + for b in f.blocks.values() { + for op in &b.ops { + if !has_effect(op) { + continue; + } + for i in &op.inputs { + if let Operand::Value(v) = i { + if live.insert(*v) { + work.push(*v); + } + } + } + } + } + let defs: BTreeMap = f.definitions(); + while let Some(v) = work.pop() { + let Some((block, index)) = defs.get(&v) else { + continue; + }; + let Some(op) = f.blocks.get(block).and_then(|b| b.ops.get(*index)) else { + continue; + }; + for i in &op.inputs { + if let Operand::Value(u) = i { + if live.insert(*u) { + work.push(*u); + } + } + } + } + + let mut changed = Changes::default(); + for b in f.blocks.values_mut() { + let before = b.ops.len(); + b.ops.retain(|op| match op.out { + Some(v) => has_effect(op) || live.contains(&v), + None => true, + }); + changed.removed += before - b.ops.len(); + } + changed +} + +/// True when an operation matters for reasons other than its result. +fn has_effect(op: &crate::ssa::SsaOp) -> bool { + match &op.kind { + SsaKind::Phi => false, + SsaKind::Op(o) => matches!( + o, + Op::Store + | Op::Branch + | Op::CBranch + | Op::BranchInd + | Op::Call + | Op::CallInd + | Op::Return + | Op::Unimplemented + ), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn constant_arithmetic_folds() { + assert_eq!(evaluate(Op::IntAdd, &[2, 3], 8, &[]), Some(5)); + assert_eq!(evaluate(Op::IntMul, &[6, 7], 8, &[]), Some(42)); + } + + #[test] + fn folding_respects_the_output_width() { + // Four bytes, so the carry is dropped rather than kept. + assert_eq!(evaluate(Op::IntAdd, &[0xffff_ffff, 1], 4, &[]), Some(0)); + } + + #[test] + fn division_by_zero_does_not_fold() { + assert_eq!(evaluate(Op::IntDiv, &[1, 0], 8, &[]), None); + assert_eq!(evaluate(Op::IntSRem, &[1, 0], 8, &[]), None); + } + + #[test] + fn signed_operations_use_the_input_width() { + // A one-byte 0xff is -1, and the shift has to know that. + let inputs = [Operand::Const(0xff, 1), Operand::Const(1, 1)]; + assert_eq!(evaluate(Op::IntSRight, &[0xff, 1], 1, &inputs), Some(0xff)); + } + + #[test] + fn the_flag_operations_are_left_alone() { + // They depend on the operand width in a way the folder would have to + // duplicate, so it does not guess. + assert_eq!(evaluate(Op::IntCarry, &[1, 2], 8, &[]), None); + assert_eq!(evaluate(Op::IntSCarry, &[1, 2], 8, &[]), None); + } +} + +/// Bits a value can have set, as a mask. +/// +/// Cheap and surprisingly load-bearing: a 32-bit write to a 64-bit register is +/// a merge of two halves, and knowing that one half is provably zero is what +/// collapses the merge back into the value that was written. Without it every +/// sub-register operation carries its own bookkeeping into the output. +#[derive(Debug, Default)] +pub struct KnownBits { + masks: BTreeMap, +} + +impl KnownBits { + /// Compute for every value in a function. + pub fn of(f: &SsaFunction) -> KnownBits { + let defs = f.definitions(); + let mut masks: BTreeMap = BTreeMap::new(); + // Iterate: a phi's mask depends on its inputs, which may be defined + // later. The masks only ever grow, so this settles. + for _ in 0..8 { + let mut changed = false; + for b in f.blocks.values() { + for op in &b.ops { + let Some(out) = op.out else { continue }; + let m = mask_of_op(f, &defs, &masks, op); + let width = width_mask(out.location.size); + let m = m & width; + if masks.get(&out).copied() != Some(m) { + masks.insert(out, m); + changed = true; + } + } + } + if !changed { + break; + } + } + KnownBits { masks } + } + + /// The mask of one operand. + pub fn operand(&self, o: &Operand) -> u64 { + match o { + Operand::Const(v, size) => *v & width_mask(*size), + Operand::Value(v) => self + .masks + .get(v) + .copied() + .unwrap_or_else(|| width_mask(v.location.size)), + Operand::Undefined(l) => width_mask(l.size), + } + } +} + +fn width_mask(size: u8) -> u64 { + if size >= 8 || size == 0 { + u64::MAX + } else { + (1u64 << (size as u64 * 8)) - 1 + } +} + +fn mask_of_op( + f: &SsaFunction, + defs: &BTreeMap, + masks: &BTreeMap, + op: &crate::ssa::SsaOp, +) -> u64 { + let get = |o: &Operand| -> u64 { + match o { + Operand::Const(v, size) => *v & width_mask(*size), + Operand::Value(v) => masks + .get(v) + .copied() + .unwrap_or_else(|| width_mask(v.location.size)), + Operand::Undefined(l) => width_mask(l.size), + } + }; + let _ = (f, defs); + let a = op.inputs.first().map(&get).unwrap_or(u64::MAX); + let b = op.inputs.get(1).map(&get).unwrap_or(u64::MAX); + match op.kind { + SsaKind::Phi => op.inputs.iter().map(&get).fold(0, |acc, m| acc | m), + SsaKind::Op(o) => match o { + Op::Copy => a, + Op::IntAnd => a & b, + Op::IntOr | Op::IntXor => a | b, + Op::IntZExt => a, + Op::SubPiece => width_mask(op.size), + Op::Load => width_mask(op.size), + Op::IntLeft => match op.inputs.get(1).and_then(|i| i.as_const()) { + Some(n) if n < 64 => a << n, + _ => u64::MAX, + }, + Op::IntRight => match op.inputs.get(1).and_then(|i| i.as_const()) { + Some(n) if n < 64 => a >> n, + _ => u64::MAX, + }, + // A comparison or a flag is one bit. + Op::IntEqual + | Op::IntNotEqual + | Op::IntLess + | Op::IntLessEqual + | Op::IntSLess + | Op::IntSLessEqual + | Op::IntCarry + | Op::IntSCarry + | Op::IntSBorrow + | Op::BoolAnd + | Op::BoolOr + | Op::BoolXor + | Op::BoolNot + | Op::FloatEqual + | Op::FloatNotEqual + | Op::FloatLess + | Op::FloatLessEqual + | Op::FloatNan => 1, + Op::PopCount | Op::LzCount => 0x7f, + _ => u64::MAX, + }, + } +} + +/// Rewrite operations whose result is already available in one of their inputs. +/// +/// The rules are the identities that machine code produces by construction: +/// masking bits that are already zero, merging a half that is provably empty, +/// exclusive-oring a value with itself. +pub fn simplify(f: &mut SsaFunction) -> Changes { + let known = KnownBits::of(f); + // A snapshot of what each value is computed by, so a rule can look one + // step back without borrowing the function while it is being rewritten. + let mut source: BTreeMap)> = BTreeMap::new(); + for b in f.blocks.values() { + for op in &b.ops { + if let (Some(out), SsaKind::Op(o)) = (op.out, op.kind.clone()) { + source.insert(out, (o, op.inputs.clone())); + } + } + } + let mut changes = Changes::default(); + for b in f.blocks.values_mut() { + for op in &mut b.ops { + let SsaKind::Op(o) = op.kind else { continue }; + let (a, c) = (op.inputs.first().cloned(), op.inputs.get(1).cloned()); + let (Some(a), Some(c)) = (a, c) else { continue }; + let (ma, mc) = (known.operand(&a), known.operand(&c)); + let same = match (&a, &c) { + (Operand::Value(x), Operand::Value(y)) => x == y, + (Operand::Undefined(x), Operand::Undefined(y)) => x == y, + _ => false, + }; + let width = width_mask(op.size); + + let replacement: Option = match o { + // Masking off bits that were never set. + Op::IntAnd => { + if let Some(k) = c.as_const() { + if ma & width & !k == 0 { + Some(a) + } else if ma & k == 0 { + Some(Operand::Const(0, op.size)) + } else { + None + } + } else if same { + Some(a) + } else { + None + } + } + // Merging with a half that is empty. + Op::IntOr => { + if mc == 0 { + Some(a) + } else if ma == 0 { + Some(c) + } else if same { + Some(a) + } else { + None + } + } + Op::IntXor => { + if same { + Some(Operand::Const(0, op.size)) + } else if mc == 0 { + Some(a) + } else if ma == 0 { + Some(c) + } else { + None + } + } + Op::IntAdd | Op::IntSub if c.as_const() == Some(0) => Some(a), + Op::IntSub if same => Some(Operand::Const(0, op.size)), + Op::IntMul if c.as_const() == Some(1) => Some(a), + Op::IntMul if c.as_const() == Some(0) => Some(Operand::Const(0, op.size)), + Op::IntLeft | Op::IntRight | Op::IntSRight if c.as_const() == Some(0) => Some(a), + // Widening something already that wide. + Op::IntZExt if ma & width == ma && a.size() == op.size => Some(a), + // Taking the low bytes of a value that has no others. The + // replacement has to be the same width: a narrow read is the + // only record of how many bits an operation works at, and a + // signed comparison rebuilt from operands that grew reads the + // sign bit in the wrong place. + Op::SubPiece if c.as_const() == Some(0) && ma & !width_mask(op.size) == 0 => { + narrowed_to(&source, &a, op.size) + } + _ => None, + }; + + if let Some(r) = replacement { + op.kind = SsaKind::Op(Op::Copy); + op.inputs = vec![r]; + changes.folded += 1; + continue; + } + + // Masking one half of a merge away, which is what reading a + // sub-register through a whole-register merge amounts to. Handled + // separately because it rewrites the operands rather than + // replacing the operation. + if o == Op::IntAnd { + if let (Some(k), Operand::Value(v)) = (c.as_const(), &a) { + if let Some((Op::IntOr, parts)) = source.get(v) { + let (p, q) = (parts.first().cloned(), parts.get(1).cloned()); + if let (Some(p), Some(q)) = (p, q) { + let keep = if known.operand(&p) & k == 0 { + Some(q) + } else if known.operand(&q) & k == 0 { + Some(p) + } else { + None + }; + if let Some(keep) = keep { + op.inputs = vec![keep, c]; + changes.folded += 1; + continue; + } + } + } + } + } + } + } + changes +} + +/// An operand of exactly `size` bytes holding what the low `size` bytes of `o` +/// hold, when a widening put them there. +/// +/// Zero-extending a value and then reading its low bytes back is what lifting a +/// narrow register write followed by a narrow read produces, and the value +/// before the extension is already the right width. Without this the only way +/// to keep the width is to keep the extract. +fn narrowed_to( + source: &BTreeMap)>, + o: &Operand, + size: u8, +) -> Option { + let mut current = *o; + // A bound rather than a visited set: the chains are a handful of links and + // a cycle would otherwise spin here. + for _ in 0..16 { + if current.size() == size { + return Some(current); + } + let Operand::Value(v) = current else { + return None; + }; + let (Op::IntZExt, inputs) = source.get(&v)? else { + return None; + }; + let next = *inputs.first()?; + // Extending something narrower than the read puts zeroes in the bytes + // being read, so the value before it is not what is wanted. + if next.size() < size { + return None; + } + current = next; + } + None +} + +/// Rewrite flag algebra back into the comparison it stands for. +/// +/// A machine has no `<`; it has a subtraction and four flags, and a condition +/// is a formula over them. Left alone that formula is what the decompiler +/// prints, and `(a - b) >> 63 != 0 == borrow(a, b)` is not what anyone wrote. +/// These rules recognize the formulas the architectures actually emit and put +/// the comparison back. +pub fn patterns(f: &mut SsaFunction) -> Changes { + // The width each operation works at travels with it: a comparison rebuilt + // from a subtraction has to compare the bits the subtraction subtracted. + let mut source: BTreeMap, u8)> = BTreeMap::new(); + for b in f.blocks.values() { + for op in &b.ops { + if let (Some(out), SsaKind::Op(o)) = (op.out, op.kind.clone()) { + source.insert(out, (o, op.inputs.clone(), op.size)); + } + } + } + + let mut changes = Changes::default(); + for b in f.blocks.values_mut() { + for op in &mut b.ops { + let SsaKind::Op(o) = op.kind else { continue }; + let (Some(a), Some(c)) = (op.inputs.first().cloned(), op.inputs.get(1).cloned()) else { + continue; + }; + + // A comparison against a subtraction is a comparison of its + // operands, which is what the subtraction was for. + if matches!(o, Op::IntEqual | Op::IntNotEqual) && c.as_const() == Some(0) { + if let Some((x, y, width)) = as_subtraction(&source, &a) { + if let (Some(x), Some(y)) = (at_width(x, width), at_width(y, width)) { + op.inputs = vec![x, y]; + changes.folded += 1; + continue; + } + } + } + + // The signed comparisons: the sign of the difference against the + // overflow flag. + if matches!(o, Op::IntEqual | Op::IntNotEqual) { + if let (Some((x, y, width)), Some((px, py))) = + (sign_of_difference(&source, &a), as_borrow(&source, &c)) + { + // Both operands have to be the width the machine compared + // at, or the sign bit the comparison reads is not the one + // the flags were computed from. + let narrowed = match (x == px && y == py, at_width(x, width)) { + (true, Some(x)) => at_width(y, width).map(|y| (x, y)), + _ => None, + }; + if let Some((x, y)) = narrowed { + op.kind = SsaKind::Op(if o == Op::IntEqual { + // Sign agrees with overflow: not less than. + Op::IntSLessEqual + } else { + Op::IntSLess + }); + op.inputs = if o == Op::IntEqual { + vec![y, x] + } else { + vec![x, y] + }; + changes.folded += 1; + continue; + } + } + } + + // Not equal and not less is greater. The inequality is on the + // subtraction's operands and the `<=` is on them reversed, so + // `x != y && y <= x` is `y < x`, which is the `<=`'s own order. + if o == Op::BoolAnd { + if let (Some((x, y)), Some((px, py))) = + (as_inequality(&source, &a), as_signed_le(&source, &c)) + { + if x == py && y == px { + op.kind = SsaKind::Op(Op::IntSLess); + op.inputs = vec![px, py]; + changes.folded += 1; + continue; + } + } + } + } + } + changes +} + +/// The operands of a subtraction and the width it works at, if that is what an +/// operand is. +fn as_subtraction( + source: &BTreeMap, u8)>, + o: &Operand, +) -> Option<(Operand, Operand, u8)> { + let Operand::Value(v) = o else { return None }; + let (Op::IntSub, inputs, size) = source.get(v)? else { + return None; + }; + Some((inputs.first().cloned()?, inputs.get(1).cloned()?, *size)) +} + +/// An operand read at `width` bytes, or `None` when it cannot be. +/// +/// A constant is whatever width it is needed at, since its value says +/// everything about it. Anything else already carries a width, and a +/// comparison rebuilt at a width the operand does not have would read a +/// different sign bit than the machine did. +fn at_width(o: Operand, width: u8) -> Option { + match o { + _ if o.size() == width => Some(o), + Operand::Const(v, _) => Some(Operand::Const(v & width_mask(width), width)), + _ => None, + } +} + +/// `(x - y) >> (bits - 1) != 0`, which is the negative flag. +fn sign_of_difference( + source: &BTreeMap, u8)>, + o: &Operand, +) -> Option<(Operand, Operand, u8)> { + let Operand::Value(v) = o else { return None }; + let (Op::IntNotEqual, inputs, _) = source.get(v)? else { + return None; + }; + if inputs.get(1)?.as_const() != Some(0) { + return None; + } + let Operand::Value(shifted) = inputs.first()? else { + return None; + }; + let (Op::IntRight, shift_inputs, _) = source.get(shifted)? else { + return None; + }; + let shifted_value = *shift_inputs.first()?; + let bits = shifted_value.size() as u64 * 8; + if shift_inputs.get(1)?.as_const() != Some(bits - 1) { + return None; + } + // A comparison against zero leaves no subtraction behind: `x - 0` is `x` + // and an earlier rule already said so, so the sign of the difference is + // the sign of the value. Without this the whole flag formula survives into + // the output as a borrow against zero. + as_subtraction(source, &shifted_value).or_else(|| { + let width = shifted_value.size(); + Some((shifted_value, Operand::Const(0, width), width)) + }) +} + +/// The operands of a signed borrow, if that is what an operand is. +fn as_borrow( + source: &BTreeMap, u8)>, + o: &Operand, +) -> Option<(Operand, Operand)> { + let Operand::Value(v) = o else { return None }; + let (Op::IntSBorrow, inputs, _) = source.get(v)? else { + return None; + }; + Some((inputs.first().cloned()?, inputs.get(1).cloned()?)) +} + +fn as_inequality( + source: &BTreeMap, u8)>, + o: &Operand, +) -> Option<(Operand, Operand)> { + let Operand::Value(v) = o else { return None }; + let (op, inputs, _) = source.get(v)?; + match op { + Op::IntNotEqual => Some((inputs.first().cloned()?, inputs.get(1).cloned()?)), + // The same thing said the other way round, which is how a machine that + // inverts its condition codes writes it. + Op::BoolNot => as_equality(source, inputs.first()?), + _ => None, + } +} + +fn as_equality( + source: &BTreeMap, u8)>, + o: &Operand, +) -> Option<(Operand, Operand)> { + let Operand::Value(v) = o else { return None }; + let (Op::IntEqual, inputs, _) = source.get(v)? else { + return None; + }; + Some((inputs.first().cloned()?, inputs.get(1).cloned()?)) +} + +fn as_signed_le( + source: &BTreeMap, u8)>, + o: &Operand, +) -> Option<(Operand, Operand)> { + let Operand::Value(v) = o else { return None }; + let (Op::IntSLessEqual, inputs, _) = source.get(v)? else { + return None; + }; + Some((inputs.first().cloned()?, inputs.get(1).cloned()?)) +} + +/// The divisor an unsigned reciprocal multiply and shift stand for, verified. +/// +/// A compiler replaces `x / d` by `(x * magic) >> shift`, and `magic` is all +/// that survives into the machine code. The divisor is recoverable from it: +/// the transformation is correct only when `d` lies in `[2^shift / magic, +/// 2^shift / magic + 1)`, and that interval holds exactly one integer. What is +/// returned is that integer, but only after it has been checked against the +/// true quotient at every dividend in `0..=max`. Nothing here trusts the +/// derivation: a divisor that is wrong by one is a silently wrong program, and +/// the check is what a formula cannot be. +pub fn unsigned_divisor(magic: u64, shift: u32, max: u64) -> Option { + let m = magic as i128; + let d = candidate_divisor(m, shift)?; + quotient_agrees(m, shift, d, 0, 0, max as i128).then_some(d as u64) +} + +/// The divisor a signed reciprocal multiply and shift stand for, verified. +/// +/// The signed sequence is `(magic * x >> shift) + (x < 0)`, where the shift is +/// arithmetic and the added bit is what turns a floor into a truncation. The +/// two halves of the range are different statements and both are checked: on +/// the negative side the correction is one short of a floor, which is what the +/// `-1` in the second check records. +pub fn signed_divisor(magic: u64, shift: u32, bits: u32) -> Option { + if !(2..=64).contains(&bits) { + return None; + } + let m = magic as i128; + let d = candidate_divisor(m, shift)?; + let top = 1i128 << (bits - 1); + let ok = + quotient_agrees(m, shift, d, 0, 0, top - 1) && quotient_agrees(m, shift, d, -1, 1, top); + ok.then_some(d as u64) +} + +/// The only divisor a magic number and a shift can stand for. +/// +/// Agreement at the top of a quotient step forces `magic * d >= 2^shift` and +/// agreement just below the next one forces `magic * d < 2^shift + magic`, so +/// `d` is pinned to one integer before anything is checked. A multiplier of one +/// is refused, which is what keeps an ordinary shift an ordinary shift: a +/// compiler needs no reciprocal to divide by a power of two, so a shift with no +/// multiply in front of it is bit manipulation and printing it as a division +/// would bury what the code is doing. +fn candidate_divisor(m: i128, shift: u32) -> Option { + if m <= 1 || shift >= 126 { + return None; + } + let total = 1i128 << shift; + let d = (total + m - 1) / m; + (d >= 3 && d <= u64::MAX as i128).then_some(d) +} + +/// True when `floor((m * x + bias) / 2^shift)` is `x / d` at every integer `x` +/// in `lo..=hi`. +/// +/// Closed form rather than sampled. Writing `x` as `k * d + r` turns the left +/// side into `k` exactly when `0 <= k*e + r*m + bias < 2^shift`, where +/// `e = m*d - 2^shift` is what the reciprocal drifts by per step of the +/// quotient. That expression rises with `r` inside each run of `d` dividends +/// and drops at every run boundary, so its extremes over any interval are at +/// the ends of the interval and at the first and last whole run, which is a +/// handful of points to evaluate. Anything that overflows is refused rather +/// than wrapped. +fn quotient_agrees(m: i128, shift: u32, d: i128, bias: i128, lo: i128, hi: i128) -> bool { + if m <= 0 || d <= 0 || lo < 0 || hi < lo || shift >= 126 { + return false; + } + let span = 1i128 << shift; + let Some(e) = m.checked_mul(d).and_then(|p| p.checked_sub(span)) else { + return false; + }; + let value = |x: i128| -> Option { + (x / d) + .checked_mul(e)? + .checked_add((x % d).checked_mul(m)?)? + .checked_add(bias) + }; + let mut points = vec![lo, hi]; + for p in [ + // The end of the run `lo` falls in, and the start of the run `hi` does. + (lo / d) * d + d - 1, + (hi / d) * d, + // The last whole run inside the interval, and the first. + ((hi + 1) / d) * d - 1, + ((lo + d - 1) / d) * d, + ] { + if p >= lo && p <= hi { + points.push(p); + } + } + points + .into_iter() + .all(|x| matches!(value(x), Some(v) if v >= 0 && v < span)) +} + +/// How far a recognizer follows a chain of definitions. +const REACH: u32 = 24; + +/// A value the code computes as `floor(m * x / 2^shift)` for one dividend `x`. +/// +/// Every rule that builds one of these is exact: the value it describes is the +/// value the machine computes, not an approximation of it. That is what makes +/// the divisor check meaningful. +#[derive(Debug, Clone, Copy)] +struct Scaled { + /// The dividend as the code names it, before the widening. + base: Operand, + /// The dividend after the widening the multiply reads it through. + wide: Operand, + /// True when that widening was a sign extension. + signed: bool, + /// The exact multiplier, never a rounded one. + m: i128, + /// How far the exact product has been floor-divided, as a power of two. + shift: u32, +} + +/// What a function's values are computed by, for the reciprocal rules. +struct Reciprocal<'a> { + source: &'a BTreeMap, u8)>, + known: &'a KnownBits, + /// What [`Reciprocal::scaled`] answered for an operand at a depth. + /// + /// It is a pure function of the two, and it branches: the `SubPiece` rule + /// falls through to a second reading when the first does not fit, `IntAdd` + /// tries two rules, and `combined` tries both operand orders. With + /// [`REACH`] at 24 that is a search of up to 2^24 states over a graph with + /// far fewer, and on one `IntRight` in a zlib function built at `-O0` it + /// took 28 seconds. Remembering the answers makes it linear in the states + /// that exist. + memo: RefCell>>, +} + +impl Reciprocal<'_> { + /// What computed an operand, if anything in this function did. + fn def(&self, o: &Operand) -> Option<(Op, &Vec, u8)> { + let Operand::Value(v) = o else { return None }; + let (op, inputs, size) = self.source.get(v)?; + Some((*op, inputs, *size)) + } + + /// The interval the dividend lies in. + /// + /// A signed dividend spans its width; an unsigned one is bounded by the + /// bits it can have set, which is tighter and is what makes a division of + /// an already shifted value verifiable at all. + fn base_range(&self, s: &Scaled) -> (i128, i128) { + let bits = s.base.size().clamp(1, 8) as u32 * 8; + if s.signed { + (-(1i128 << (bits - 1)), (1i128 << (bits - 1)) - 1) + } else { + (0, self.known.operand(&s.base) as i128) + } + } + + /// The interval the scaled value itself lies in. + fn range(&self, s: &Scaled) -> Option<(i128, i128)> { + let (lo, hi) = self.base_range(s); + let a = s.m.checked_mul(lo)? >> s.shift; + let b = s.m.checked_mul(hi)? >> s.shift; + Some((a.min(b), a.max(b))) + } + + /// True when every value it can take survives being held in `size` bytes. + /// + /// The rules read the machine's registers as the mathematical values they + /// stand for, and that reading is only honest while nothing has wrapped. + fn fits(&self, s: &Scaled, size: u8) -> bool { + let Some((lo, hi)) = self.range(s) else { + return false; + }; + let bits = size.clamp(1, 8) as u32 * 8; + if s.signed { + lo >= -(1i128 << (bits - 1)) && hi < (1i128 << (bits - 1)) + } else { + lo >= 0 && (bits >= 127 || hi < (1i128 << bits)) + } + } + + /// True when an operand is the dividend itself, however it is spelled. + fn is_base(&self, o: &Operand, s: &Scaled) -> bool { + *o == s.base || *o == s.wide || self.same(o, &s.base) + } + + /// The value an operand carries, with the copies and the width changes + /// that do not change it stripped off. + fn core(&self, o: &Operand, depth: u32) -> Operand { + if depth == 0 { + return *o; + } + let Some((op, inputs, _)) = self.def(o) else { + return *o; + }; + let through = match op { + Op::Copy | Op::IntZExt | Op::IntSExt => true, + Op::SubPiece => inputs.get(1).and_then(|i| i.as_const()) == Some(0), + _ => false, + }; + match (through, inputs.first()) { + (true, Some(i)) => self.core(i, depth - 1), + _ => *o, + } + } + + /// True when two operands carry the same value. + fn same(&self, a: &Operand, b: &Operand) -> bool { + a == b || self.core(a, REACH) == self.core(b, REACH) + } + + /// The exact value an operand computes, in reciprocal form. + fn scaled(&self, o: &Operand, depth: u32) -> Option { + if let Some(hit) = self.memo.borrow().get(&(*o, depth)) { + return *hit; + } + let out = self.scaled_uncached(o, depth); + self.memo.borrow_mut().insert((*o, depth), out); + out + } + + fn scaled_uncached(&self, o: &Operand, depth: u32) -> Option { + if depth == 0 { + return None; + } + let (op, inputs, size) = self.def(o)?; + let first = *inputs.first()?; + match op { + Op::Copy => self.scaled(&first, depth - 1), + // The widening is where the dividend enters, and whether it was + // signed or zero filled is what says how to read every bit above. + Op::IntZExt | Op::IntSExt => { + if first.size() >= size { + return None; + } + let s = Scaled { + base: first, + wide: *o, + signed: op == Op::IntSExt, + m: 1, + shift: 0, + }; + self.fits(&s, size).then_some(s) + } + // Multiplying a value that has already been floor-divided loses + // the remainder, so only an exact product composes. + Op::IntMul => { + let (c, other) = multiplier(inputs, size)?; + let a = self.scaled(&other, depth - 1)?; + if a.shift != 0 { + return None; + } + let s = Scaled { + m: a.m.checked_mul(c)?, + ..a + }; + // The multiplier may be negative here: a reciprocal that needs + // more bits than the multiply holds is stored as its low half, + // and the missing one comes back as the dividend added later. + self.fits(&s, size).then_some(s) + } + Op::IntSRight => { + let n = inputs.get(1)?.as_const()? as u32; + let a = self.scaled(&first, depth - 1)?; + if !a.signed || n >= 126 || !self.fits(&a, size) { + return None; + } + Some(Scaled { + shift: a.shift + n, + ..a + }) + } + Op::IntRight => { + let n = inputs.get(1)?.as_const()? as u32; + let a = self.scaled(&first, depth - 1)?; + if a.signed || n >= 126 || !self.fits(&a, size) { + return None; + } + Some(Scaled { + shift: a.shift + n, + ..a + }) + } + Op::SubPiece if inputs.get(1)?.as_const() == Some(0) => { + // A logical shift whose zero fill lands above the bytes kept + // is, at this width, the arithmetic shift a signed value + // needs, and it is how a compiler reads the high half of a + // product back. + if let Some((Op::IntRight, hi, inner)) = self.def(&first) { + let n = hi.get(1)?.as_const()? as u32; + if n + size as u32 * 8 >= inner as u32 * 8 && n < 126 { + if let Some(a) = self.scaled(hi.first()?, depth - 1) { + if self.fits(&a, inner) { + let s = Scaled { + shift: a.shift + n, + ..a + }; + return self.fits(&s, size).then_some(s); + } + } + } + } + let a = self.scaled(&first, depth - 1)?; + self.fits(&a, size).then_some(a) + } + Op::IntAdd => self + .halved_sum(inputs, size, depth) + .or_else(|| self.combined(inputs, size, 1, depth)), + Op::IntSub => self.combined(inputs, size, -1, depth), + _ => None, + } + } + + /// A term of a sum, where the dividend spelled plainly counts as one. + fn term(&self, o: &Operand, of: &Scaled, depth: u32) -> Option { + if self.is_base(o, of) { + return Some(Scaled { + m: 1, + shift: 0, + ..*of + }); + } + let s = self.scaled(o, depth)?; + (self.same(&s.base, &of.base) && s.signed == of.signed).then_some(s) + } + + /// `a + b` or `a - b`, which is exact only while one side is unshifted: + /// two floors added are not the floor of the sum. + fn combined(&self, inputs: &[Operand], size: u8, sign: i128, depth: u32) -> Option { + let (l, r) = (*inputs.first()?, *inputs.get(1)?); + let orders: &[(Operand, Operand)] = if sign < 0 { + &[(l, r)] + } else { + &[(l, r), (r, l)] + }; + for (l, r) in orders { + let Some(a) = self.scaled(l, depth - 1) else { + continue; + }; + let Some(b) = self.term(r, &a, depth - 1) else { + continue; + }; + if b.shift != 0 { + continue; + } + let Some(scale) = 1i128.checked_shl(a.shift) else { + continue; + }; + let Some(m) = + b.m.checked_mul(scale) + .and_then(|t| a.m.checked_add(sign * t)) + else { + continue; + }; + let s = Scaled { m, ..a }; + if self.fits(&s, size) { + return Some(s); + } + } + None + } + + /// `t + ((x - t) >> 1)`, which is `floor((x + t) / 2)`. + /// + /// A compiler emits this when the reciprocal needs one bit more than the + /// multiply can hold: the bit it cannot store is the `x` this adds back, + /// and the halving is what puts the result back in range. + fn halved_sum(&self, inputs: &[Operand], size: u8, depth: u32) -> Option { + let (l, r) = (*inputs.first()?, *inputs.get(1)?); + for (t, half) in [(l, r), (r, l)] { + let Some(a) = self.scaled(&t, depth - 1) else { + continue; + }; + // The subtraction only borrows nothing, and the shift only floors, + // while `t` cannot exceed `x`. + let Some(scale) = 1i128.checked_shl(a.shift) else { + continue; + }; + if a.signed || a.m < 0 || a.m > scale { + continue; + } + let Some((Op::IntRight, hi, _)) = self.def(&half) else { + continue; + }; + if hi.get(1)?.as_const() != Some(1) { + continue; + } + let Some((Op::IntSub, sub, _)) = self.def(hi.first()?) else { + continue; + }; + if !self.same(sub.get(1)?, &t) || !self.is_base(sub.first()?, &a) { + continue; + } + let Some(m) = a.m.checked_add(scale) else { + continue; + }; + let s = Scaled { + m, + shift: a.shift + 1, + ..a + }; + if self.fits(&s, size) { + return Some(s); + } + } + None + } + + /// The dividend at the width the division has to be written at. + fn dividend(&self, s: &Scaled, size: u8) -> Option { + [s.wide, s.base].into_iter().find(|o| o.size() == size) + } + + /// `v >> n` where the shift reads the sign, through the truncation the + /// machine may have done afterwards. + fn arithmetic_shift(&self, o: &Operand) -> Option<(Operand, u32)> { + let (op, inputs, size) = self.def(o)?; + match op { + Op::Copy => self.arithmetic_shift(inputs.first()?), + Op::IntSRight => Some((*inputs.first()?, inputs.get(1)?.as_const()? as u32)), + Op::SubPiece if inputs.get(1)?.as_const() == Some(0) => { + let (inner, within, wide) = self.def(inputs.first()?)?; + let n = within.get(1)?.as_const()? as u32; + match inner { + Op::IntSRight => Some((*within.first()?, n)), + // The zero fill lands above the bytes kept, so at this + // width the logical shift is the arithmetic one. + Op::IntRight if n + size as u32 * 8 >= wide as u32 * 8 => { + Some((*within.first()?, n)) + } + _ => None, + } + } + _ => None, + } + } + + /// The value whose sign bit an operand holds. + fn sign_bit(&self, o: &Operand) -> Option { + let (op, inputs, size) = self.def(o)?; + match op { + Op::Copy => self.sign_bit(inputs.first()?), + Op::SubPiece if inputs.get(1)?.as_const() == Some(0) => self.sign_bit(inputs.first()?), + Op::IntRight if inputs.get(1)?.as_const() == Some(size as u64 * 8 - 1) => { + Some(*inputs.first()?) + } + _ => None, + } + } + + /// An unsigned reciprocal: the final shift of a magic product. + fn unsigned_quotient(&self, o: Op, inputs: &[Operand], size: u8) -> Option<(Op, Vec)> { + if o != Op::IntRight { + return None; + } + let n = inputs.get(1)?.as_const()? as u32; + let s = self.scaled(inputs.first()?, REACH)?; + if s.signed || s.m < 2 || s.m > u64::MAX as i128 { + return None; + } + let (_, hi) = self.base_range(&s); + let d = unsigned_divisor(s.m as u64, s.shift.checked_add(n)?, hi as u64)?; + let x = self.dividend(&s, size)?; + Some((Op::IntDiv, vec![x, Operand::Const(d, size)])) + } + + /// A signed reciprocal: an arithmetic shift of a magic product plus the + /// sign bit of that same product, which is what rounds toward zero. + fn signed_quotient(&self, o: Op, inputs: &[Operand], size: u8) -> Option<(Op, Vec)> { + if o != Op::IntAdd { + return None; + } + let (l, r) = (*inputs.first()?, *inputs.get(1)?); + for (high, sign) in [(l, r), (r, l)] { + let Some((value, n)) = self.arithmetic_shift(&high) else { + continue; + }; + if self.sign_bit(&sign) != Some(value) { + continue; + } + let Some(s) = self.scaled(&value, REACH) else { + continue; + }; + if !s.signed || s.m < 2 || s.m > u64::MAX as i128 { + continue; + } + let bits = s.base.size().clamp(1, 8) as u32 * 8; + let Some(total) = s.shift.checked_add(n) else { + continue; + }; + let Some(d) = signed_divisor(s.m as u64, total, bits) else { + continue; + }; + let Some(x) = self.dividend(&s, size) else { + continue; + }; + return Some((Op::IntSDiv, vec![x, Operand::Const(d, size)])); + } + None + } + + /// `c ? a : b`, written the way a conditional move lifts: a mask that is + /// all ones or all zeroes, and its complement. + fn select(&self, o: &Operand) -> Option<(Operand, Operand, Operand)> { + let (Op::IntOr, parts, _) = self.def(o)? else { + return None; + }; + self.select_parts(parts) + } + + /// The same, read from the two halves of the `or` rather than from the + /// value they were combined into, for a rule whose whole subject is the + /// select and not something built on it. + fn select_parts(&self, parts: &[Operand]) -> Option<(Operand, Operand, Operand)> { + let (Op::IntAnd, left, _) = self.def(parts.first()?)? else { + return None; + }; + let (Op::IntAnd, right, _) = self.def(parts.get(1)?)? else { + return None; + }; + let (l0, l1) = (*left.first()?, *left.get(1)?); + let (r0, r1) = (*right.first()?, *right.get(1)?); + for (taken, mask) in [(l0, l1), (l1, l0)] { + let Some(condition) = self.condition_mask(&mask) else { + continue; + }; + for (other, complement) in [(r0, r1), (r1, r0)] { + if let Some((Op::IntNot, of, _)) = self.def(&complement) { + if of.first() == Some(&mask) { + return Some((condition, taken, other)); + } + } + } + } + None + } + + /// The condition a `0 - c` mask stands for. + fn condition_mask(&self, o: &Operand) -> Option { + let (op, inputs, _) = self.def(o)?; + let widened = match op { + Op::IntNegate => *inputs.first()?, + Op::IntSub if inputs.first()?.as_const() == Some(0) => *inputs.get(1)?, + _ => return None, + }; + let (Op::IntZExt, of, _) = self.def(&widened)? else { + return None; + }; + Some(*of.first()?) + } + + /// Whether a condition says `x` is negative, or says it is not. + fn negative_test(&self, c: &Operand, x: &Operand, depth: u32) -> Option { + if depth == 0 { + return None; + } + let (op, inputs, _) = self.def(c)?; + let first = *inputs.first()?; + let second = inputs.get(1).copied(); + let zero = |o: Option| o.and_then(|o| o.as_const()) == Some(0); + match op { + Op::Copy => self.negative_test(&first, x, depth - 1), + Op::BoolNot => Some(!self.negative_test(&first, x, depth - 1)?), + Op::IntSLess if zero(second) && self.same(&first, x) => Some(true), + // `0 <= x` is how the flag rewrite spells the complement. + Op::IntSLessEqual if first.as_const() == Some(0) && self.same(&second?, x) => { + Some(false) + } + Op::IntNotEqual if zero(second) => self.is_sign_test(&first, x).then_some(true), + Op::IntEqual if zero(second) => match self.is_sign_test(&first, x) { + true => Some(false), + false => Some(!self.negative_test(&first, x, depth - 1)?), + }, + _ => None, + } + } + + /// True when an operand is `x`'s sign bit, by shift or by mask. + fn is_sign_test(&self, o: &Operand, x: &Operand) -> bool { + let Some((op, inputs, size)) = self.def(o) else { + return false; + }; + let Some(first) = inputs.first() else { + return false; + }; + if !self.same(first, x) { + return false; + } + let top = size.clamp(1, 8) as u64 * 8 - 1; + match (op, inputs.get(1).and_then(|i| i.as_const())) { + (Op::IntRight, Some(n)) => n == top, + (Op::IntAnd, Some(k)) => k == 1u64 << top, + _ => false, + } + } + + /// A signed division by a power of two: the dividend biased when it is + /// negative, then shifted. The bias is what makes the shift round toward + /// zero the way the language says a division does. + fn power_of_two_quotient( + &self, + o: Op, + inputs: &[Operand], + size: u8, + ) -> Option<(Op, Vec)> { + if o != Op::IntSRight { + return None; + } + let k = inputs.get(1)?.as_const()?; + let bits = size.clamp(1, 8) as u64 * 8; + if k == 0 || k >= bits { + return None; + } + let x = self.biased(inputs.first()?, k, size)?; + Some((Op::IntSDiv, vec![x, Operand::Const(1u64 << k, size)])) + } + + /// The dividend `x` of a value that is `x` biased by `2^k - 1` when `x` is + /// negative, however the bias was arrived at. + fn biased(&self, o: &Operand, k: u64, size: u8) -> Option { + self.branch_biased(o, k, size) + .or_else(|| self.shift_biased(o, k, size)) + } + + /// `x < 0 ? x + 2^k - 1 : x`, which is what a conditional move spells. + fn branch_biased(&self, o: &Operand, k: u64, size: u8) -> Option { + let (condition, taken, other) = self.select(o)?; + for (biased, plain, negative) in [(taken, other, true), (other, taken, false)] { + let Some((Op::IntAdd, sum, _)) = self.def(&biased) else { + continue; + }; + if !self.same(sum.first()?, &plain) { + continue; + } + if sum.get(1)?.as_const() != Some((1u64 << k) - 1) { + continue; + } + if self.negative_test(&condition, &plain, REACH) != Some(negative) { + continue; + } + if plain.size() != size { + continue; + } + return Some(plain); + } + None + } + + /// `x + (2^k - 1 when x is negative)`, where the bias was computed with + /// shifts rather than with a branch. + /// + /// The sign bit of `x` is `2^1 - 1` or zero, so for a divisor of two one + /// logical shift is the whole bias. For anything wider the value has to be + /// smeared first: an arithmetic shift by `bits - 1` makes the sign a mask + /// of all ones or all zeroes, and a logical shift back by `bits - k` keeps + /// `k` of them. Reading the second shift alone would be wrong, because the + /// top `k` bits of a negative number are not all ones. + fn shift_biased(&self, o: &Operand, k: u64, size: u8) -> Option { + let (Op::IntAdd, sum, _) = self.def(o)? else { + return None; + }; + let bits = size.clamp(1, 8) as u64 * 8; + let (l, r) = (*sum.first()?, *sum.get(1)?); + for (x, bias) in [(l, r), (r, l)] { + if x.size() != size || bias.size() != size { + continue; + } + let Some((Op::IntRight, shift, width)) = self.def(&bias) else { + continue; + }; + let Some(n) = shift.get(1).and_then(|i| i.as_const()) else { + continue; + }; + let Some(inner) = shift.first() else { continue }; + if width != size { + continue; + } + if k == 1 && n == bits - 1 && self.same(inner, &x) { + return Some(x); + } + if n != bits - k { + continue; + } + let Some((Op::IntSRight, sign, smeared)) = self.def(inner) else { + continue; + }; + let whole = sign.get(1).and_then(|i| i.as_const()) == Some(bits - 1); + if smeared == size && whole && sign.first().is_some_and(|v| self.same(v, &x)) { + return Some(x); + } + } + None + } + + /// `x - ((x + bias) & -2^k)`, which is `x % 2^k` on a signed value. + /// + /// The masked value is the quotient times `2^k`, so what the subtraction + /// leaves is the remainder. A compiler reaches for this rather than a + /// shift and a multiply back because the mask costs one instruction. + fn masked_remainder(&self, o: Op, inputs: &[Operand], size: u8) -> Option<(Op, Vec)> { + if o != Op::IntSub { + return None; + } + let x = *inputs.first()?; + if x.size() != size { + return None; + } + let (Op::IntAnd, masked, width) = self.def(inputs.get(1)?)? else { + return None; + }; + let bits = size.clamp(1, 8) as u32 * 8; + let whole = if bits >= 64 { + u64::MAX + } else { + (1u64 << bits) - 1 + }; + let mask = masked.get(1)?.as_const()? & whole; + let k = mask.trailing_zeros() as u64; + if width != size || k == 0 || k >= bits as u64 || mask != (u64::MAX << k) & whole { + return None; + } + let y = self.biased(masked.first()?, k, size)?; + self.same(&y, &x) + .then(|| (Op::IntSRem, vec![x, Operand::Const(1u64 << k, size)])) + } + + /// `x < 0 ? -(x & 1) : (x & 1)`, which is `x % 2` on a signed value. + /// + /// The remainder of a signed division by two is the low bit wearing the + /// dividend's sign, and a compiler emits that rather than anything that + /// looks like a division. Two only: for a wider power the low bits of a + /// negative dividend are not its remainder, and a compiler biases the + /// value first instead, which is the rule above. + fn low_bit_remainder(&self, o: Op, inputs: &[Operand], size: u8) -> Option<(Op, Vec)> { + if o != Op::IntOr { + return None; + } + let (condition, taken, other) = self.select_parts(inputs)?; + for (negated, plain, negative) in [(taken, other, true), (other, taken, false)] { + if !self.is_negation(&negated, &plain) { + continue; + } + let Some((Op::IntAnd, masked, width)) = self.def(&plain) else { + continue; + }; + if width != size || masked.get(1)?.as_const() != Some(1) { + continue; + } + let x = *masked.first()?; + if x.size() != size { + continue; + } + if self.negative_test(&condition, &x, REACH) != Some(negative) { + continue; + } + return Some((Op::IntSRem, vec![x, Operand::Const(2, size)])); + } + None + } + + /// True when one operand is what another one negated. + fn is_negation(&self, o: &Operand, of: &Operand) -> bool { + let Some((op, inputs, _)) = self.def(o) else { + return false; + }; + let value = match op { + Op::IntNegate => inputs.first(), + Op::IntSub if inputs.first().and_then(|i| i.as_const()) == Some(0) => inputs.get(1), + _ => return false, + }; + value.is_some_and(|v| self.same(v, of)) + } + + /// How many times a quotient an operand is, if it is a multiple of one. + fn multiple_of_quotient(&self, o: &Operand, depth: u32) -> Option<(i128, Operand)> { + if depth == 0 { + return None; + } + let (op, inputs, size) = self.def(o)?; + let first = *inputs.first()?; + match op { + Op::IntDiv | Op::IntSDiv => Some((1, *o)), + Op::Copy | Op::IntZExt | Op::IntSExt => self.multiple_of_quotient(&first, depth - 1), + Op::SubPiece if inputs.get(1)?.as_const() == Some(0) => { + self.multiple_of_quotient(&first, depth - 1) + } + Op::IntNegate => { + let (k, q) = self.multiple_of_quotient(&first, depth - 1)?; + Some((-k, q)) + } + Op::IntMul => { + let (c, other) = multiplier(inputs, size)?; + let (k, q) = self.multiple_of_quotient(&other, depth - 1)?; + Some((k.checked_mul(c)?, q)) + } + Op::IntLeft => { + let n = inputs.get(1)?.as_const()? as u32; + let (k, q) = self.multiple_of_quotient(&first, depth - 1)?; + Some((k.checked_mul(1i128.checked_shl(n)?)?, q)) + } + Op::IntAdd | Op::IntSub => { + let (k, q) = self.multiple_of_quotient(&first, depth - 1)?; + let (j, p) = self.multiple_of_quotient(inputs.get(1)?, depth - 1)?; + if q != p { + return None; + } + let k = if op == Op::IntAdd { + k.checked_add(j)? + } else { + k.checked_sub(j)? + }; + Some((k, q)) + } + _ => None, + } + } + + /// `x - d * (x / d)`, which is the remainder the compiler did not have an + /// instruction for. + fn remainder(&self, o: Op, inputs: &[Operand], size: u8) -> Option<(Op, Vec)> { + let wanted: i128 = match o { + Op::IntSub => 1, + Op::IntAdd => -1, + _ => return None, + }; + let (l, r) = (*inputs.first()?, *inputs.get(1)?); + let orders: &[(Operand, Operand)] = if o == Op::IntSub { + &[(l, r)] + } else { + &[(l, r), (r, l)] + }; + for (x, multiple) in orders { + if x.size() != size { + continue; + } + let Some((k, q)) = self.multiple_of_quotient(multiple, REACH) else { + continue; + }; + let Some((division, operands, _)) = self.def(&q) else { + continue; + }; + let Some(d) = operands.get(1).and_then(|i| i.as_const()) else { + continue; + }; + if k != wanted * d as i128 || !self.same(operands.first()?, x) { + continue; + } + let rem = if division == Op::IntSDiv { + Op::IntSRem + } else { + Op::IntRem + }; + return Some((rem, vec![*x, Operand::Const(d, size)])); + } + None + } + + /// The division or remainder an operation is, if it is one. + fn recognize(&self, o: Op, inputs: &[Operand], size: u8) -> Option<(Op, Vec)> { + self.unsigned_quotient(o, inputs, size) + .or_else(|| self.signed_quotient(o, inputs, size)) + .or_else(|| self.power_of_two_quotient(o, inputs, size)) + .or_else(|| self.remainder(o, inputs, size)) + .or_else(|| self.masked_remainder(o, inputs, size)) + .or_else(|| self.low_bit_remainder(o, inputs, size)) + } +} + +/// A multiply's constant factor and whatever it multiplies, either way round. +/// +/// Multiplication commutes and the two lifters do not put the reciprocal on +/// the same side: x86-64 loads the magic number into a register and multiplies +/// the argument into it, so the constant arrives first. Reading only the second +/// operand left `x / 81` unfolded on one architecture and folded on the other. +fn multiplier(inputs: &[Operand], size: u8) -> Option<(i128, Operand)> { + let (l, r) = (*inputs.first()?, *inputs.get(1)?); + match (l.as_const(), r.as_const()) { + (_, Some(c)) => Some((sign_extended(c, size), l)), + (Some(c), None) => Some((sign_extended(c, size), r)), + _ => None, + } +} + +/// A constant read as the signed number a `size`-byte operand holds. +fn sign_extended(v: u64, size: u8) -> i128 { + match size.clamp(1, 8) as u32 * 8 { + 64 => v as i64 as i128, + bits => { + let shift = 128 - bits; + ((v as i128) << shift) >> shift + } + } +} + +/// Rewrite the reciprocal multiplies a compiler emits back into divisions. +/// +/// No compiler emits a divide instruction for a constant divisor: it +/// multiplies by a fixed point reciprocal and shifts, and three or four +/// instructions stand where one operator did. Undoing that is the difference +/// between `x / 81` and `x * 0xca4587e7 >> 38`. +/// +/// The recovery is arithmetic, not a table of known constants: the divisor +/// follows from the magic number, the shift and the range the dividend lives +/// in. Every candidate is then checked against the true quotient over that +/// whole range in closed form, and one that does not agree everywhere is not +/// emitted. A division nothing verified would be a silently wrong program, +/// which is worse than the multiply it replaced. +pub fn divisions(f: &mut SsaFunction) -> Changes { + let mut source: BTreeMap, u8)> = BTreeMap::new(); + for b in f.blocks.values() { + for op in &b.ops { + if let (Some(out), SsaKind::Op(o)) = (op.out, op.kind.clone()) { + source.insert(out, (o, op.inputs.clone(), op.size)); + } + } + } + let known = KnownBits::of(f); + let rules = Reciprocal { + source: &source, + known: &known, + memo: RefCell::new(BTreeMap::new()), + }; + + let mut changes = Changes::default(); + for b in f.blocks.values_mut() { + for op in b.ops.iter_mut() { + let SsaKind::Op(o) = op.kind else { continue }; + let Some((kind, inputs)) = rules.recognize(o, &op.inputs, op.size) else { + continue; + }; + op.kind = SsaKind::Op(kind); + op.inputs = inputs; + changes.folded += 1; + } + } + changes +} + +/// Replace a repeated computation with the value it already produced. +/// +/// Within a block only, and only for operations with no effect, so the +/// question of whether the earlier one is reachable from the later never +/// arises. Lifting produces these constantly: a compare and the branch that +/// reads it both recompute the same subtraction, and until they are the same +/// value no rule can see that they are the same condition. +pub fn cse(f: &mut SsaFunction) -> Changes { + let mut changes = Changes::default(); + for b in f.blocks.values_mut() { + let mut seen: BTreeMap<(Op, Vec, u8), Value> = BTreeMap::new(); + for op in &mut b.ops { + let SsaKind::Op(o) = op.kind else { continue }; + if !pure(o) { + continue; + } + let Some(out) = op.out else { continue }; + let key = (o, op.inputs.clone(), op.size); + match seen.get(&key) { + Some(earlier) => { + op.kind = SsaKind::Op(Op::Copy); + op.inputs = vec![Operand::Value(*earlier)]; + changes.folded += 1; + } + None => { + seen.insert(key, out); + } + } + } + } + changes +} + +/// True when an operation depends on nothing but its inputs. +fn pure(o: Op) -> bool { + !matches!( + o, + Op::Load + | Op::Store + | Op::Call + | Op::CallInd + | Op::Branch + | Op::CBranch + | Op::BranchInd + | Op::Return + | Op::Unimplemented + | Op::Copy + ) +} diff --git a/crates/e5r-ir/src/proto.rs b/crates/e5r-ir/src/proto.rs new file mode 100644 index 0000000..b19afc9 --- /dev/null +++ b/crates/e5r-ir/src/proto.rs @@ -0,0 +1,808 @@ +//! What a function takes and gives back, worked out from what it does. +//! +//! A prototype is not written down in a stripped binary, but it is visible: a +//! register read before anything wrote it arrived with a value, and a register +//! written on the way to a return is left for the caller. That is enough to +//! recover the shape of most functions, and where it is not, saying so is +//! better than inventing a parameter. +//! +//! The convention gives the candidates and the code decides which of them are +//! used. A function that reads the third argument register and not the first +//! two still takes three arguments: the first two are unused, not absent. + +use std::collections::BTreeSet; +use std::fmt; + +use e5r_core::Arch; +use e5r_core::provenance::Strength; +use e5r_types::cdecl; +use e5r_types::ctype::{Model, Signature, Type, TypeId, Types}; + +use crate::abi::Abi; +use crate::conv::{self, Definitions, Detected, Observed}; +use crate::op::{Op, Space}; +use crate::ssa::{Location, Operand, SsaFunction, SsaKind}; + +/// What a function appears to take and return. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Prototype { + /// How many integer argument registers it takes, counting the unused ones + /// before the last one it reads. + pub integer_arguments: usize, + /// How many floating point ones. + pub float_arguments: usize, + /// How wide each integer argument is actually read, in bytes. + /// + /// A register arrives eight bytes wide whatever the source declared, so a + /// prototype taken from the register file makes every argument + /// `uint64_t`. When a function only ever reads the low four bytes of one, + /// four is the width it was written against. Empty when nothing was + /// recovered; otherwise one entry per integer argument. + pub integer_widths: Vec, + /// The same for each floating point argument, which is how a `float` + /// argument is told from a `double` one: both arrive in the same register + /// and only the reads say which was declared. + pub float_widths: Vec, + /// Offsets from the entry stack pointer of the arguments passed there. + pub stack_arguments: Vec, + /// Where it leaves a value the caller could read, when it leaves one. + /// + /// A possibility, not a promise. A function that computes an address into + /// the first argument register on its way out has left something there, + /// and nothing inside the function says whether anyone reads it: only its + /// callers do. What this does guarantee is the other direction — a + /// function that leaves nothing has `None` — so a caller is never denied a + /// result that exists. + pub returns: Option, + /// True when the result is a floating point register. + pub returns_float: bool, + /// Callee-saved registers it writes without putting back, which is a + /// convention the compiler invented rather than the standard one. + pub unsaved: Vec, + /// True when the function puts back every callee-saved register it + /// touched, which is the part of the standard convention a function alone + /// can be checked against. + /// + /// Narrower than [`Prototype::detected`], which compares everything. + pub standard: bool, + /// The convention the function actually uses, as opposed to the one the + /// platform declares. + /// + /// Recovery on its own fills the counts above from the platform default's + /// fixed order. This says whether that order is the one the function + /// keeps, names the registers when it is not, and says how strongly. A + /// detected convention that contradicts the default is reported here and + /// never normalised into it. + pub detected: Detected, + /// The name a declaration gave the function, when one did. + pub name: Option, + /// The parameters, with the types a declaration gave them. + /// + /// Empty when nothing was asserted: recovery on its own knows how many + /// registers arrive with values and nothing about what is in them. + pub parameters: Vec, + /// The declared return type, when a declaration gave one. `None` here and + /// a `returns` register together mean the code leaves something behind + /// that nobody has named. + pub return_type: Option, + /// True when a declaration said the function is variadic. + pub varargs: bool, + /// True when the declared result is too large for the result registers and + /// the caller passes its address as a hidden first argument. + /// + /// The emitter needs this: with it set, the first integer argument + /// register is not parameter zero. + pub returns_via_memory: bool, + /// Which of these fields the code decided and which a person did. + pub origin: Origin, + /// Where the declaration and the code disagree. The declaration wins; this + /// is the record of what it overrode. + pub conflicts: Vec, + /// What the code alone said, kept when a declaration overrode it. + /// + /// An analyst who declares three parameters where the code reads four + /// registers is entitled to be believed, and the fourth read still + /// happened. Nothing the machine saw is thrown away by an assertion. + pub recovered: Option>, +} + +/// Where each part of a prototype came from. +/// +/// `ROADMAP.md` bet 4: every fact says how strongly it is known. A parameter +/// count worked out from the registers a function reads is +/// [`Strength::Inferred`]; one a person wrote down is [`Strength::Asserted`] +/// and outranks it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Origin { + /// The parameters: how many, where and what. + pub arguments: Strength, + /// The result and its register. + pub returns: Strength, + /// Whether it is variadic. + pub varargs: Strength, +} + +impl Default for Origin { + fn default() -> Origin { + // Recovery reads a proven decode and takes a step past it, which is + // exactly what `Inferred` means. + Origin { + arguments: Strength::Inferred, + returns: Strength::Inferred, + varargs: Strength::Inferred, + } + } +} + +/// Where the convention passes one argument. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Storage { + /// The nth integer argument register the convention names. + Integer(usize), + /// The nth floating point one. + Float(usize), + /// A slot at this offset from the entry stack pointer. + Stack(i64), +} + +/// One parameter of a declared prototype. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Parameter { + /// Its name, when the declaration gave one. + pub name: Option, + /// Its declared type. + pub ty: TypeId, + /// Its width in bytes, zero when the type has no size. + pub size: u64, + /// Where it is passed. More than one slot for an aggregate that spans + /// registers. + pub storage: Vec, + /// Whether the code or a person decided this. + pub strength: Strength, +} + +/// A disagreement between an asserted declaration and the code. +/// +/// Every one of these is reported and none of them changes the answer: the +/// declaration is the analyst's to make. The point is that an output which +/// differs from what the machine found can say why. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Conflict { + /// The code reads an argument register the declaration does not account + /// for. The declaration still wins; this says what was set aside. + ExtraArgument { + /// The register file offset the code read. + register: u64, + /// Which argument slot of the convention that is. + slot: usize, + }, + /// The declaration names an argument the code never reads. + UnreadArgument { + /// Its index in the declaration. + index: usize, + }, + /// The declaration needs a register the convention does not pass arguments + /// or results in. + OffConvention { + /// The parameter it applies to, or `None` for the result. + index: Option, + /// What the convention cannot do. + detail: String, + }, + /// The declaration returns nothing and the code leaves a value behind. + ResultDiscarded { + /// Where the code left it. + register: u64, + }, + /// The declaration returns a value the code never computes. + ResultMissing, + /// The declaration's result register is not the one the code writes. + ResultElsewhere { + /// Where the convention says the declared result goes. + declared: u64, + /// Where the code actually writes. + written: u64, + }, +} + +impl fmt::Display for Conflict { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Conflict::ExtraArgument { register, slot } => write!( + f, + "the code reads argument register {slot} (offset {register:#x}), which the declaration does not name" + ), + Conflict::UnreadArgument { index } => { + write!(f, "parameter {index} is declared and never read") + } + Conflict::OffConvention { index, detail } => match index { + Some(n) => write!(f, "parameter {n}: {detail}"), + None => write!(f, "the result: {detail}"), + }, + Conflict::ResultDiscarded { register } => write!( + f, + "the declaration returns nothing and the code leaves a value in {register:#x}" + ), + Conflict::ResultMissing => { + write!(f, "the declaration returns a value the code never computes") + } + Conflict::ResultElsewhere { declared, written } => write!( + f, + "the declaration's result belongs in {declared:#x} and the code writes {written:#x}" + ), + } + } +} + +/// A C declaration a person asserted about a function. +/// +/// It owns its types because a declaration mentions types nothing else in the +/// program has heard of: an analyst who writes `int f(struct sockaddr *)` has +/// just introduced `struct sockaddr`. +#[derive(Debug, Clone)] +pub struct Asserted { + /// The types the declaration mentions. + pub types: Types, + /// The function's own signature. + pub signature: Signature, + /// The name the declaration gave, when it gave one. + pub name: Option, + /// The text it was written as. The annotation log stores the source and + /// parses on read, so this is what came out of the log. + pub text: String, +} + +impl Asserted { + /// Parse a C function declaration, laid out the way `arch` lays types out. + pub fn parse(text: &str, arch: &Arch) -> Result { + Asserted::parse_into(Types::for_model(model_of(arch)), text) + } + + /// The same, starting from a store that already holds the program's types, + /// so a declaration can refer to a structure recovered from debug + /// information by name. + pub fn parse_into(mut types: Types, text: &str) -> Result { + let (name, signature) = cdecl::prototype(&mut types, text)?; + Ok(Asserted { + types, + signature, + name, + text: text.to_string(), + }) + } +} + +/// The widths and alignments an architecture's C compiler uses. +/// +/// The container decides this as much as the machine does: the same x86-64 +/// code is LP64 under ELF and LLP64 under PE. Only the architecture is known +/// here, so this is the ELF and Mach-O answer and a PE loader that wants the +/// other one passes [`Model::llp64`] itself. +pub fn model_of(arch: &Arch) -> Model { + match arch { + Arch::X86_64 => Model::lp64(), + // A plain `char` is unsigned on ARM, which changes what a comparison + // against a byte means. + Arch::AArch64 => Model::lp64().with_unsigned_char(), + Arch::X86 => Model::ilp32(), + Arch::Arm | Arch::Thumb => Model::ilp32().with_unsigned_char(), + _ => Model::lp64(), + } +} + +impl Prototype { + /// How many arguments in total, for a caller that has to pass them. + pub fn arity(&self) -> usize { + if !self.parameters.is_empty() { + return self.parameters.len(); + } + self.integer_arguments + self.float_arguments + self.stack_arguments.len() + } + + /// True when any part of this was written down by a person. + pub fn is_asserted(&self) -> bool { + self.origin.arguments == Strength::Asserted + || self.origin.returns == Strength::Asserted + || self.origin.varargs == Strength::Asserted + } +} + +/// Recover what a function takes and returns, letting a declaration override +/// what the code says. +/// +/// Recovery runs either way, and fills in everything the declaration did not +/// say: an assertion about the parameters leaves the callee-saved analysis +/// alone, because a person writing a prototype has said nothing about that. +/// What the machine found is kept in [`Prototype::recovered`] whenever a +/// declaration displaced it. +pub fn recover_with(f: &SsaFunction, abi: &Abi, asserted: Option<&Asserted>) -> Prototype { + let machine = recover(f, abi); + match asserted { + Some(a) => apply(machine, a, abi), + None => machine, + } +} + +/// Lay an asserted signature over a recovered prototype. +fn apply(machine: Prototype, asserted: &Asserted, abi: &Abi) -> Prototype { + let (parameters, mut conflicts, reserved) = classify(&asserted.types, &asserted.signature, abi); + + // The hidden result pointer occupies a register no declared parameter + // names, so the count is not just what the parameters landed on. + let integer_arguments = slots_used(¶meters, |s| match s { + Storage::Integer(n) => Some(*n), + _ => None, + }) + .max(reserved); + let float_arguments = slots_used(¶meters, |s| match s { + Storage::Float(n) => Some(*n), + _ => None, + }); + let mut stack_arguments: Vec = parameters + .iter() + .flat_map(|p| p.storage.iter()) + .filter_map(|s| match s { + Storage::Stack(o) => Some(*o), + _ => None, + }) + .collect(); + stack_arguments.sort(); + stack_arguments.dedup(); + + // A register the code reads that the declaration does not account for is + // not dropped: honour the declaration and say what was set aside. + for slot in integer_arguments..machine.integer_arguments { + if let Some(register) = abi.integer_arguments.get(slot) { + conflicts.push(Conflict::ExtraArgument { + register: *register, + slot, + }); + } + } + for slot in float_arguments..machine.float_arguments { + if let Some(register) = abi.float_arguments.get(slot) { + conflicts.push(Conflict::ExtraArgument { + register: *register, + slot, + }); + } + } + for (index, p) in parameters.iter().enumerate() { + let read = p.storage.iter().any(|s| match s { + Storage::Integer(n) => *n < machine.integer_arguments, + Storage::Float(n) => *n < machine.float_arguments, + Storage::Stack(o) => machine.stack_arguments.contains(o), + }); + if !read { + conflicts.push(Conflict::UnreadArgument { index }); + } + } + + let (returns, returns_float, mut return_conflicts) = + classify_return(&asserted.types, &asserted.signature, abi, &machine); + conflicts.append(&mut return_conflicts); + + Prototype { + integer_arguments, + float_arguments, + // An asserted declaration carries its own types, so the widths the + // body reads at are the recovered prototype's business, not this one's. + integer_widths: Vec::new(), + float_widths: Vec::new(), + stack_arguments, + returns, + returns_float, + // A person writing a prototype has said nothing about which + // callee-saved registers the body clobbers, so the code keeps these. + unsaved: machine.unsaved.clone(), + standard: machine.standard, + detected: machine.detected.clone(), + name: asserted.name.clone(), + return_type: asserted.signature.returns, + varargs: asserted.signature.varargs, + returns_via_memory: returns_in_memory(&asserted.types, &asserted.signature), + origin: Origin { + arguments: Strength::Asserted, + returns: Strength::Asserted, + varargs: Strength::Asserted, + }, + parameters, + conflicts, + recovered: Some(Box::new(machine)), + } +} + +fn slots_used(parameters: &[Parameter], pick: impl Fn(&Storage) -> Option) -> usize { + parameters + .iter() + .flat_map(|p| p.storage.iter()) + .filter_map(pick) + .map(|n| n + 1) + .max() + .unwrap_or(0) +} + +/// Place a declared signature's parameters on the convention's registers. +/// +/// This is the common path of the System V and AAPCS rules and not the whole +/// of either: an integer or a pointer takes one integer register, a `float` or +/// `double` takes one vector register, a small aggregate takes as many integer +/// registers as it needs, and anything that does not fit goes on the stack in +/// declaration order. The classification of an aggregate holding floats +/// differs between the two conventions and is not modelled; where this is +/// wrong it is wrong about where a value is, which is reported as a conflict +/// when the code disagrees rather than silently believed. +#[allow(clippy::type_complexity)] +fn classify(types: &Types, sig: &Signature, abi: &Abi) -> (Vec, Vec, usize) { + let mut out = Vec::new(); + let mut conflicts = Vec::new(); + let mut integers = 0usize; + let mut floats = 0usize; + let mut stack = abi.stack_argument_base; + + // A result too large for the registers is returned in memory the caller + // provides, and the pointer to it arrives as a hidden first argument. It + // shifts every declared parameter by one register, which is the kind of + // thing that makes a decompilation wrong everywhere if it is missed. + let mut reserved = 0usize; + if returns_in_memory(types, sig) { + if abi.integer_arguments.is_empty() { + conflicts.push(Conflict::OffConvention { + index: None, + detail: "the result is returned in memory and the convention has no register to pass the address in" + .into(), + }); + } else { + integers += 1; + reserved = 1; + } + } + + for (index, (name, ty)) in sig.parameters.iter().enumerate() { + let size = types.size_of(*ty).unwrap_or(0); + let resolved = types.resolve(*ty); + let mut storage = Vec::new(); + + let wants_float = matches!(types.get(resolved), Some(Type::Float { size: 4 | 8 })); + if wants_float { + if abi.float_arguments.is_empty() { + conflicts.push(Conflict::OffConvention { + index: Some(index), + detail: "is floating point and the convention passes no arguments in floating point registers" + .into(), + }); + } else if floats < abi.float_arguments.len() { + storage.push(Storage::Float(floats)); + floats += 1; + } + } else { + let registers = register_count(types, resolved, size, abi); + if registers > 0 && integers + registers <= abi.integer_arguments.len() { + for _ in 0..registers { + storage.push(Storage::Integer(integers)); + integers += 1; + } + } + } + + if storage.is_empty() { + // Everything that did not land in a register lands on the stack, + // aligned to a slot, in declaration order. + let slot = abi.stack_argument_base.max(8); + let width = size.max(1).div_ceil(slot as u64) * slot as u64; + storage.push(Storage::Stack(stack)); + stack += width as i64; + } + + out.push(Parameter { + name: name.clone(), + ty: *ty, + size, + storage, + strength: Strength::Asserted, + }); + } + (out, conflicts, reserved) +} + +/// How many integer registers a value of this type occupies, or zero when it +/// does not go in registers at all. +fn register_count(types: &Types, resolved: TypeId, size: u64, abi: &Abi) -> usize { + let slot = abi.stack_argument_base.max(8) as u64; + match types.get(resolved) { + // An aggregate up to two registers wide is passed in them; anything + // larger is passed in memory by every convention this models. + Some(Type::Composite(_)) | Some(Type::Array(..)) => { + if size == 0 || size > 2 * slot { + 0 + } else { + size.div_ceil(slot) as usize + } + } + // An x87 `long double` is passed in memory on x86-64 and this is the + // only float wider than a register. + Some(Type::Float { size }) if *size as u64 > slot => 0, + Some(Type::Void) | None => 0, + _ => 1, + } +} + +/// True when the declared result is too large for the result registers. +fn returns_in_memory(types: &Types, sig: &Signature) -> bool { + let Some(ty) = sig.returns else { return false }; + let resolved = types.resolve(ty); + if !matches!( + types.get(resolved), + Some(Type::Composite(_)) | Some(Type::Array(..)) + ) { + return false; + } + types.size_of(ty).is_none_or(|s| s > 16) +} + +/// Where the declared result goes, and how that compares with the code. +fn classify_return( + types: &Types, + sig: &Signature, + abi: &Abi, + machine: &Prototype, +) -> (Option, bool, Vec) { + let mut conflicts = Vec::new(); + let Some(ty) = sig.returns else { + if let Some(register) = machine.returns { + conflicts.push(Conflict::ResultDiscarded { register }); + } + return (None, false, conflicts); + }; + + let resolved = types.resolve(ty); + let wants_float = matches!(types.get(resolved), Some(Type::Float { size: 4 | 8 })); + let declared = if wants_float { + match abi.results.iter().find(|r| **r >= abi.vector_base) { + Some(r) => Some(*r), + None => { + conflicts.push(Conflict::OffConvention { + index: None, + detail: + "is floating point and the convention has no floating point result register" + .into(), + }); + abi.results.first().copied() + } + } + } else { + match abi.results.iter().find(|r| **r < abi.vector_base) { + Some(r) => Some(*r), + None => { + conflicts.push(Conflict::OffConvention { + index: None, + detail: "the convention has no integer result register".into(), + }); + None + } + } + }; + + match (declared, machine.returns) { + (Some(d), Some(w)) if d != w => { + conflicts.push(Conflict::ResultElsewhere { + declared: d, + written: w, + }); + } + (Some(_), None) => conflicts.push(Conflict::ResultMissing), + _ => {} + } + (declared, wants_float, conflicts) +} + +/// Recover what a function takes and returns. +pub fn recover(f: &SsaFunction, abi: &Abi) -> Prototype { + recover_observed(f, abi, &Observed::default()) +} + +/// The same, with what the function's callers said about it. +/// +/// A function alone cannot settle whether it returns anything, and cannot +/// corroborate a convention the compiler invented. Its callers can: see +/// [`conv::observe`], which turns one caller's code into this. +pub fn recover_observed(f: &SsaFunction, abi: &Abi, observed: &Observed) -> Prototype { + let facts = conv::facts(f); + let definitions = f.definitions(); + let detected = conv::detect_from(f, abi, &facts, &definitions, observed); + + // The highest argument register read, plus one: a function that reads the + // third and not the first two still takes three. + let integer_arguments = last_used(&facts.live_in, &abi.integer_arguments); + let float_arguments = last_used(&facts.live_in, &abi.float_arguments); + + // Arguments the caller left on the stack, which promotion turned into + // locations above the entry stack pointer. + let mut stack_arguments: Vec = facts + .live_in + .iter() + .filter(|l| l.space == Space::Stack && (l.offset as i64) > 0) + .map(|l| l.offset as i64) + .collect(); + stack_arguments.sort(); + stack_arguments.dedup(); + + // The result, once the callers have had their say. + let returns = detected.result; + let returns_float = detected.result_float; + + // A callee-saved register this function writes and does not put back. + let unsaved = conv::clobbered(f, abi, &facts.written, &definitions); + + let integer_widths = (0..integer_arguments) + .map(|n| match abi.integer_arguments.get(n) { + Some(offset) => read_width(f, &facts.live_in, *offset), + None => 8, + }) + .collect(); + let float_widths = (0..float_arguments) + .map(|n| match abi.float_arguments.get(n) { + Some(offset) => read_width(f, &facts.live_in, *offset), + None => 8, + }) + .collect(); + + Prototype { + integer_arguments, + float_arguments, + integer_widths, + float_widths, + stack_arguments, + returns, + returns_float, + standard: unsaved.is_empty(), + unsaved, + detected, + ..Prototype::default() + } +} + +/// How wide an incoming argument register is actually read. +/// +/// Every read of it has to take the low piece and nothing may read the rest. +/// One full-width read, one piece taken from anywhere but the start, or a use +/// as anything other than the value being narrowed, and the register keeps the +/// width it arrived at -- a prototype that claims a narrower argument than the +/// body reads would drop bits the machine uses. +fn read_width(f: &SsaFunction, live_in: &BTreeSet, offset: u64) -> u8 { + let Some(l) = live_in + .iter() + .find(|l| l.space == Space::Register && l.offset == offset) + else { + return 8; + }; + let arriving = Operand::Undefined(*l); + let mut widest = 0u8; + for b in f.blocks.values() { + for op in &b.ops { + let narrowing = op.kind == SsaKind::Op(Op::SubPiece) + && op.size < l.size + && matches!(op.inputs.get(1), Some(Operand::Const(0, _))); + for (i, input) in op.inputs.iter().enumerate() { + if *input != arriving { + continue; + } + if !(narrowing && i == 0) { + return l.size; + } + widest = widest.max(op.size); + } + } + } + if widest == 0 { l.size } else { widest } +} + +/// One past the last of `candidates` that appears among the live-in set. +fn last_used(live_in: &BTreeSet, candidates: &[u64]) -> usize { + candidates + .iter() + .enumerate() + .filter(|(_, offset)| { + live_in + .iter() + .any(|l| l.space == Space::Register && l.offset == **offset) + }) + .map(|(n, _)| n + 1) + .next_back() + .unwrap_or(0) +} + +/// Which result register the function leaves a value in, if any. +/// +/// The value that reaches a return, when the function computed it. A register +/// still holding what it arrived with was not a result: the caller put it +/// there. The one written latest wins, because a function that computes into a +/// general register and then converts into a vector one writes both. +pub(crate) fn returned( + f: &SsaFunction, + abi: &Abi, + written: &BTreeSet, + definitions: &Definitions, +) -> Option { + let mut best: Option<(usize, u64)> = None; + for b in f.blocks.values() { + if !b.ops.iter().any(|op| op.kind == SsaKind::Op(Op::Return)) { + continue; + } + for offset in &abi.results { + if !written.contains(offset) { + continue; + } + let location = Location { + space: Space::Register, + offset: *offset, + size: 8, + }; + // What reaches the return: the last definition in this block, or + // the newest one anywhere when the block itself did not write it. + let (rank, value) = match b + .ops + .iter() + .enumerate() + .rev() + .find(|(_, op)| op.out.is_some_and(|v| v.location == location)) + { + Some((n, op)) => (n, op.out?), + None => { + let newest = f + .blocks + .values() + .flat_map(|other| other.ops.iter()) + .filter_map(|op| op.out) + .filter(|v| v.location == location) + .max_by_key(|v| v.version)?; + (0, newest) + } + }; + if is_entry_value(f, definitions, value, location, 0) { + continue; + } + if best.is_none_or(|(at, _)| rank > at) { + best = Some((rank, *offset)); + } + } + } + best.map(|(_, offset)| offset) +} + +/// True when a value is the one the function was entered with, however many +/// copies and merges it came through. +pub(crate) fn is_entry_value( + f: &SsaFunction, + definitions: &Definitions, + value: crate::ssa::Value, + location: Location, + depth: u32, +) -> bool { + if depth > 16 { + return false; + } + let Some((block, index)) = definitions.get(&value) else { + return false; + }; + let Some(op) = f.blocks.get(block).and_then(|b| b.ops.get(*index)) else { + return false; + }; + let inputs: Vec<&Operand> = match op.kind { + SsaKind::Phi => op.inputs.iter().collect(), + SsaKind::Op(Op::Copy) | SsaKind::Op(Op::Load) => op.inputs.iter().take(1).collect(), + _ => return false, + }; + // A load restores a spilled register: what it reads is not tracked, so a + // reload of the same location counts as a restore only when the function + // spilled it first. Treating a load as a restore is the assumption a + // prologue and epilogue actually satisfy. + if op.kind == SsaKind::Op(Op::Load) { + return true; + } + inputs.iter().all(|i| match i { + Operand::Undefined(l) => *l == location, + Operand::Value(v) => is_entry_value(f, definitions, *v, location, depth + 1), + Operand::Const(..) => false, + }) +} diff --git a/crates/e5r-ir/src/range.rs b/crates/e5r-ir/src/range.rs new file mode 100644 index 0000000..710fd55 --- /dev/null +++ b/crates/e5r-ir/src/range.rs @@ -0,0 +1,374 @@ +//! What values a computation can produce. +//! +//! An interval per value, worked out from the operations that make it. It is +//! what bounds a jump table's index, what proves a comparison is always true, +//! and what says a loop counter never leaves the array it walks. +//! +//! Every answer is sound in one direction: the interval always contains every +//! value the computation can produce. It is allowed to be wider than the truth +//! and is often very much wider, but it never excludes something that can +//! happen, because a bound that is sometimes wrong is worse than no bound. + +use std::collections::BTreeMap; + +use crate::op::{Op, Space}; +use crate::ssa::{Operand, SsaFunction, SsaKind, Value}; + +/// The values a computation can produce, as a signed interval. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Range { + /// The smallest, read as signed. + pub low: i64, + /// The largest. + pub high: i64, +} + +impl Range { + /// Everything. + pub const ANY: Range = Range { + low: i64::MIN, + high: i64::MAX, + }; + + /// Exactly one value. + pub fn exact(v: i64) -> Range { + Range { low: v, high: v } + } + + /// From a bound pair, in either order. + pub fn new(a: i64, b: i64) -> Range { + Range { + low: a.min(b), + high: a.max(b), + } + } + + /// Everything a value of this many bytes can hold, unsigned. + pub fn unsigned(size: u8) -> Range { + match size { + 1 => Range::new(0, 0xff), + 2 => Range::new(0, 0xffff), + 4 => Range::new(0, 0xffff_ffff), + _ => Range::ANY, + } + } + + /// Everything a value of this many bytes can hold, read as signed. + pub fn signed(size: u8) -> Range { + match size { + 1 => Range::new(-0x80, 0x7f), + 2 => Range::new(-0x8000, 0x7fff), + 4 => Range::new(-0x8000_0000, 0x7fff_ffff), + _ => Range::ANY, + } + } + + /// This interval with its `size` bytes read as signed rather than + /// unsigned, which is what a sign extension does to its source. + /// + /// A narrow read gives an unsigned interval, so `0..=0xff` on a byte is + /// every byte and not every non-negative byte. Passing that through a sign + /// extension unchanged claims `(int8_t)b` cannot be negative, which is how + /// a range came to exclude a value the processor produced. + pub fn reinterpret_signed(self, size: u8) -> Range { + let bits = size as u32 * 8; + if bits >= 64 { + return self; + } + let modulus = 1i64 << bits; + let half = modulus >> 1; + // Wholly inside one half: shift it or leave it, and the interval stays + // exact. Straddling the halves means the wrap is inside it, and the + // only sound answer is everything the width can hold. + if self.low >= 0 && self.high < half { + self + } else if self.low >= half && self.high < modulus { + Range::new(self.low - modulus, self.high - modulus) + } else { + Range::signed(size) + } + } + + /// True when nothing outside this interval can happen. + pub fn contains(&self, v: i64) -> bool { + self.low <= v && v <= self.high + } + + /// True when the interval is everything, which is the same as knowing + /// nothing. + pub fn is_any(&self) -> bool { + self.low == i64::MIN && self.high == i64::MAX + } + + /// How many values it admits, saturating. + pub fn width(&self) -> u64 { + (self.high as i128 - self.low as i128).unsigned_abs() as u64 + } + + /// The smallest interval containing both. + pub fn join(self, other: Range) -> Range { + Range { + low: self.low.min(other.low), + high: self.high.max(other.high), + } + } + + fn add(self, other: Range) -> Range { + match ( + self.low.checked_add(other.low), + self.high.checked_add(other.high), + ) { + (Some(low), Some(high)) => Range { low, high }, + _ => Range::ANY, + } + } + + fn sub(self, other: Range) -> Range { + match ( + self.low.checked_sub(other.high), + self.high.checked_sub(other.low), + ) { + (Some(low), Some(high)) => Range { low, high }, + _ => Range::ANY, + } + } + + fn mul(self, other: Range) -> Range { + // Every corner, because the signs decide which is the extreme. + let corners = [ + self.low.checked_mul(other.low), + self.low.checked_mul(other.high), + self.high.checked_mul(other.low), + self.high.checked_mul(other.high), + ]; + if corners.iter().any(|c| c.is_none()) { + return Range::ANY; + } + let values: Vec = corners.into_iter().flatten().collect(); + Range { + low: *values.iter().min().unwrap_or(&i64::MIN), + high: *values.iter().max().unwrap_or(&i64::MAX), + } + } +} + +/// How many rounds before an interval that keeps growing is given up on. +const ROUNDS: u32 = 8; + +/// The interval of every value in a function. +pub fn ranges(f: &SsaFunction) -> BTreeMap { + let mut out: BTreeMap = BTreeMap::new(); + // Iterate: a phi depends on values defined later, and the intervals only + // widen, so this settles. A value still growing when the rounds run out is + // widened to everything rather than left half-computed. + for round in 0..ROUNDS { + let mut changed = false; + for b in f.blocks.values() { + for op in &b.ops { + let Some(out_value) = op.out else { continue }; + let mut computed = evaluate(op, &out); + // The last round widens anything that is still moving, which + // is what makes a loop counter terminate at something honest. + if round + 1 == ROUNDS { + if let Some(previous) = out.get(&out_value) { + if *previous != computed { + computed = Range::ANY; + } + } + } + let joined = match out.get(&out_value) { + Some(previous) => previous.join(computed), + None => computed, + }; + if out.get(&out_value) != Some(&joined) { + out.insert(out_value, joined); + changed = true; + } + } + } + if !changed { + break; + } + } + out +} + +/// The interval one operand can take. +pub fn of(operand: &Operand, known: &BTreeMap) -> Range { + match operand { + Operand::Const(v, size) => { + // A constant is exact, read at its own width the way the + // operation that made it will read it. + let bits = (*size as u32 * 8).min(64); + let signed = if bits == 64 { + *v as i64 + } else { + ((*v << (64 - bits)) as i64) >> (64 - bits) + }; + Range::exact(signed) + } + Operand::Value(v) => known.get(v).copied().unwrap_or(Range::ANY), + Operand::Undefined(l) if l.space == Space::Register => Range::ANY, + Operand::Undefined(_) => Range::ANY, + } +} + +fn evaluate(op: &crate::ssa::SsaOp, known: &BTreeMap) -> Range { + let a = || { + op.inputs + .first() + .map(|i| of(i, known)) + .unwrap_or(Range::ANY) + }; + let b = || op.inputs.get(1).map(|i| of(i, known)).unwrap_or(Range::ANY); + + match op.kind { + // A merge can be anything either path produced. + SsaKind::Phi => op + .inputs + .iter() + .map(|i| of(i, known)) + .reduce(Range::join) + .unwrap_or(Range::ANY), + SsaKind::Op(kind) => match kind { + Op::Copy => a(), + Op::IntAdd => a().add(b()), + Op::IntSub => a().sub(b()), + Op::IntMul => a().mul(b()), + // A mask cannot produce a bit the mask does not have. + Op::IntAnd => match op.inputs.get(1).and_then(|i| i.as_const()) { + Some(mask) if mask <= i64::MAX as u64 => { + let bounded = Range::new(0, mask as i64); + // Masking a value already known non-negative cannot raise + // it either. + let source = a(); + if source.low >= 0 { + Range::new(0, source.high.min(mask as i64)) + } else { + bounded + } + } + _ => Range::ANY, + }, + Op::IntOr | Op::IntXor => Range::ANY, + // A shift by a constant is a multiply or a divide. + Op::IntLeft => match op.inputs.get(1).and_then(|i| i.as_const()) { + Some(n) if n < 62 => a().mul(Range::exact(1i64 << n)), + _ => Range::ANY, + }, + Op::IntRight => { + let source = a(); + match (op.inputs.get(1).and_then(|i| i.as_const()), source.low >= 0) { + (Some(n), true) if n < 64 => Range::new(source.low >> n, source.high >> n), + _ => Range::unsigned(op.size), + } + } + Op::IntSRight => match op.inputs.get(1).and_then(|i| i.as_const()) { + Some(n) if n < 64 => { + let source = a(); + Range::new(source.low >> n, source.high >> n) + } + _ => Range::ANY, + }, + // Widening says exactly how wide the result is. + Op::IntZExt => { + let source = a(); + let from = op.inputs.first().map(|i| i.size()).unwrap_or(op.size); + if source.low >= 0 { + Range::new(0, source.high.max(0)) + } else { + Range::unsigned(from) + } + } + // The source of a sign extension is usually a narrow read, whose + // interval is unsigned; reading it as signed is the whole point of + // the operation. + Op::IntSExt => { + let from = op.inputs.first().map(|i| i.size()).unwrap_or(op.size); + a().reinterpret_signed(from) + } + Op::SubPiece => match op.inputs.get(1).and_then(|i| i.as_const()) { + Some(0) => { + let source = a(); + let width = Range::unsigned(op.size); + if source.low >= 0 && width.contains(source.high) { + Range::new(0, source.high) + } else { + width + } + } + _ => Range::unsigned(op.size), + }, + Op::Load => Range::unsigned(op.size), + // A truth is a truth. + Op::IntEqual + | Op::IntNotEqual + | Op::IntLess + | Op::IntLessEqual + | Op::IntSLess + | Op::IntSLessEqual + | Op::IntCarry + | Op::IntSCarry + | Op::IntSBorrow + | Op::BoolAnd + | Op::BoolOr + | Op::BoolXor + | Op::BoolNot + | Op::FloatEqual + | Op::FloatNotEqual + | Op::FloatLess + | Op::FloatLessEqual + | Op::FloatNan => Range::new(0, 1), + Op::PopCount => Range::new(0, 64), + Op::LzCount => Range::new(0, 64), + // Division cannot make a number bigger than what it divides, once + // the sign is accounted for. + Op::IntDiv | Op::IntRem => { + let source = a(); + if source.low >= 0 { + Range::new(0, source.high) + } else { + Range::ANY + } + } + _ => Range::ANY, + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_mask_bounds_what_it_masks() { + assert_eq!(Range::new(0, 0xff).high, 255); + assert!(Range::new(0, 0xff).contains(0)); + assert!(Range::new(0, 0xff).contains(255)); + assert!(!Range::new(0, 0xff).contains(256)); + } + + #[test] + fn arithmetic_that_would_overflow_gives_up_rather_than_wrapping() { + let huge = Range::new(i64::MAX - 1, i64::MAX); + assert!(huge.add(Range::exact(2)).is_any()); + assert!(huge.mul(Range::exact(2)).is_any()); + } + + #[test] + fn joining_keeps_both() { + let a = Range::new(0, 10); + let b = Range::new(-5, 3); + let joined = a.join(b); + assert_eq!(joined, Range::new(-5, 10)); + assert!(joined.contains(-5) && joined.contains(10)); + } + + #[test] + fn a_constant_is_read_at_its_own_width() { + let known: BTreeMap = BTreeMap::new(); + // 0xff as one byte is minus one, not two hundred and fifty five. + assert_eq!(of(&Operand::Const(0xff, 1), &known), Range::exact(-1)); + assert_eq!(of(&Operand::Const(0xff, 8), &known), Range::exact(255)); + } +} diff --git a/crates/e5r-ir/src/shape.rs b/crates/e5r-ir/src/shape.rs new file mode 100644 index 0000000..7ebd61b --- /dev/null +++ b/crates/e5r-ir/src/shape.rs @@ -0,0 +1,245 @@ +//! What a pointer points at, inferred from how it is used. +//! +//! A function that takes a pointer and reads four bytes at offset zero, four +//! at offset four and eight at offset eight is being handed a structure with +//! three fields, and the code says so whether or not the binary carries debug +//! information. This pass collects those accesses and reports them; naming the +//! fields is a person's job, and inventing names for them would be a claim. +//! +//! It reports what it saw, not what it concluded. An access through a pointer +//! that was itself loaded from memory is not attributed to the original, and a +//! stride that two accesses disagree about is recorded as two strides rather +//! than averaged into one. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::op::{Op, Space}; +use crate::ssa::{Location, Operand, SsaFunction, SsaKind, Value}; + +/// One access through a pointer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Access { + /// Byte offset from the pointer. + pub offset: i64, + /// How many bytes were touched. + pub size: u8, + /// True when the access wrote. + pub write: bool, +} + +/// What was seen through one pointer. +#[derive(Debug, Clone, Default)] +pub struct Shape { + /// The accesses, deduplicated and in offset order. + pub accesses: BTreeSet, + /// Strides an index was scaled by, which is the size of the element when + /// the pointer walks an array. + pub strides: BTreeSet, +} + +impl Shape { + /// The fields this implies: one per distinct offset, at the widest access + /// seen there. + pub fn fields(&self) -> Vec<(i64, u8)> { + let mut widest: BTreeMap = BTreeMap::new(); + for a in &self.accesses { + let entry = widest.entry(a.offset).or_insert(a.size); + *entry = (*entry).max(a.size); + } + widest.into_iter().collect() + } + + /// The size the accesses imply, which is the stride when there is one and + /// otherwise the end of the last field. + pub fn size(&self) -> Option { + if let Some(stride) = self.strides.iter().next_back() { + return Some(*stride); + } + let (offset, size) = self.fields().into_iter().next_back()?; + // Checked: the offset is an i64 read out of an address computation, so + // one near the top of the range plus a field width leaves the type. + (offset >= 0) + .then_some(offset as u64)? + .checked_add(size as u64) + } +} + +/// Everything seen through each pointer that arrived from outside. +pub fn shapes(f: &SsaFunction) -> BTreeMap { + let mut out: BTreeMap = BTreeMap::new(); + let defs = f.definitions(); + + for b in f.blocks.values() { + for op in &b.ops { + let SsaKind::Op(o) = op.kind else { continue }; + let (address, size, write) = match o { + Op::Load => (op.inputs.first(), op.size, false), + Op::Store => ( + op.inputs.first(), + op.inputs.get(1).map(|i| i.size()).unwrap_or(0), + true, + ), + _ => continue, + }; + let Some(address) = address else { continue }; + let Some(walk) = base_of(f, &defs, address, 0) else { + continue; + }; + // Only pointers that arrived from outside: anything computed here + // is already described by whatever produced it. + let Some(location) = walk.base else { continue }; + let shape = out.entry(location).or_default(); + shape.accesses.insert(Access { + offset: walk.offset, + size, + write, + }); + shape.strides.extend(walk.strides); + } + } + out +} + +/// What an address expression resolves to. +struct Walk { + /// The incoming value it is measured from, when there is one. + base: Option, + /// The constant part. + offset: i64, + /// Scales an index was multiplied by on the way. + strides: Vec, +} + +/// Unwind an address into a base, a constant offset and any strides. +fn base_of( + f: &SsaFunction, + defs: &BTreeMap, + operand: &Operand, + depth: u32, +) -> Option { + if depth > 16 { + return None; + } + match operand { + Operand::Undefined(l) if l.space == Space::Register => Some(Walk { + base: Some(*l), + offset: 0, + strides: Vec::new(), + }), + Operand::Undefined(_) | Operand::Const(..) => None, + Operand::Value(v) => { + let (block, index) = defs.get(v)?; + let op = f.blocks.get(block)?.ops.get(*index)?; + // A pointer the loop advances arrives as a phi: one input is where + // it started and the other is itself plus a step. The base is the + // start, and the step is the size of what it walks. + if op.kind == SsaKind::Phi { + let mut base: Option = None; + let mut strides: Vec = Vec::new(); + for input in &op.inputs { + if let Some(step) = step_around(f, defs, input, *v) { + strides.push(step); + continue; + } + let w = base_of(f, defs, input, depth + 1)?; + match &base { + // Two different starting points is two pointers, and + // saying which was accessed would be a guess. + Some(existing) if existing.base != w.base => return None, + Some(_) => {} + None => base = Some(w), + } + } + let mut walk = base?; + walk.strides.extend(strides); + return Some(walk); + } + let SsaKind::Op(o) = op.kind else { return None }; + match o { + Op::Copy | Op::IntZExt | Op::IntSExt => { + base_of(f, defs, op.inputs.first()?, depth + 1) + } + Op::IntAdd | Op::IntSub => { + let (a, b) = (op.inputs.first()?, op.inputs.get(1)?); + let sign = if o == Op::IntSub { -1 } else { 1 }; + // One side is the pointer, the other the displacement. + if let Some(k) = b.as_const() { + let mut walk = base_of(f, defs, a, depth + 1)?; + walk.offset += sign * k as i64; + return Some(walk); + } + if let Some(k) = a.as_const() { + let mut walk = base_of(f, defs, b, depth + 1)?; + walk.offset += k as i64; + return Some(walk); + } + // Neither is constant: one side is the base and the other + // a scaled index, whose stride says how big an element is. + let left = base_of(f, defs, a, depth + 1); + let right = base_of(f, defs, b, depth + 1); + let strides = stride_of(f, defs, a, 0) + .into_iter() + .chain(stride_of(f, defs, b, 0)) + .collect::>(); + match (left, right) { + (Some(mut w), None) | (None, Some(mut w)) => { + w.strides.extend(strides); + Some(w) + } + _ => None, + } + } + _ => None, + } + } + } +} + +/// The constant a value adds to `phi` on its way back round, when it is the +/// phi's own increment rather than a separate pointer. +fn step_around( + f: &SsaFunction, + defs: &BTreeMap, + operand: &Operand, + phi: Value, +) -> Option { + let Operand::Value(v) = operand else { + return None; + }; + if *v == phi { + return Some(0); + } + let (block, index) = defs.get(v)?; + let op = f.blocks.get(block)?.ops.get(*index)?; + let SsaKind::Op(Op::IntAdd) = op.kind else { + return None; + }; + let (a, b) = (op.inputs.first()?, op.inputs.get(1)?); + let step = b.as_const().or_else(|| a.as_const())?; + let other = if b.as_const().is_some() { a } else { b }; + (other.as_value() == Some(phi)).then_some(step) +} + +/// The scale an index was multiplied by, when it was. +fn stride_of( + f: &SsaFunction, + defs: &BTreeMap, + operand: &Operand, + depth: u32, +) -> Option { + if depth > 8 { + return None; + } + let Operand::Value(v) = operand else { + return None; + }; + let (block, index) = defs.get(v)?; + let op = f.blocks.get(block)?.ops.get(*index)?; + let SsaKind::Op(o) = op.kind else { return None }; + match o { + Op::IntLeft => op.inputs.get(1)?.as_const().map(|n| 1u64 << n.min(31)), + Op::IntMul => op.inputs.get(1)?.as_const(), + Op::Copy | Op::IntZExt | Op::IntSExt => stride_of(f, defs, op.inputs.first()?, depth + 1), + _ => None, + } +} diff --git a/crates/e5r-ir/src/ssa.rs b/crates/e5r-ir/src/ssa.rs new file mode 100644 index 0000000..b9d06be --- /dev/null +++ b/crates/e5r-ir/src/ssa.rs @@ -0,0 +1,711 @@ +//! Static single assignment form, and the dataflow that needs it. +//! +//! Every value gets one definition, so "where did this come from" is a lookup +//! rather than a search. That is what makes constant propagation, dead code +//! elimination and eventually expression rebuilding tractable. +//! +//! The complication is that machine registers overlap: `w0` is the low half of +//! `x0`, and a naive renaming would treat them as unrelated and lose the +//! dependency. Locations are therefore canonical whole registers, and a narrow +//! access is made explicit: a narrow read becomes a `SubPiece` of the whole +//! register, and a narrow write becomes a masked merge with it. After that +//! every definition covers a whole location and the renaming is ordinary. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_core::Addr; + +use crate::func::Function; +use crate::op::{IrOp, Op, Space, Varnode}; + +/// A storage location, at the granularity SSA versions. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Location { + /// Which space. + pub space: Space, + /// The canonical offset: the start of the whole register or temporary. + pub offset: u64, + /// The whole location's size. + pub size: u8, +} + +/// The canonical location a varnode touches, or `None` for constants and +/// memory, which are not versioned. +/// +/// Registers are canonicalized to eight-byte boundaries, which is how the +/// AArch64 register file is laid out and what makes `w0` and `x0` one location. +/// The one-byte flags sit above the registers and are their own locations. +pub fn location(v: Varnode) -> Option { + match v.space { + Space::Const | Space::Ram => None, + Space::Unique | Space::Stack => Some(Location { + space: v.space, + offset: v.offset, + size: v.size, + }), + Space::Register => { + // The flags are single bytes and are not part of an eight-byte + // register; anything a byte wide is treated as its own location. + if v.size == 1 { + return Some(Location { + space: Space::Register, + offset: v.offset, + size: 1, + }); + } + // An access that crosses an eight-byte boundary is not one + // location, and pretending it is would let a write to the upper + // half clobber the lower. Such an access has no canonical location + // and the builder treats it as opaque. + if v.offset % 8 + v.size as u64 > 8 { + return None; + } + Some(Location { + space: Space::Register, + offset: v.offset & !7, + size: 8, + }) + } + } +} + +impl Location { + /// The varnode naming the whole location. + pub fn whole(self) -> Varnode { + Varnode { + space: self.space, + offset: self.offset, + size: self.size, + } + } +} + +/// A versioned value: a location and which definition of it this is. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Value { + /// Which location. + pub location: Location, + /// Which definition, counting from zero at the function's entry. + pub version: u32, +} + +impl std::fmt::Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let space = match self.location.space { + Space::Register => "r", + Space::Unique => "u", + Space::Ram => "m", + Space::Const => "c", + Space::Stack => "s", + }; + write!(f, "{space}{:x}_{}", self.location.offset, self.version) + } +} + +/// One operation in SSA form. +#[derive(Debug, Clone)] +pub struct SsaOp { + /// The machine address it came from. + pub addr: Addr, + /// Which operation, or a phi. + pub kind: SsaKind, + /// Where the result goes. + pub out: Option, + /// The inputs, each either a versioned value or a constant. + pub inputs: Vec, + /// The size the operation works at, in bytes. + pub size: u8, +} + +/// A phi or an ordinary operation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SsaKind { + /// A merge of one value per incoming edge. + Phi, + /// An ordinary IR operation. + Op(Op), +} + +/// An SSA operand. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Operand { + /// A versioned value. + Value(Value), + /// A literal. + Const(u64, u8), + /// A location nothing in this function defined: an argument, or memory. + Undefined(Location), +} + +impl Operand { + /// The width of an operand in bytes. + pub fn size(self) -> u8 { + match self { + Operand::Const(_, size) => size, + Operand::Value(v) => v.location.size, + Operand::Undefined(l) => l.size, + } + } + + /// The literal, if this is one. + pub fn as_const(self) -> Option { + match self { + Operand::Const(v, _) => Some(v), + _ => None, + } + } + + /// The value, if this is one. + pub fn as_value(self) -> Option { + match self { + Operand::Value(v) => Some(v), + _ => None, + } + } +} + +impl std::fmt::Display for Operand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Operand::Value(v) => write!(f, "{v}"), + Operand::Const(v, _) => write!(f, "{v:#x}"), + Operand::Undefined(l) => write!(f, "{}_in", l.whole()), + } + } +} + +/// One block in SSA form. +#[derive(Debug, Clone, Default)] +pub struct SsaBlock { + /// The operations, phis first. + pub ops: Vec, + /// Successors, in the order the phis expect their inputs. + pub successors: Vec, + /// Predecessors, in the order the phis take their inputs. + pub predecessors: Vec, +} + +/// A function in SSA form. +#[derive(Debug, Clone)] +pub struct SsaFunction { + /// Which architecture the code came from. + pub arch: e5r_core::Arch, + /// Where it starts. + pub entry: Addr, + /// Blocks by start address. + pub blocks: BTreeMap, +} + +impl SsaFunction { + /// How many operations there are. + pub fn op_count(&self) -> usize { + self.blocks.values().map(|b| b.ops.len()).sum() + } + + /// How many phis were needed. + pub fn phi_count(&self) -> usize { + self.blocks + .values() + .flat_map(|b| b.ops.iter()) + .filter(|o| o.kind == SsaKind::Phi) + .count() + } + + /// The single definition of each value. + pub fn definitions(&self) -> BTreeMap { + let mut out = BTreeMap::new(); + for (addr, b) in &self.blocks { + for (i, op) in b.ops.iter().enumerate() { + if let Some(v) = op.out { + out.insert(v, (*addr, i)); + } + } + } + out + } +} + +/// Immediate dominators, by the iterative algorithm. +/// +/// Chosen over Lengauer-Tarjan because it is twenty lines, its correctness is +/// visible, and the functions here have hundreds of blocks rather than +/// millions. +fn dominators(f: &Function, order: &[Addr]) -> BTreeMap { + let index: BTreeMap = order.iter().enumerate().map(|(i, a)| (*a, i)).collect(); + let mut idom: BTreeMap = BTreeMap::new(); + idom.insert(f.entry, f.entry); + + let mut changed = true; + let mut rounds = 0; + while changed && rounds < 1000 { + changed = false; + rounds += 1; + for at in order.iter().skip(1) { + let Some(b) = f.blocks.get(at) else { continue }; + let mut new: Option = None; + for p in &b.predecessors { + if !idom.contains_key(p) { + continue; + } + new = Some(match new { + None => *p, + Some(cur) => intersect(&idom, &index, cur, *p), + }); + } + if let Some(n) = new { + if idom.get(at) != Some(&n) { + idom.insert(*at, n); + changed = true; + } + } + } + } + idom +} + +/// Walk up the dominator tree until two blocks meet. +fn intersect( + idom: &BTreeMap, + index: &BTreeMap, + mut a: Addr, + mut b: Addr, +) -> Addr { + let mut guard = 0; + while a != b && guard < 10_000 { + guard += 1; + let (ia, ib) = ( + index.get(&a).copied().unwrap_or(0), + index.get(&b).copied().unwrap_or(0), + ); + if ia > ib { + a = idom.get(&a).copied().unwrap_or(a); + } else { + b = idom.get(&b).copied().unwrap_or(b); + } + if idom.get(&a) == Some(&a) && idom.get(&b) == Some(&b) && a != b { + break; + } + } + a +} + +/// Dominance frontiers, which is where phis go. +fn frontiers(f: &Function, idom: &BTreeMap) -> BTreeMap> { + let mut df: BTreeMap> = BTreeMap::new(); + for (at, b) in &f.blocks { + if b.predecessors.len() < 2 { + continue; + } + for p in &b.predecessors { + let mut runner = *p; + let mut guard = 0; + while runner != idom.get(at).copied().unwrap_or(*at) && guard < 10_000 { + guard += 1; + df.entry(runner).or_default().insert(*at); + let next = idom.get(&runner).copied().unwrap_or(runner); + if next == runner { + break; + } + runner = next; + } + } + } + df +} + +/// Convert a function to SSA form. +pub fn build(f: &Function) -> SsaFunction { + let order = f.reverse_postorder(); + let idom = dominators(f, &order); + let df = frontiers(f, &idom); + + // Which locations each block defines, and which are defined anywhere. + let mut defined_in: BTreeMap> = BTreeMap::new(); + let mut all: BTreeSet = BTreeSet::new(); + for (at, b) in &f.blocks { + let set = defined_in.entry(*at).or_default(); + for op in &b.ops { + if let Some(l) = op.out.and_then(location) { + set.insert(l); + all.insert(l); + } + } + } + + // Phi placement: the iterated dominance frontier of each location's + // definition sites. + let mut phis: BTreeMap> = BTreeMap::new(); + for loc in &all { + let mut work: Vec = defined_in + .iter() + .filter(|(_, s)| s.contains(loc)) + .map(|(a, _)| *a) + .collect(); + let mut placed: BTreeSet = BTreeSet::new(); + while let Some(at) = work.pop() { + for target in df.get(&at).into_iter().flatten() { + if placed.insert(*target) { + phis.entry(*target).or_default().insert(*loc); + work.push(*target); + } + } + } + } + + // Renaming, walking the dominator tree so each block starts from the + // versions its dominator ended with. + let children = dominator_children(&idom, f.entry); + let mut counters: BTreeMap = BTreeMap::new(); + let mut out: BTreeMap = BTreeMap::new(); + let mut incoming: BTreeMap> = BTreeMap::new(); + incoming.insert(f.entry, BTreeMap::new()); + // What reaches the end of each block, for filling the phis. Not the same + // as what the block defines: a location a block never touches still has a + // value there, inherited from the block that dominates it, and a phi + // reading only the predecessor's own definitions loses it. + let mut exit_state: BTreeMap> = BTreeMap::new(); + + // Explicit stack rather than recursion. + let mut stack = vec![f.entry]; + let mut visited: BTreeSet = BTreeSet::new(); + while let Some(at) = stack.pop() { + if !visited.insert(at) { + continue; + } + let Some(block) = f.blocks.get(&at) else { + continue; + }; + let mut current = incoming.get(&at).cloned().unwrap_or_default(); + let mut ops: Vec = Vec::new(); + + // Phis first, one per location that needs one here. + for loc in phis.get(&at).into_iter().flatten() { + let version = bump(&mut counters, *loc); + let value = Value { + location: *loc, + version, + }; + ops.push(SsaOp { + addr: at, + kind: SsaKind::Phi, + out: Some(value), + // Filled in once every predecessor has been renamed. + inputs: Vec::new(), + size: loc.size, + }); + current.insert(*loc, Operand::Value(value)); + } + + for op in &block.ops { + let (converted, updates) = convert(op, ¤t, &mut counters); + ops.extend(converted); + for (l, v) in updates { + current.insert(l, v); + } + } + + out.insert( + at, + SsaBlock { + ops, + successors: block.successors.clone(), + predecessors: block.predecessors.clone(), + }, + ); + exit_state.insert(at, current.clone()); + + // Every successor inherits this block's final versions for its phis, + // and every dominator child inherits them wholesale. + for s in &block.successors { + incoming.entry(*s).or_default(); + let entry = incoming.get_mut(s).unwrap(); + for (l, v) in ¤t { + entry.entry(*l).or_insert(*v); + } + } + for c in children.get(&at).into_iter().flatten() { + incoming.insert(*c, current.clone()); + stack.push(*c); + } + // Blocks the dominator tree does not reach from here still need doing. + for s in &block.successors { + if !visited.contains(s) && f.blocks.contains_key(s) { + stack.push(*s); + } + } + } + + // Fill the phi inputs, one per predecessor in order. + let final_versions = exit_state; + for (at, b) in out.iter_mut() { + let preds = b.predecessors.clone(); + for op in b.ops.iter_mut() { + if op.kind != SsaKind::Phi { + continue; + } + let Some(v) = op.out else { continue }; + op.inputs = preds + .iter() + .map(|p| { + final_versions + .get(p) + .and_then(|m| m.get(&v.location)) + .copied() + .unwrap_or(Operand::Undefined(v.location)) + }) + .collect(); + let _ = at; + } + } + + SsaFunction { + arch: f.arch.clone(), + entry: f.entry, + blocks: out, + } +} + +fn bump(counters: &mut BTreeMap, loc: Location) -> u32 { + let c = counters.entry(loc).or_insert(0); + let v = *c; + *c += 1; + v +} + +/// The blocks each block immediately dominates. +fn dominator_children(idom: &BTreeMap, entry: Addr) -> BTreeMap> { + let mut out: BTreeMap> = BTreeMap::new(); + for (block, parent) in idom { + if *block == entry || block == parent { + continue; + } + out.entry(*parent).or_default().push(*block); + } + out +} + +/// Convert one IR operation, making narrow accesses explicit. +/// +/// Returns the operations to emit and the location versions they define. +fn convert( + op: &IrOp, + current: &BTreeMap, + counters: &mut BTreeMap, +) -> (Vec, Vec<(Location, Operand)>) { + let mut emitted = Vec::new(); + let mut updates = Vec::new(); + let mut extra_temp = 0u64; + + // Read each input, extracting from the whole location when the access is + // narrower than it. + let mut inputs = Vec::new(); + for v in op.inputs() { + inputs.push(read( + *v, + current, + counters, + &mut emitted, + &mut updates, + op.addr, + &mut extra_temp, + )); + } + + // A memory address is an input; a load's size comes from its output. + let size = op + .out + .map(|o| o.size) + .or_else(|| op.input(0).map(|i| i.size)) + .unwrap_or(8); + + match op.out.and_then(|o| location(o).map(|l| (o, l))) { + // A write that covers the whole location: an ordinary definition. + Some((o, loc)) if o.size == loc.size && o.offset == loc.offset => { + let version = bump(counters, loc); + let value = Value { + location: loc, + version, + }; + emitted.push(SsaOp { + addr: op.addr, + kind: SsaKind::Op(op.op), + out: Some(value), + inputs, + size, + }); + updates.push((loc, Operand::Value(value))); + } + // A narrow write: compute into a temporary, then merge it with the old + // whole value. Making this explicit is what keeps the dependency on the + // untouched bits visible to dataflow. + Some((o, loc)) => { + let temp_loc = Location { + space: Space::Unique, + offset: 1 << 40 | o.offset << 8 | extra_temp, + size: o.size, + }; + extra_temp += 1; + let tv = Value { + location: temp_loc, + version: bump(counters, temp_loc), + }; + emitted.push(SsaOp { + addr: op.addr, + kind: SsaKind::Op(op.op), + out: Some(tv), + inputs, + size, + }); + + let old = current + .get(&loc) + .copied() + .unwrap_or(Operand::Undefined(loc)); + let shift = (o.offset - loc.offset) * 8; + let field = if o.size as u64 * 8 >= 64 { + u64::MAX + } else { + ((1u64 << (o.size as u32 * 8)) - 1) << shift + }; + + // old & !field + let cleared_loc = Location { + space: Space::Unique, + offset: 1 << 41 | o.offset << 8 | extra_temp, + size: loc.size, + }; + extra_temp += 1; + let cleared = Value { + location: cleared_loc, + version: bump(counters, cleared_loc), + }; + emitted.push(SsaOp { + addr: op.addr, + kind: SsaKind::Op(Op::IntAnd), + out: Some(cleared), + inputs: vec![old, Operand::Const(!field, loc.size)], + size: loc.size, + }); + + // widen(temp) << shift + let widened_loc = Location { + space: Space::Unique, + offset: 1 << 42 | o.offset << 8 | extra_temp, + size: loc.size, + }; + extra_temp += 1; + let widened = Value { + location: widened_loc, + version: bump(counters, widened_loc), + }; + emitted.push(SsaOp { + addr: op.addr, + kind: SsaKind::Op(Op::IntZExt), + out: Some(widened), + inputs: vec![Operand::Value(tv)], + size: loc.size, + }); + let placed = if shift == 0 { + widened + } else { + let placed_loc = Location { + space: Space::Unique, + offset: 1 << 43 | o.offset << 8 | extra_temp, + size: loc.size, + }; + let p = Value { + location: placed_loc, + version: bump(counters, placed_loc), + }; + emitted.push(SsaOp { + addr: op.addr, + kind: SsaKind::Op(Op::IntLeft), + out: Some(p), + inputs: vec![Operand::Value(widened), Operand::Const(shift, 1)], + size: loc.size, + }); + p + }; + // extra_temp is not read again in this arm; the merge below needs + // no further temporaries. + let _ = extra_temp; + + let version = bump(counters, loc); + let merged = Value { + location: loc, + version, + }; + emitted.push(SsaOp { + addr: op.addr, + kind: SsaKind::Op(Op::IntOr), + out: Some(merged), + inputs: vec![Operand::Value(cleared), Operand::Value(placed)], + size: loc.size, + }); + updates.push((loc, Operand::Value(merged))); + } + // No output, or an output in memory: emitted as it is. + None => { + emitted.push(SsaOp { + addr: op.addr, + kind: SsaKind::Op(op.op), + out: None, + inputs, + size, + }); + } + } + + (emitted, updates) +} + +/// Read a varnode, extracting from its whole location when it is narrower. +#[allow(clippy::too_many_arguments)] +fn read( + v: Varnode, + current: &BTreeMap, + counters: &mut BTreeMap, + emitted: &mut Vec, + _updates: &mut Vec<(Location, Operand)>, + addr: Addr, + extra_temp: &mut u64, +) -> Operand { + if v.space == Space::Const { + return Operand::Const(v.offset, v.size); + } + let Some(loc) = location(v) else { + // Memory: not versioned, so it reads as undefined storage. + return Operand::Undefined(Location { + space: Space::Ram, + offset: v.offset, + size: v.size, + }); + }; + let whole = current + .get(&loc) + .copied() + .unwrap_or(Operand::Undefined(loc)); + if v.size == loc.size && v.offset == loc.offset { + return whole; + } + // Narrower than the location: extract the bytes. + let shift = v.offset - loc.offset; + let out_loc = Location { + space: Space::Unique, + offset: 1 << 44 | v.offset << 8 | *extra_temp, + size: v.size, + }; + *extra_temp += 1; + let out = Value { + location: out_loc, + version: bump(counters, out_loc), + }; + emitted.push(SsaOp { + addr, + kind: SsaKind::Op(Op::SubPiece), + out: Some(out), + inputs: vec![whole, Operand::Const(shift, 1)], + size: v.size, + }); + Operand::Value(out) +} diff --git a/crates/e5r-ir/src/stack.rs b/crates/e5r-ir/src/stack.rs new file mode 100644 index 0000000..9f72989 --- /dev/null +++ b/crates/e5r-ir/src/stack.rs @@ -0,0 +1,244 @@ +//! Turning stack slots into variables. +//! +//! A compiler spills a local to the stack and reloads it; lifting that +//! faithfully gives a store and a load through an address that is the stack +//! pointer plus a constant. Left alone, every local in the output reads as +//! `*(uint64_t *)(sp - 16 + 12)`, which is what the machine does and not what +//! anyone wrote. +//! +//! This pass finds the accesses whose address is a known offset from the stack +//! pointer on entry and rewrites them into reads and writes of a slot, which +//! SSA construction then treats like any other location. The result is a +//! variable. +//! +//! It refuses whenever it cannot prove the rewrite is safe. If any stack +//! address is used as a value — passed to a call, stored somewhere, computed +//! into — then something may read the slot through a pointer and promoting it +//! would lose that write. If two slots overlap without being identical, the +//! same. A refusal costs readability; a wrong promotion costs correctness. + +use std::collections::{BTreeMap, BTreeSet}; + +use e5r_core::Addr; + +use crate::abi; +use crate::func::Function; +use crate::op::{IrOp, Op, Space, Varnode}; + +/// What a pass changed. +#[derive(Debug, Clone, Copy, Default)] +pub struct Promoted { + /// Accesses rewritten into slot reads and writes. + pub accesses: usize, + /// Distinct slots created. + pub slots: usize, +} + +/// A value's offset from the stack pointer on entry, when it has one. +type Offsets = BTreeMap; + +/// Promote the stack slots this function only ever reads and writes directly. +pub fn promote(f: &mut Function) -> Promoted { + let sp = Varnode::register(abi::of(&f.arch).stack_pointer, 8); + let order = f.reverse_postorder(); + + // The stack offset each block starts with, which a function that pushes in + // one block and pops in another needs. + let mut entry_state: BTreeMap = BTreeMap::new(); + let mut initial = Offsets::new(); + initial.insert(sp, 0); + entry_state.insert(f.entry, initial); + + let mut slots: BTreeMap<(i64, u8), usize> = BTreeMap::new(); + let mut escaped = false; + + // Two sweeps: the first settles the offsets at each block's entry, the + // second records what is accessible and whether anything escapes. + for _ in 0..4 { + for at in &order { + let Some(block) = f.blocks.get(at) else { + continue; + }; + let mut state = entry_state.get(at).cloned().unwrap_or_default(); + for op in &block.ops { + step(op, sp, &mut state, &mut slots, &mut escaped); + } + for s in &block.successors { + match entry_state.get(s) { + None => { + entry_state.insert(*s, state.clone()); + } + Some(existing) => { + // Only what both paths agree on survives the merge. + let merged: Offsets = existing + .iter() + .filter(|(k, v)| state.get(*k) == Some(*v)) + .map(|(k, v)| (*k, *v)) + .collect(); + if merged.len() != existing.len() { + entry_state.insert(*s, merged); + } + } + } + } + } + } + + if escaped || slots.is_empty() { + return Promoted::default(); + } + // Overlapping slots of different widths alias, and this pass has no way to + // say that, so none of them are promoted. + let keys: Vec<(i64, u8)> = slots.keys().copied().collect(); + let overlapping: BTreeSet<(i64, u8)> = keys + .iter() + .filter(|a| { + keys.iter() + .any(|b| *b != **a && a.0 < b.0 + b.1 as i64 && b.0 < a.0 + a.1 as i64) + }) + .copied() + .collect(); + + // Rewrite. + let mut changed = Promoted::default(); + let mut used: BTreeSet<(i64, u8)> = BTreeSet::new(); + for at in &order { + let mut state = entry_state.get(at).cloned().unwrap_or_default(); + let Some(block) = f.blocks.get_mut(at) else { + continue; + }; + for op in &mut block.ops { + let mut ignore = false; + let before = state.clone(); + step(op, sp, &mut state, &mut BTreeMap::new(), &mut ignore); + + match op.op { + Op::Load => { + let (Some(addr), Some(out)) = (op.input(0), op.out) else { + continue; + }; + let Some(offset) = before.get(&addr).copied() else { + continue; + }; + let key = (offset, out.size); + if overlapping.contains(&key) { + continue; + } + *op = IrOp::new(op.addr, Op::Copy, Some(out)).with(slot(key)); + used.insert(key); + changed.accesses += 1; + } + Op::Store => { + let (Some(addr), Some(value)) = (op.input(0), op.input(1)) else { + continue; + }; + let Some(offset) = before.get(&addr).copied() else { + continue; + }; + let key = (offset, value.size); + if overlapping.contains(&key) { + continue; + } + *op = IrOp::new(op.addr, Op::Copy, Some(slot(key))).with(value); + used.insert(key); + changed.accesses += 1; + } + _ => {} + } + } + } + changed.slots = used.len(); + changed +} + +/// The varnode a slot lives in. Negative offsets are the usual case, so the +/// offset is stored as its two's complement pattern. +fn slot((offset, size): (i64, u8)) -> Varnode { + Varnode { + space: Space::Stack, + offset: offset as u64, + size, + } +} + +/// Follow one operation's effect on what is known about the stack pointer. +fn step( + op: &IrOp, + sp: Varnode, + state: &mut Offsets, + slots: &mut BTreeMap<(i64, u8), usize>, + escaped: &mut bool, +) { + // An address used as anything but an address means the slot could be read + // through a pointer, and nothing can be promoted. + let tracked = |v: Varnode, state: &Offsets| state.contains_key(&v); + match op.op { + Op::Load => { + if let (Some(addr), Some(out)) = (op.input(0), op.out) { + if let Some(offset) = state.get(&addr).copied() { + *slots.entry((offset, out.size)).or_default() += 1; + } + // A loaded value is not a stack address any more. + state.remove(&out); + } + } + Op::Store => { + if let (Some(addr), Some(value)) = (op.input(0), op.input(1)) { + if let Some(offset) = state.get(&addr).copied() { + *slots.entry((offset, value.size)).or_default() += 1; + } + // Storing an address puts it somewhere this pass cannot see. + if tracked(value, state) { + *escaped = true; + } + } + } + Op::Copy => { + if let (Some(src), Some(out)) = (op.input(0), op.out) { + match state.get(&src).copied() { + Some(offset) if out.size == 8 => { + state.insert(out, offset); + } + _ => { + state.remove(&out); + } + } + } + } + Op::IntAdd | Op::IntSub => { + let (Some(a), Some(b), Some(out)) = (op.input(0), op.input(1), op.out) else { + return; + }; + let base = state.get(&a).copied(); + match (base, b.value()) { + (Some(offset), Some(k)) if out.size == 8 => { + let delta = k as i64; + let next = if op.op == Op::IntAdd { + offset.wrapping_add(delta) + } else { + offset.wrapping_sub(delta) + }; + state.insert(out, next); + } + _ => { + // A stack address combined with something unknown is an + // address this pass cannot follow. + if base.is_some() || tracked(b, state) { + *escaped = true; + } + state.remove(&out); + } + } + } + _ => { + for i in op.inputs() { + if tracked(*i, state) && *i != sp { + *escaped = true; + } + } + if let Some(out) = op.out { + state.remove(&out); + } + } + } +} diff --git a/crates/e5r-ir/tests/arm32.rs b/crates/e5r-ir/tests/arm32.rs new file mode 100644 index 0000000..a1bc83d --- /dev/null +++ b/crates/e5r-ir/tests/arm32.rs @@ -0,0 +1,440 @@ +//! The ARM32 lifter measured against a processor. +//! +//! This host is aarch64, which does not run A32 code, so the ARM side runs +//! under qemu -- the way the rest of the lifter's hardware oracle already +//! works. Nothing below is an assertion about what the lifter ought to do: +//! every expected value came out of a processor. +//! +//! The cases are the ones where this architecture differs from every other the +//! lifter covers: the barrel shifter on the second operand and the carry it +//! leaves behind, predication as part of the mnemonic, and the load and store +//! multiple forms that move a frame in one instruction. + +use std::process::Command; + +use e5r_core::{Addr, Arch}; +use e5r_ir::interp::{Step, step}; +use e5r_ir::{Machine, lift}; + +/// Where the interpreted machine's stack goes. +const STACK: u64 = 0x4000_0000; + +/// One case: a name, and assembly that leaves its answer in `r0`. +/// +/// `sp` is the harness's, so a case that moves it has to put it back. The +/// push and pop cases do exactly that, which is the point of testing them. +const CASES: &[(&str, &str)] = &[ + // The barrel shifter on the second operand, which no other architecture + // the lifter covers has. + ( + "shifted_add", + "mov r1, #5\n mov r2, #3\n add r0, r1, r2, lsl #4\n", + ), + ( + "shifted_sub", + "mov r1, #100\n mov r2, #8\n sub r0, r1, r2, lsr #2\n", + ), + ( + "shifted_asr", + "mvn r1, #0\n mov r2, #0\n add r0, r2, r1, asr #4\n", + ), + ( + "shifted_ror", + "mov r1, #0xff\n mov r2, #0\n add r0, r2, r1, ror #4\n", + ), + // The carry the shifter leaves, which is not the carry out of the + // arithmetic and which `adc` then reads. + ( + "shifter_carry", + "mov r1, #0x80000000\n movs r2, r1, lsl #1\n mov r0, #0\n adc r0, r0, #0\n", + ), + ( + "shifter_carry_lsr", + "mov r1, #3\n movs r2, r1, lsr #1\n mov r0, #0\n adc r0, r0, #0\n", + ), + ( + "rrx_through_carry", + "mov r1, #1\n movs r2, r1, lsr #1\n mov r0, #0\n rrx r0, r0\n", + ), + // Predication, which is part of the mnemonic and applies to almost + // everything. + ( + "predicated_taken", + "mov r0, #1\n cmp r0, #1\n moveq r0, #0x55\n", + ), + ( + "predicated_skipped", + "mov r0, #1\n cmp r0, #2\n moveq r0, #0x55\n", + ), + ( + "predicated_add", + "mov r0, #10\n mov r1, #3\n cmp r1, #3\n addeq r0, r0, #7\n addne r0, r0, #100\n", + ), + ( + "predicated_signed", + "mov r0, #0\n mov r1, #1\n cmn r1, #2\n movlt r0, #1\n movge r0, #2\n", + ), + // The flags themselves, at a 32-bit width. + ( + "carry_out", + "mvn r1, #0\n adds r1, r1, #1\n mov r0, #0\n adc r0, r0, #0\n", + ), + ( + "borrow_out", + "mov r1, #1\n subs r1, r1, #2\n mov r0, #0\n adc r0, r0, #0\n", + ), + ( + "overflow_set", + "mov r1, #0x80000000\n subs r1, r1, #1\n mov r0, #0\n movvs r0, #1\n", + ), + // push and pop, which move several registers and the stack in one go. + ( + "push_pop", + "mov r4, #0x11\n mov r5, #0x22\n push {r4, r5}\n mov r4, #0\n mov r5, #0\n pop {r4, r5}\n add r0, r4, r5\n", + ), + ( + "push_order", + "mov r4, #0x10\n mov r5, #0x20\n push {r4, r5}\n ldr r0, [sp]\n add sp, sp, #8\n", + ), + // Multiplies, including the long forms that write two registers. + ("mul_wraps", "mov r1, #0x10000\n mul r0, r1, r1\n"), + ("umull_high", "mov r1, #0x10000\n umull r2, r0, r1, r1\n"), + ( + "smull_negative", + "mvn r1, #0\n mov r2, #2\n smull r3, r0, r1, r2\n", + ), + ( + "mla_accumulates", + "mov r1, #3\n mov r2, #4\n mov r3, #5\n mla r0, r1, r2, r3\n", + ), + // Extensions and the bit operations. + ("uxtb_keeps_low", "mvn r1, #0\n uxtb r0, r1\n"), + ("sxtb_extends", "mov r1, #0xff\n sxtb r0, r1\n"), + ("clz_counts", "mov r1, #0x8000\n clz r0, r1\n"), + ("rev_swaps", "ldr r1, =0x11223344\n rev r0, r1\n"), + ("rbit_reverses", "mov r1, #1\n rbit r0, r1\n"), + ( + "ubfx_extracts", + "ldr r1, =0x12345678\n ubfx r0, r1, #4, #8\n", + ), + ( + "bfi_inserts", + "ldr r0, =0x12345678\n mov r1, #0xf\n bfi r0, r1, #8, #4\n", + ), + // Loads and stores, including the writeback the addressing modes have. + ( + "store_load", + "sub sp, sp, #16\n ldr r1, =0xdeadbeef\n str r1, [sp, #4]\n ldr r0, [sp, #4]\n add sp, sp, #16\n", + ), + ( + "post_index", + "sub sp, sp, #16\n mov r1, #7\n mov r2, sp\n str r1, [r2], #4\n sub r0, r2, sp\n add sp, sp, #16\n", + ), + ( + "byte_load_zero", + "sub sp, sp, #16\n mvn r1, #0\n str r1, [sp]\n ldrb r0, [sp]\n add sp, sp, #16\n", + ), + ( + "byte_load_signed", + "sub sp, sp, #16\n mvn r1, #0\n str r1, [sp]\n ldrsb r0, [sp]\n add sp, sp, #16\n", + ), + ( + "scaled_index", + "sub sp, sp, #32\n mov r1, #0x99\n mov r2, #2\n str r1, [sp, r2, lsl #2]\n ldr r0, [sp, #8]\n add sp, sp, #32\n", + ), +]; + +/// Thumb-specific cases, which A32 cannot express. +/// +/// The narrow encodings write back into the first source and name it once, the +/// wide ones carry a `.w`, and an IT block makes the instructions after it +/// conditional with nothing in their own halfwords saying so -- the decoder +/// has to carry that state from one to the next, and this is what says it does. +const THUMB_CASES: &[(&str, &str)] = &[ + ( + "two_operand_add", + "movs r0, #10\n movs r1, #7\n adds r0, r1\n", + ), + ( + "two_operand_sub", + "movs r0, #10\n movs r1, #7\n subs r0, r1\n", + ), + ( + "two_operand_shift", + "movs r0, #1\n movs r1, #4\n lsls r0, r1\n", + ), + ( + "two_operand_ror", + "ldr r0, =0x12345678\n movs r1, #4\n rors r0, r1\n", + ), + ("wide_add", "movs r1, #5\n add.w r0, r1, #0x1000\n"), + ( + "wide_load", + "sub sp, #16\n ldr r1, =0xcafe\n str.w r1, [sp, #4]\n ldr.w r0, [sp, #4]\n add sp, #16\n", + ), + // An IT block: the instructions after it are conditional and their own + // halfwords do not say so. + ( + "it_taken", + "movs r0, #1\n cmp r0, #1\n itt eq\n moveq r0, #0x33\n addeq r0, #1\n", + ), + ( + "it_skipped", + "movs r0, #1\n cmp r0, #2\n itt eq\n moveq r0, #0x33\n addeq r0, #1\n", + ), + ( + "it_else", + "movs r0, #1\n cmp r0, #2\n ite eq\n moveq r0, #0x44\n movne r0, #0x55\n", + ), + ( + "it_four", + "movs r0, #0\n cmp r0, #0\n itttt eq\n addeq r0, #1\n addeq r0, #2\n addeq r0, #4\n addeq r0, #8\n", + ), + // The epilogue a Thumb function writes when it cannot say `pop {.., pc}`: + // it reads the return address off the stack straight into the program + // counter. The low bit of what it loads is the instruction set to switch + // to and is not part of the address. + ( + "ldr_pc_returns", + "movs r0, #0x11\n ldr r1, =1f + 1\n push {r1}\n ldr pc, [sp], #4\n movs r0, #0x99\n b 2f\n1:\n movs r0, #0x42\n2:\n", + ), + // cbz and cbnz, which exist only in Thumb. + ( + "cbz_taken", + "movs r0, #0\n cbz r0, 1f\n movs r0, #0x99\n1:\n", + ), + ( + "cbnz_taken", + "movs r0, #7\n cbnz r0, 1f\n movs r0, #0x99\n1:\n", + ), +]; + +fn tool(name: &str) -> Option { + Command::new(name) + .arg("--version") + .output() + .ok() + .filter(|o| o.status.success()) + .map(|_| name.to_string()) +} + +/// Run every case on a processor and hand back the `r0` each one leaves. +fn on_hardware(cases: &[(&str, &str)], thumb: bool) -> Option> { + let cc = tool("clang")?; + let qemu = tool("qemu-arm").or_else(|| tool("qemu-arm-static"))?; + // A test runs with its crate as the working directory, not the workspace. + let lld = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build/ld"); + if !lld.join("ld.lld").exists() { + return None; // the corpus has not been built + } + // The two instruction sets are two tests and run at once, so the + // directory is named for the set as well as the process. + let set = if thumb { "t32" } else { "a32" }; + let dir = std::env::temp_dir().join(format!("e5r-arm32-{set}-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("m.s"); + let bin = dir.join("m"); + + // Written as assembly rather than C with inline assembly: this host has no + // ARM libc headers, and the cases are assembly anyway. The results go out + // through a raw `write` and the program exits with a raw `_exit`, both + // through `svc #0` with the number in r7, which is how a 32-bit ARM + // process reaches the kernel. + let mut body = String::from(if thumb { + ".text\n.thumb\n.syntax unified\n.globl _start\n.thumb_func\n_start:\n" + } else { + ".text\n.arm\n.globl _start\n_start:\n" + }); + for (_, asm) in cases { + body.push_str(asm); + body.push_str(" ldr r4, =out\n str r0, [r4]\n"); + body.push_str(" mov r7, #4\n mov r0, #1\n mov r1, r4\n mov r2, #4\n svc #0\n"); + } + body.push_str(" mov r7, #1\n mov r0, #0\n svc #0\n"); + // One literal pool, after the last instruction and before the data. Not + // one per case: `.ltorg` emits the constants where it stands, and a pool + // in the middle of the text is executed as instructions on the way past. + // The whole program is well inside the 4 KB a pc-relative load reaches. + body.push_str(" .ltorg\n"); + body.push_str(".data\nout: .word 0\n"); + std::fs::write(&src, body).ok()?; + + let built = Command::new(&cc) + .args([ + "--target=arm-unknown-linux-gnueabi", + "-march=armv7-a", + "-static", + "-nostdlib", + "-ffreestanding", + ]) + .arg(format!("-B{}", lld.display())) + .arg("-fuse-ld=lld") + .arg("-o") + .arg(&bin) + .arg(&src) + .output() + .ok()?; + if !built.status.success() { + return None; + } + let ran = Command::new(&qemu).arg(&bin).output().ok()?; + let _ = std::fs::remove_dir_all(&dir); + let raw = ran.stdout; + if raw.len() != cases.len() * 4 { + return None; + } + Some( + raw.chunks_exact(4) + .map(|c| u32::from_le_bytes(c.try_into().unwrap())) + .collect(), + ) +} + +/// Assemble one case, with its literal pool, and hand back the bytes and where +/// in them the instructions stop. +fn assemble(text: &str, thumb: bool) -> Option<(Vec, usize)> { + let cc = tool("clang")?; + let set = if thumb { "t32" } else { "a32" }; + let dir = std::env::temp_dir().join(format!("e5r-arm32asm-{set}-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("a.s"); + let obj = dir.join("a.o"); + let bin = dir.join("a.bin"); + // A marker instruction after the case, so the interpreter knows where to + // stop without having to tell an instruction from a literal. + let head = if thumb { + ".text\n.thumb\n.syntax unified\n" + } else { + ".text\n.arm\n" + }; + std::fs::write(&src, format!("{head}{text}\n udf.w #0xdead\n .ltorg\n")).ok()?; + let ok = Command::new(&cc) + .args([ + "--target=arm-unknown-linux-gnueabi", + "-march=armv7-a", + "-c", + "-o", + ]) + .arg(&obj) + .arg(&src) + .status() + .ok()?; + if !ok.success() { + return None; + } + let cut = Command::new("llvm-objcopy-18") + .args(["-O", "binary", "--only-section=.text"]) + .arg(&obj) + .arg(&bin) + .status() + .or_else(|_| { + Command::new("llvm-objcopy") + .args(["-O", "binary", "--only-section=.text"]) + .arg(&obj) + .arg(&bin) + .status() + }) + .ok()?; + if !cut.success() { + return None; + } + let out = std::fs::read(&bin).ok()?; + let _ = std::fs::remove_dir_all(&dir); + // The marker: the wide `udf` is e7fdeafd on A32 and f7ff addead on Thumb, + // both four bytes and neither a shape a case ends with. + let want: [u8; 4] = if thumb { + [0xfd, 0xf7, 0xad, 0xae] + } else { + [0xfd, 0xea, 0xfd, 0xe7] + }; + let end = out.windows(4).position(|w| w == want)?; + Some((out, end)) +} + +/// Decode, lift and run a case's bytes, and read `r0` back. +/// +/// The image is placed where the assembler put it so a pc-relative literal +/// load reads the pool that follows the instructions. +fn interpreted(bytes: &[u8], end: usize, thumb: bool) -> Option { + // Where the assembler thinks the code is. A case that loads a label out + // of its literal pool gets the address the object file holds, which is + // measured from the start of its section, so the image goes there. + const BASE: u64 = 0; + let mut m = Machine::new(); + m.set_reg(lift::arm::sp_offset(), 4, STACK); + m.write_mem(BASE, bytes); + let mut at = 0usize; + // Thumb's IT block predicates the instructions after it, and the state has + // to be carried from one decode to the next exactly as the analysis does. + let mut it = e5r_arch::arm::ItState::default(); + while at < end { + let arch = if thumb { Arch::Thumb } else { Arch::Arm }; + let insn = if thumb { + let (i, next) = e5r_arch::arm::decode_thumb(&bytes[at..], Addr(BASE + at as u64), it)?; + it = next; + i + } else { + e5r_arch::arm::decode(&bytes[at..], Addr(BASE + at as u64))? + }; + let lifted = lift::arm::lift_mode(&insn, thumb); + assert!( + lifted.complete, + "{} is not modelled", + e5r_arch::format(&arch, &insn, false) + ); + // A case may branch over an instruction, which `cbz` exists to do, so + // a jump inside the run is followed rather than treated as the end. + // An IT block does not branch: the decoder folds its condition into + // the instructions after it, which is the state carried above. + let mut jumped = None; + for op in &lifted.ops { + match step(&mut m, op) { + Step::Next => {} + // A return inside the run is a jump: these cases have no + // caller, so the address it goes to is one of their own. + Step::Jump(to) | Step::Leave(to) => { + jumped = Some(usize::try_from(to.get().checked_sub(BASE)?).ok()?); + break; + } + _ => return None, + } + } + at = match jumped { + Some(to) => to, + None => at + insn.len as usize, + }; + } + Some(m.reg(lift::arm::gpr_offset(0), 4) as u32) +} + +#[test] +fn the_arm32_lifter_computes_what_the_processor_computes() { + check(CASES, false, "A32"); +} + +#[test] +fn the_thumb_lifter_computes_what_the_processor_computes() { + check(THUMB_CASES, true, "Thumb"); +} + +fn check(cases: &[(&str, &str)], thumb: bool, what: &str) { + let Some(expected) = on_hardware(cases, thumb) else { + return; // no clang, no qemu-arm, or the corpus has not been built + }; + let mut checked = 0; + for ((name, asm), want) in cases.iter().zip(expected) { + // Not a silent skip: `on_hardware` has already built and run a + // program with these same cases in it, so an assembler that cannot + // assemble one of them now is a broken harness reporting a pass. + let (bytes, end) = + assemble(asm, thumb).unwrap_or_else(|| panic!("{name}: did not assemble")); + let got = interpreted(&bytes, end, thumb).unwrap_or_else(|| panic!("{name}: did not run")); + assert_eq!( + got, want, + "{name}: the processor says {want:#x}, the lifter says {got:#x}" + ); + checked += 1; + } + assert_eq!(checked, cases.len()); + eprintln!("{checked} {what} case(s) agree with the processor"); +} diff --git a/crates/e5r-ir/tests/asserted_prototype.rs b/crates/e5r-ir/tests/asserted_prototype.rs new file mode 100644 index 0000000..f59cce3 --- /dev/null +++ b/crates/e5r-ir/tests/asserted_prototype.rs @@ -0,0 +1,331 @@ +//! The gate on the loop this whole path exists for. +//! +//! An analyst looks at a decompilation, asserts a declaration, and looks +//! again. Before this, the assertion was stored and ignored: only `name` fed +//! back, so `annotate type` wrote a line the engine never read and 596 of +//! DecBench's 764 functions scored zero on type recovery because there was no +//! way to tell the tool anything about a type. +//! +//! The emitter is not reachable from this crate, so the gate is set where it +//! can be: assert a declaration, run prototype recovery, and check that the +//! resulting `Prototype` has the parameter count, widths, storage and return +//! type the declaration named, with the strength saying a person said so. +//! +//! Functions here are built by hand rather than lifted. What is under test is +//! the merge of an assertion with a recovery, and a hand-built function makes +//! the code's own answer exact instead of whatever a compiler happened to +//! emit. + +use std::collections::BTreeMap; + +use e5r_core::provenance::Strength; +use e5r_core::{Addr, Arch}; +use e5r_ir::abi::{self, Abi}; +use e5r_ir::op::{Op, Space}; +use e5r_ir::proto::{self, Asserted, Conflict, Storage}; +use e5r_ir::ssa::{Location, Operand, SsaBlock, SsaFunction, SsaKind, SsaOp, Value}; + +const ENTRY: Addr = Addr(0x1000); + +fn reg(offset: u64) -> Location { + Location { + space: Space::Register, + offset, + size: 8, + } +} + +/// A function that reads `reads` argument registers and leaves a computed +/// value in the first result register. +fn function(abi: &Abi, reads: usize, leaves_result: bool) -> SsaFunction { + let result = reg(abi.results[0]); + let mut ops = Vec::new(); + for n in 0..reads { + let version = n as u32 + 1; + let source = Operand::Undefined(reg(abi.integer_arguments[n])); + let inputs = if n == 0 { + vec![source] + } else { + vec![ + Operand::Value(Value { + location: result, + version: version - 1, + }), + source, + ] + }; + ops.push(SsaOp { + addr: ENTRY, + kind: SsaKind::Op(if n == 0 { Op::Copy } else { Op::IntAdd }), + out: Some(Value { + location: result, + version, + }), + inputs, + size: 8, + }); + } + if !leaves_result { + // Drop what was computed: a function with no result writes the + // register on the way through and leaves nothing that reaches a + // return. + ops.clear(); + } + ops.push(SsaOp { + addr: ENTRY, + kind: SsaKind::Op(Op::Return), + out: None, + inputs: Vec::new(), + size: 8, + }); + let mut blocks = BTreeMap::new(); + blocks.insert( + ENTRY, + SsaBlock { + ops, + successors: Vec::new(), + predecessors: Vec::new(), + }, + ); + SsaFunction { + arch: Arch::X86_64, + entry: ENTRY, + blocks, + } +} + +#[test] +fn an_asserted_declaration_decides_the_parameters_and_says_who_decided() { + let abi = abi::of(&Arch::X86_64); + // The code reads four argument registers, which is what recovery sees. + let f = function(&abi, 4, true); + let machine = proto::recover(&f, &abi); + assert_eq!(machine.integer_arguments, 4); + assert_eq!(machine.origin.arguments, Strength::Inferred); + assert!(machine.parameters.is_empty()); + + let asserted = Asserted::parse( + "int arith8(unsigned char a, unsigned char b)", + &Arch::X86_64, + ) + .expect("a declaration an analyst would type"); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + + // The count, the widths, the storage and the return type are the ones the + // declaration named. + assert_eq!(p.name.as_deref(), Some("arith8")); + assert_eq!(p.parameters.len(), 2); + assert_eq!(p.integer_arguments, 2); + assert_eq!(p.float_arguments, 0); + assert!(p.stack_arguments.is_empty()); + for (n, param) in p.parameters.iter().enumerate() { + assert_eq!(param.size, 1, "an unsigned char is one byte"); + assert_eq!(param.storage, vec![Storage::Integer(n)]); + assert_eq!(param.strength, Strength::Asserted); + } + assert_eq!(p.parameters[0].name.as_deref(), Some("a")); + assert_eq!(p.parameters[1].name.as_deref(), Some("b")); + assert_eq!(p.returns, Some(abi.results[0])); + assert!(!p.returns_float); + assert_eq!(asserted.types.size_of(p.return_type.unwrap()), Some(4)); + + // And the output can say that a person is why it changed. + assert_eq!(p.origin.arguments, Strength::Asserted); + assert_eq!(p.origin.returns, Strength::Asserted); + assert!(p.is_asserted()); + assert!(Strength::Asserted > Strength::Proven); +} + +#[test] +fn a_declaration_that_contradicts_the_machine_wins_and_the_reads_are_kept() { + let abi = abi::of(&Arch::X86_64); + let f = function(&abi, 4, true); + let asserted = Asserted::parse("int three(int a, int b, int c)", &Arch::X86_64).unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + + assert_eq!(p.integer_arguments, 3, "the declaration is honoured"); + // The fourth read is not silently dropped: it is a recorded conflict and + // the machine's own answer is still there to look at. + assert_eq!( + p.conflicts, + vec![Conflict::ExtraArgument { + register: abi.integer_arguments[3], + slot: 3, + }] + ); + let recovered = p.recovered.as_ref().expect("what the code said"); + assert_eq!(recovered.integer_arguments, 4); + assert_eq!(recovered.origin.arguments, Strength::Inferred); + assert!(format!("{}", p.conflicts[0]).contains("does not name")); +} + +#[test] +fn a_declaration_that_names_more_than_the_code_reads_says_so() { + let abi = abi::of(&Arch::X86_64); + let f = function(&abi, 1, true); + let asserted = Asserted::parse("int f(int a, int b, int c)", &Arch::X86_64).unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + assert_eq!(p.integer_arguments, 3); + assert!(p.conflicts.contains(&Conflict::UnreadArgument { index: 1 })); + assert!(p.conflicts.contains(&Conflict::UnreadArgument { index: 2 })); + assert!(!p.conflicts.contains(&Conflict::UnreadArgument { index: 0 })); +} + +#[test] +fn a_void_declaration_over_a_function_that_leaves_a_value_is_a_conflict() { + let abi = abi::of(&Arch::X86_64); + let f = function(&abi, 1, true); + let asserted = Asserted::parse("void f(int a)", &Arch::X86_64).unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + assert_eq!(p.returns, None); + assert_eq!(p.return_type, None); + assert_eq!( + p.conflicts, + vec![Conflict::ResultDiscarded { + register: abi.results[0] + }] + ); + + // And the other direction. + let f = function(&abi, 1, false); + let asserted = Asserted::parse("int f(int a)", &Arch::X86_64).unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + assert_eq!(p.returns, Some(abi.results[0])); + assert!(p.conflicts.contains(&Conflict::ResultMissing)); +} + +#[test] +fn floating_point_parameters_and_results_go_to_the_vector_registers() { + let abi = abi::of(&Arch::X86_64); + let f = function(&abi, 1, true); + let asserted = + Asserted::parse("double scale(double x, int n, float y)", &Arch::X86_64).unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + assert_eq!(p.parameters[0].storage, vec![Storage::Float(0)]); + assert_eq!(p.parameters[1].storage, vec![Storage::Integer(0)]); + assert_eq!(p.parameters[2].storage, vec![Storage::Float(1)]); + assert_eq!(p.float_arguments, 2); + assert_eq!(p.integer_arguments, 1); + assert!(p.returns_float); + assert!(p.returns.unwrap() >= abi.vector_base); +} + +#[test] +fn arguments_past_the_registers_land_on_the_stack_in_order() { + let abi = abi::of(&Arch::X86_64); + let f = function(&abi, 6, true); + let asserted = Asserted::parse( + "int many(int a, int b, int c, int d, int e, int f, int g, int h)", + &Arch::X86_64, + ) + .unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + assert_eq!(p.integer_arguments, 6, "System V has six of them"); + // x86 pushes the return address, so the first stack argument is one slot + // past the entry pointer. + assert_eq!(p.parameters[6].storage, vec![Storage::Stack(8)]); + assert_eq!(p.parameters[7].storage, vec![Storage::Stack(16)]); + assert_eq!(p.stack_arguments, vec![8, 16]); +} + +#[test] +fn a_result_too_large_for_the_registers_takes_the_first_one_as_its_address() { + let abi = abi::of(&Arch::X86_64); + let f = function(&abi, 2, true); + let asserted = Asserted::parse( + "struct big { long a; long b; long c; } make(int n)", + &Arch::X86_64, + ) + .unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + // The hidden pointer shifts every declared parameter by one register, + // which is the kind of thing that makes a decompilation wrong everywhere + // if it is missed. + assert_eq!(p.parameters[0].storage, vec![Storage::Integer(1)]); + assert_eq!(p.integer_arguments, 2); + + // A structure that does fit is passed in the registers themselves. + let asserted = Asserted::parse( + "int take(struct pair { int a; int b; } p, int n)", + &Arch::X86_64, + ) + .unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + assert_eq!(p.parameters[0].storage, vec![Storage::Integer(0)]); + assert_eq!(p.parameters[1].storage, vec![Storage::Integer(1)]); +} + +#[test] +fn a_declaration_the_convention_cannot_satisfy_is_reported_and_not_a_crash() { + let mut abi = abi::of(&Arch::X86_64); + // A soft-float convention: the registers exist and arguments never go in + // them. Declaring a double is then a real disagreement with the ABI. + abi.float_arguments.clear(); + abi.results.retain(|r| *r < abi.vector_base); + let f = function(&abi, 1, true); + let asserted = Asserted::parse("double f(double x)", &Arch::X86_64).unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + + assert!( + p.conflicts + .iter() + .any(|c| matches!(c, Conflict::OffConvention { index: Some(0), .. })), + "{:?}", + p.conflicts + ); + assert!( + p.conflicts + .iter() + .any(|c| matches!(c, Conflict::OffConvention { index: None, .. })), + "{:?}", + p.conflicts + ); + // It still produced an answer: the parameter went where it could. + assert_eq!(p.parameters[0].storage, vec![Storage::Stack(8)]); +} + +#[test] +fn a_varargs_declaration_is_recorded_as_one() { + let abi = abi::of(&Arch::X86_64); + let f = function(&abi, 2, true); + let asserted = Asserted::parse("int printf(const char *fmt, ...)", &Arch::X86_64).unwrap(); + let p = proto::recover_with(&f, &abi, Some(&asserted)); + assert!(p.varargs); + assert_eq!(p.origin.varargs, Strength::Asserted); + assert_eq!(p.integer_arguments, 1); +} + +#[test] +fn no_assertion_leaves_recovery_exactly_as_it_was() { + let abi = abi::of(&Arch::X86_64); + let f = function(&abi, 3, true); + assert_eq!( + proto::recover_with(&f, &abi, None), + proto::recover(&f, &abi) + ); + let p = proto::recover(&f, &abi); + assert!(!p.is_asserted()); + assert!(p.recovered.is_none()); + assert_eq!(p.arity(), 3); +} + +#[test] +fn the_type_model_follows_the_architecture() { + // The same declaration is a different prototype on a 32-bit target, + // because a `long` is not the same width there. + let lp64 = Asserted::parse("int f(long x)", &Arch::X86_64).unwrap(); + assert_eq!(lp64.types.size_of(lp64.signature.parameters[0].1), Some(8)); + let ilp32 = Asserted::parse("int f(long x)", &Arch::X86).unwrap(); + assert_eq!( + ilp32.types.size_of(ilp32.signature.parameters[0].1), + Some(4) + ); +} + +#[test] +fn a_declaration_that_is_not_a_function_is_refused_rather_than_believed() { + assert!(Asserted::parse("int x", &Arch::X86_64).is_err()); + assert!(Asserted::parse("struct widget *p", &Arch::X86_64).is_err()); + let e = Asserted::parse("int f(frobnicate x)", &Arch::X86_64).unwrap_err(); + assert!(e.message.contains("frobnicate"), "{}", e.message); +} diff --git a/crates/e5r-ir/tests/byte_registers.rs b/crates/e5r-ir/tests/byte_registers.rs new file mode 100644 index 0000000..31b3ae5 --- /dev/null +++ b/crates/e5r-ir/tests/byte_registers.rs @@ -0,0 +1,244 @@ +//! Byte-sized register operands, resolved against the register that holds them. +//! +//! The dataflow versions the register file in eight-byte units and keeps +//! one-byte register locations for the flags, which sit above the registers. A +//! lifter that names `al`, or the low byte of `w0`, as a one-byte register +//! varnode therefore names storage that no write to the whole register ever +//! reaches: the read misses its definition and the write is dead. These tests +//! pin that no lifted function does that, and that the two shapes it broke +//! carry their value through. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::{Addr, Arch}; +use e5r_format::LoadOptions; +use e5r_ir::lift::{aarch64, x86}; +use e5r_ir::op::Space; +use e5r_ir::ssa::{self, Location, Operand, SsaFunction, Value}; +use e5r_ir::{func, opt, stack}; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +fn ssa_of(f: &func::Function) -> SsaFunction { + let mut s = ssa::build(f); + opt::optimize(&mut s); + s +} + +/// Every complete function in a program, lifted and optimized, as the +/// decompiler sees them. +fn functions(p: &Program) -> Vec<(String, SsaFunction)> { + p.functions_by_address() + .filter(|f| f.is_complete() && f.cfg.blocks.len() < 200) + .filter_map(|f| { + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + if !ir.is_complete() { + return None; + } + stack::promote(&mut ir); + Some((f.display_name(), ssa_of(&ir))) + }) + .collect() +} + +fn one(p: &Program, name: &str) -> Option { + let f = p + .functions_by_address() + .find(|f| f.display_name() == name)?; + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + stack::promote(&mut ir); + Some(ssa_of(&ir)) +} + +/// The first register-file offset that is not a general register. Above it a +/// one-byte location is a flag, which is meant to be a location of its own. +fn flags_begin(arch: &Arch) -> u64 { + match arch { + Arch::AArch64 => aarch64::gpr_offset(32), + _ => x86::gpr_offset(16), + } +} + +/// Every location an operation names, whether defined or read. +fn locations(f: &SsaFunction) -> Vec<(Addr, Location)> { + let mut out = Vec::new(); + for b in f.blocks.values() { + for op in &b.ops { + if let Some(v) = op.out { + out.push((op.addr, v.location)); + } + for i in &op.inputs { + match i { + Operand::Value(v) => out.push((op.addr, v.location)), + Operand::Undefined(l) => out.push((op.addr, *l)), + Operand::Const(..) => {} + } + } + } + } + out +} + +/// No function anywhere in the corpus names a byte of a general register as a +/// location of its own. +/// +/// This is the invariant the two defects broke. It is checked over whole +/// binaries rather than over one instruction because the ways in are many: a +/// `setcc`, a shift count in `cl`, an `strb` of a register, a byte multiply. +#[test] +fn no_function_versions_a_byte_of_a_general_register() { + for binary in [ + "dt-arith.x64.O2", + "dt-memory.x64.O2", + "driver.x64.O0", + "driver.x64.O2", + "dt-arith.a64.O2", + "dt-memory.a64.O2", + "driver.a64.O0", + "driver.a64.O2", + ] { + let Some(p) = open(binary) else { continue }; + let limit = flags_begin(&p.object.arch); + let mut bad = Vec::new(); + for (name, s) in functions(&p) { + for (at, l) in locations(&s) { + if l.space == Space::Register && l.size == 1 && l.offset < limit { + bad.push(format!("{name} at {at:?}: {:#x}:1", l.offset)); + } + } + } + bad.dedup(); + assert!( + bad.is_empty(), + "{binary}: {} byte-of-a-register location(s), the first few:\n {}", + bad.len(), + bad[..bad.len().min(8)].join("\n ") + ); + } +} + +/// Everything the value of `at` depends on, following definitions backwards. +fn depends_on(f: &SsaFunction, at: Value) -> BTreeSet { + let mut defs: BTreeMap> = BTreeMap::new(); + for b in f.blocks.values() { + for op in &b.ops { + if let Some(v) = op.out { + defs.insert(v, op.inputs.clone()); + } + } + } + let mut seen = BTreeSet::new(); + let mut out = BTreeSet::new(); + let mut work = vec![at]; + while let Some(v) = work.pop() { + if !seen.insert(v) { + continue; + } + out.insert(v.location); + for i in defs.get(&v).into_iter().flatten() { + match i { + Operand::Value(u) => work.push(*u), + Operand::Undefined(l) => { + out.insert(*l); + } + Operand::Const(..) => {} + } + } + } + out +} + +/// Every definition of a register in a function, newest last. +fn defs_of(f: &SsaFunction, offset: u64) -> Vec { + let mut out: Vec = Vec::new(); + for b in f.blocks.values() { + for op in &b.ops { + if let Some(v) = op.out { + if v.location.space == Space::Register && v.location.offset == offset { + out.push(v); + } + } + } + } + out.sort_by_key(|v| v.version); + out +} + +/// `iszero` is `xor eax, eax; test edi, edi; sete al; ret`, so the returned +/// `rax` has to depend on `rdi`. With the `sete` writing a location of its own +/// the whole comparison was dead and the function returned the constant zero. +#[test] +fn a_setcc_into_a_zeroed_register_reaches_the_result() { + let Some(p) = open("dt-arith.x64.O2") else { + return; + }; + let s = one(&p, "iszero").expect("iszero"); + let rax = *defs_of(&s, x86::gpr_offset(0)) + .last() + .expect("rax is written"); + let sources = depends_on(&s, rax); + let rdi = x86::gpr_offset(7); + assert!( + sources + .iter() + .any(|l| l.space == Space::Register && l.offset == rdi), + "iszero returns something that does not depend on rdi: {sources:?}" + ); +} + +/// `rotate` computes two different shift counts into `ecx` and shifts by `cl` +/// each time. Reading `cl` as a location of its own missed both writes, so the +/// counts collapsed into one and the function grew `rcx` as an argument. +#[test] +fn a_shift_by_cl_reads_the_ecx_the_code_computed() { + let Some(p) = open("driver.x64.O0") else { + return; + }; + let s = one(&p, "rotate").expect("rotate"); + let rcx = x86::gpr_offset(1); + for (at, l) in locations(&s) { + assert!( + !(l.space == Space::Register && l.offset == rcx && l.size == 1), + "rotate at {at:?} still versions cl on its own" + ); + } + let undefined: Vec = s + .blocks + .values() + .flat_map(|b| b.ops.iter()) + .flat_map(|op| op.inputs.iter()) + .filter_map(|i| match i { + Operand::Undefined(l) if l.space == Space::Register && l.offset == rcx => { + Some(format!("{:#x}:{}", l.offset, l.size)) + } + _ => None, + }) + .collect(); + assert!( + undefined.is_empty(), + "rotate reads rcx before writing it, so the shift count came from the caller: {undefined:?}" + ); +} diff --git a/crates/e5r-ir/tests/call_sites.rs b/crates/e5r-ir/tests/call_sites.rs new file mode 100644 index 0000000..1abcfbe --- /dev/null +++ b/crates/e5r-ir/tests/call_sites.rs @@ -0,0 +1,368 @@ +//! Convention detection over real code, gated on what the compiler recorded. +//! +//! `DW_TAG_call_site_parameter` is the producer saying where it put each +//! argument of each call. That is ground truth for argument counts and +//! positions on any fixture built with debug information, and it is the +//! strongest gate available: a detection that misses a register the compiler +//! says it filled loses an argument at every call site. +//! +//! The DWARF register numbers are translated through `e5r_ir::dwreg`, which +//! exists because nothing else joins the two numbering schemes: DWARF says +//! "register 5" and only that table knows it means `rdi`. +//! +//! The second half is a survey rather than a gate: how many functions in the +//! corpus come out non-standard, and which. A number that moves is a thing to +//! look at, not a thing to fail on, so it is printed. +//! +//! Read the PE rows with care. `lift::x86` and `opt` both ask +//! `abi::of(&arch)` which registers a call clobbers and which are live at a +//! return, and that answers System V for every x86-64 image. On a PE, `rsi` +//! and `rdi` are callee-saved, so the epilogue restoring them is dead code by +//! the System V answer and is deleted before this ever sees it. Until those +//! two take the container's convention, a Windows function that saves `rsi` +//! looks like one that clobbers it. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::Addr; +use e5r_core::provenance::Strength; +use e5r_format::dwarf::Location as DwarfLocation; +use e5r_format::{Format, LoadOptions}; +use e5r_ir::abi::Abi; +use e5r_ir::conv::{self, Convention, Observed}; +use e5r_ir::dwreg; +use e5r_ir::ssa::SsaFunction; + +/// Floor on the share of recorded call-site arguments the detection also finds. +/// Only raised. +/// +/// Measured at 33 of 37 over the fixtures below. The four it misses are one +/// function, `pick` in `em-paths.{a64,x64}.{O1,O2}`, and they are one defect +/// rather than four: `pick` copies its second argument into the first argument +/// register and then tail calls, a tail call lifts to `Op::Branch` and not to +/// `Op::Return`, and `opt`'s liveness treats the convention's registers as live +/// only at a `Return`. So the copy is dead by the optimizer's rules, dead code +/// elimination deletes it, and the only read of that register is gone before +/// detection sees the function. The fix belongs in `opt`, which must treat a +/// branch leaving the function as a call and hold the argument registers live +/// across it; the floor comes back up when it lands. +const MIN_AGREEMENT: f64 = 0.89; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// The convention the container declares, not just the one the machine does. +/// The same x86-64 code passes its first argument in `rdi` under ELF and in +/// `rcx` under PE, and reading a PE image with the System V table turns every +/// prologue that saves `xmm6` into an invented argument. +fn abi_of(p: &Program) -> Abi { + e5r_ir::abi::of_container(&p.object.arch, p.object.format == Format::Pe) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// Every function in SSA form, optimized, the way the rest of the engine +/// builds them. +fn ssa(p: &Program) -> BTreeMap { + let mut out = BTreeMap::new(); + for f in p.functions_by_address() { + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + if blocks.is_empty() { + continue; + } + let mut ir = e5r_ir::func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + e5r_ir::stack::promote(&mut ir); + let mut built = e5r_ir::ssa::build(&ir); + e5r_ir::opt::optimize(&mut built); + out.insert(f.entry, built); + } + out +} + +/// What the callers say, both from their code and from what the compiler +/// recorded about them. +fn observed( + p: &Program, + built: &BTreeMap, + abi: &Abi, +) -> BTreeMap { + let mut out: BTreeMap = BTreeMap::new(); + for f in built.values() { + for (target, seen) in conv::observe(f, abi) { + out.entry(target).or_default().merge(&seen); + } + } + for (target, recorded) in recorded(p) { + out.entry(target).or_default().recorded.extend(recorded); + } + out +} + +/// The compiler's own record: for each callee, the register file offsets it +/// filled at each call site it described. +/// +/// A parameter with no register location is a stack slot or an expression this +/// does not model, and it is dropped rather than guessed at: the gate is that +/// what the compiler named in a register is found, not that nothing else +/// exists. +fn recorded(p: &Program) -> BTreeMap>> { + census(p).0 +} + +/// The same, with a count of how much ground truth the file actually carries. +fn census(p: &Program) -> (BTreeMap>>, usize, usize, usize) { + let mut out: BTreeMap>> = BTreeMap::new(); + let mut sites = 0usize; + let mut with_target = 0usize; + let mut with_registers = 0usize; + let Some(d) = p.object.debug.as_ref() else { + return (out, 0, 0, 0); + }; + for f in d.functions.values() { + for c in &f.call_sites { + sites += 1; + let Some(target) = c.target else { continue }; + with_target += 1; + let mut registers = Vec::new(); + for parameter in &c.parameters { + let Some(DwarfLocation::Register(n)) = parameter.location.as_ref() else { + continue; + }; + if let Some(offset) = dwreg::offset(&p.object.arch, *n) { + registers.push(offset); + } + } + if !registers.is_empty() { + with_registers += 1; + out.entry(target).or_default().push(registers); + } + } + } + (out, sites, with_target, with_registers) +} + +/// Running totals, so the ratio means something. +#[derive(Default)] +struct Totals { + described: usize, + found: usize, + missed: Vec, + callees: usize, + proven: usize, + /// How much ground truth there is at all: call sites the compiler + /// described, how many name a callee we can join to, and how many carry a + /// parameter in a register. + sites: usize, + with_target: usize, + with_registers: usize, +} + +fn check(fixture: &str, totals: &mut Totals) { + let Some(p) = open(fixture) else { return }; + if p.object.debug.is_none() { + return; + } + let abi = abi_of(&p); + let built = ssa(&p); + let seen = observed(&p, &built, &abi); + + let (_, sites, with_target, with_registers) = census(&p); + totals.sites += sites; + totals.with_target += with_target; + totals.with_registers += with_registers; + + let mut described = 0usize; + let mut found = 0usize; + for (at, f) in &built { + let Some(o) = seen.get(at) else { continue }; + if o.recorded.is_empty() { + continue; + } + let d = conv::detect_with(f, &abi, o); + totals.callees += 1; + if d.strength == Strength::Proven { + totals.proven += 1; + } + let arrives: BTreeSet = d + .arguments + .iter() + .filter(|a| a.purpose.is_argument()) + .map(|a| a.location.offset) + .collect(); + for register in o.recorded_registers() { + described += 1; + if arrives.contains(®ister) { + found += 1; + } else { + let name = p + .function(*at) + .map(|f| f.display_name()) + .unwrap_or_else(|| format!("{at}")); + totals.missed.push(format!( + "{fixture}: {name} takes an argument in {register:#x} the compiler recorded \ + and the reads do not show" + )); + } + } + } + if described == 0 { + return; + } + totals.described += described; + totals.found += found; + println!("{fixture}: {found} of {described} recorded argument registers found"); +} + +#[test] +fn detection_agrees_with_what_the_compiler_recorded_about_its_own_calls() { + let mut totals = Totals::default(); + // Every fixture that carries `DW_TAG_call_site_parameter` at all, which + // is a small part of the corpus: a producer emits it at -O1 and above and + // only for calls whose arguments it could describe. Listing the ones with + // none would not make the gate stronger, it would only make the list + // longer, so the list is the ones that have it. + for fixture in [ + "em-paths.a64.O1", + "em-paths.a64.O2", + "em-paths.x64.O1", + "em-paths.x64.O2", + "wide.a64.O1.o", + "wide.a64.O2.o", + "wide.a64.O3.o", + "wide.a64.Os.o", + "wide.a64.dwarf4.o", + "wide.x64.O1.o", + "wide.x64.O2.o", + "wide.x64.O3.o", + "wide.x64.Os.o", + "wide.x64.sse42.o", + "shapes.a64.O1.o", + "shapes.a64.Os.o", + "shapes.a64.O2.cpp.o", + "hello.a64.O2", + "hello.a64.dwarf4", + "panicky", + // Carry no call-site information, and are here so a producer that + // starts emitting it is measured rather than silently skipped. + "wide.a64.O0.o", + "wide.x64.O0.o", + "shapes.a64.O0.o", + "shapes.x64.O2.o", + "hello.a64.O0", + "driver.a64.O2", + "driver.x64.O2", + ] { + check(fixture, &mut totals); + } + println!( + "{} call sites described, {} name a callee in the image, {} carry a register \ + argument", + totals.sites, totals.with_target, totals.with_registers + ); + if totals.described == 0 { + // No fixture carried call-site information, which is a gap in the + // corpus rather than a failure here. + return; + } + let share = totals.found as f64 / totals.described as f64; + println!( + "{} of {} recorded argument registers found ({share:.3}) over {} callees, \ + {} of them proven", + totals.found, totals.described, totals.callees, totals.proven + ); + assert!( + share >= MIN_AGREEMENT, + "only {} of {} recorded argument registers were found ({share:.3}), floor is \ + {MIN_AGREEMENT}. first 10:\n {}", + totals.found, + totals.described, + totals + .missed + .iter() + .take(10) + .cloned() + .collect::>() + .join("\n ") + ); +} + +/// How many functions in the corpus use a convention that is not the +/// platform's, and which. +/// +/// A survey. The number is printed rather than gated because it is a property +/// of the corpus, and a corpus of compiled C where most functions departed +/// from the convention would mean the detection is measuring something else. +#[test] +fn the_corpus_survey_says_which_functions_are_not_standard() { + let mut counts: BTreeMap<&str, usize> = BTreeMap::new(); + let mut examples: Vec = Vec::new(); + let mut total = 0usize; + let mut narrowed = 0usize; + + for fixture in [ + "wide.a64.O0.o", + "wide.a64.O2.o", + "wide.x64.O0.o", + "wide.x64.O2.o", + "shapes.a64.O2.o", + "shapes.x64.O2.o", + "shapes.a64.O2.cpp", + "hello.a64.O0", + "driver.a64.O2", + "driver.x64.O2", + "cpp-hierarchy.x64.O0.rtti", + "cpp-hierarchy.a64.O0.rtti", + "win.x64.O2.exe", + "win.a64.O2.exe", + "cpp-hierarchy.win-x64.O0.rtti.exe", + ] { + let Some(p) = open(fixture) else { continue }; + let abi = abi_of(&p); + let built = ssa(&p); + let seen = observed(&p, &built, &abi); + for (at, f) in &built { + let d = conv::detect_with(f, &abi, seen.get(at).unwrap_or(&Observed::default())); + total += 1; + *counts.entry(d.convention.as_str()).or_default() += 1; + if d.convention == Convention::Fewer { + narrowed += 1; + } + if d.convention == Convention::NonStandard && examples.len() < 40 { + let name = p + .function(*at) + .map(|f| f.display_name()) + .unwrap_or_else(|| format!("{at}")); + examples.push(format!("{fixture}: {name}: {}", d.describe())); + } + } + } + if total == 0 { + return; + } + println!("{total} functions: {counts:?}"); + println!("{narrowed} took fewer arguments than the fixed order would have given them"); + for e in &examples { + println!(" {e}"); + } + // Compiled C keeps the convention. A corpus where most functions did not + // would mean this is measuring something other than the convention. + let standard = counts.get("standard").copied().unwrap_or(0) + + counts.get("standard-fewer-arguments").copied().unwrap_or(0); + assert!( + standard * 2 >= total, + "only {standard} of {total} functions keep the platform convention" + ); +} diff --git a/crates/e5r-ir/tests/convention.rs b/crates/e5r-ir/tests/convention.rs new file mode 100644 index 0000000..0f97685 --- /dev/null +++ b/crates/e5r-ir/tests/convention.rs @@ -0,0 +1,544 @@ +//! Detecting the convention a function uses, rather than assuming one. +//! +//! Functions here are built by hand. What is under test is the classification +//! itself, and a hand-built function makes the evidence exact instead of +//! whatever a compiler happened to emit. The corpus measurements live in +//! `call_sites.rs`, where real code and the compiler's own record of it are. + +use std::collections::BTreeMap; + +use e5r_core::provenance::Strength; +use e5r_core::{Addr, Arch}; +use e5r_ir::abi::{self, Abi, Named}; +use e5r_ir::conv::{self, Convention, Departure, Observed, Purpose}; +use e5r_ir::dwreg; +use e5r_ir::lift::{aarch64, x86}; +use e5r_ir::op::{Op, Space}; +use e5r_ir::ssa::{Location, Operand, SsaBlock, SsaFunction, SsaKind, SsaOp, Value}; + +const ENTRY: Addr = Addr(0x1000); + +fn reg(offset: u64) -> Location { + Location { + space: Space::Register, + offset, + size: 8, + } +} + +fn op(kind: Op, out: Option, inputs: Vec) -> SsaOp { + SsaOp { + addr: ENTRY, + kind: SsaKind::Op(kind), + out, + inputs, + size: 8, + } +} + +fn value(location: Location, version: u32) -> Value { + Value { location, version } +} + +fn ret() -> SsaOp { + op(Op::Return, None, Vec::new()) +} + +fn func(arch: Arch, ops: Vec) -> SsaFunction { + let mut blocks = BTreeMap::new(); + blocks.insert( + ENTRY, + SsaBlock { + ops, + successors: Vec::new(), + predecessors: Vec::new(), + }, + ); + SsaFunction { + arch, + entry: ENTRY, + blocks, + } +} + +/// A function that copies each of `sources` into the first result register and +/// returns. +fn reads(abi: &Abi, arch: Arch, sources: &[u64]) -> SsaFunction { + let result = reg(abi.results[0]); + let mut ops = Vec::new(); + for (n, source) in sources.iter().enumerate() { + let version = n as u32 + 1; + let mut inputs = vec![Operand::Undefined(reg(*source))]; + if n > 0 { + inputs.push(Operand::Value(value(result, version - 1))); + } + ops.push(op( + if n == 0 { Op::Copy } else { Op::IntAdd }, + Some(value(result, version)), + inputs, + )); + } + ops.push(ret()); + func(arch, ops) +} + +#[test] +fn a_function_that_reads_no_argument_register_takes_no_arguments() { + let abi = abi::of(&Arch::X86_64); + let f = func(Arch::X86_64, vec![ret()]); + let d = conv::detect(&f, &abi); + + // The whole point: the fixed order would hand this six integer arguments + // and eight floating point ones, and it takes none. + assert_eq!(d.convention, Convention::Standard); + assert_eq!(d.arity(), 0); + assert_eq!(d.assumed_arity(), 0); + assert!(d.departures.is_empty()); + assert_eq!(d.named, Some(Named::SysV)); + assert_eq!(d.strength, Strength::Inferred); +} + +#[test] +fn a_register_the_convention_passes_nothing_in_is_a_convention_the_compiler_invented() { + let abi = abi::of(&Arch::X86_64); + // r10 is caller-saved and System V passes no argument in it, so a static + // function reading it at entry got a convention of its own. + let f = reads(&abi, Arch::X86_64, &[x86::gpr_offset(10)]); + let d = conv::detect(&f, &abi); + + assert_eq!(d.convention, Convention::NonStandard); + assert_eq!(d.custom_registers(), vec![x86::gpr_offset(10)]); + assert!(d.departures.contains(&Departure::Argument { + register: x86::gpr_offset(10) + })); + // Not normalised into the default: no named convention fits. + assert_eq!(d.named, None); + assert_eq!(d.arity(), 1); +} + +#[test] +fn the_platform_default_is_not_the_only_one_a_machine_has() { + // The same four registers, under the two x86-64 conventions. Which + // argument each one is differs, which is the whole reason a PE image + // cannot be read with the System V table. + let registers: Vec = [1u8, 2, 8, 9].iter().map(|n| x86::gpr_offset(*n)).collect(); + + let win = abi::of_named(&Arch::X86_64, Named::Win64); + let f = reads(&win, Arch::X86_64, ®isters); + let d = conv::detect(&f, &win); + let purposes: Vec = d.arguments.iter().map(|a| a.purpose).collect(); + assert_eq!( + purposes, + vec![ + Purpose::Integer(0), + Purpose::Integer(1), + Purpose::Integer(2), + Purpose::Integer(3) + ] + ); + assert_eq!(d.convention, Convention::Standard); + assert_eq!(d.named, Some(Named::Win64)); + assert_eq!(d.arity(), 4); + assert_eq!(d.assumed_arity(), 4); + + let sysv = abi::of(&Arch::X86_64); + let d = conv::detect(&reads(&sysv, Arch::X86_64, ®isters), &sysv); + let purposes: Vec = d.arguments.iter().map(|a| a.purpose).collect(); + assert_eq!( + purposes, + vec![ + Purpose::Integer(2), + Purpose::Integer(3), + Purpose::Integer(4), + Purpose::Integer(5) + ] + ); + // Read with the wrong table the same four registers look like the last + // four of six, so the fixed order invents two arguments that were never + // passed. The gap is what says so. + assert_eq!(d.convention, Convention::Fewer); + assert_eq!(d.arity(), 4); + assert_eq!(d.assumed_arity(), 6); + + // The Microsoft convention reserves shadow space, so its first stack + // argument is five slots up rather than one. + assert_eq!(win.stack_argument_base, 40); + assert_eq!(sysv.stack_argument_base, 8); +} + +#[test] +fn a_callee_that_pops_the_callers_arguments_says_so() { + let abi = abi::of(&Arch::X86_64); + let sp = reg(abi.stack_pointer); + // `ret 0x10`: the lifter pops the return address and then raises the stack + // pointer by the immediate, so it comes back sixteen bytes higher than the + // convention leaves it. + let f = func( + Arch::X86_64, + vec![ + op( + Op::IntAdd, + Some(value(sp, 1)), + vec![Operand::Undefined(sp), Operand::Const(24, 8)], + ), + ret(), + ], + ); + let d = conv::detect(&f, &abi); + assert_eq!(d.callee_pops, 16); + assert_eq!(d.convention, Convention::NonStandard); + assert!(d.departures.contains(&Departure::CalleePops { bytes: 16 })); + + // The same shape under a convention where the callee does pop is the + // named convention rather than an invention. + let stdcall = abi::of_named(&Arch::X86, Named::Cdecl); + let d = conv::detect(&f, &stdcall); + assert_eq!(d.named, Some(Named::Stdcall)); +} + +#[test] +fn a_function_that_leaves_the_stack_where_it_found_it_pops_nothing() { + let abi = abi::of(&Arch::X86_64); + let sp = reg(abi.stack_pointer); + let f = func( + Arch::X86_64, + vec![ + op( + Op::IntAdd, + Some(value(sp, 1)), + vec![Operand::Undefined(sp), Operand::Const(8, 8)], + ), + ret(), + ], + ); + let d = conv::detect(&f, &abi); + assert_eq!(d.callee_pops, 0); + assert_eq!(d.convention, Convention::Standard); +} + +#[test] +fn a_prologue_saving_a_register_is_not_an_argument_arriving_in_it() { + let abi = abi::of(&Arch::X86_64); + let rbx = reg(x86::gpr_offset(3)); + // Read rbx, use it, and reload it from the frame on the way out: the + // ordinary prologue and epilogue, not a convention. + let saved = func( + Arch::X86_64, + vec![ + op( + Op::IntAdd, + Some(value(rbx, 1)), + vec![Operand::Undefined(rbx), Operand::Const(1, 8)], + ), + op(Op::Load, Some(value(rbx, 2)), vec![Operand::Const(0x20, 8)]), + ret(), + ], + ); + let d = conv::detect(&saved, &abi); + assert_eq!(d.convention, Convention::Standard); + assert!(d.custom_registers().is_empty()); + + // The same read with no restore is a register the compiler took for + // itself, and both facts are reported. + let taken = func( + Arch::X86_64, + vec![ + op( + Op::IntAdd, + Some(value(rbx, 1)), + vec![Operand::Undefined(rbx), Operand::Const(1, 8)], + ), + ret(), + ], + ); + let d = conv::detect(&taken, &abi); + assert_eq!(d.convention, Convention::NonStandard); + assert_eq!(d.custom_registers(), vec![x86::gpr_offset(3)]); + assert!(d.departures.contains(&Departure::Clobbers { + register: x86::gpr_offset(3) + })); +} + +#[test] +fn the_registers_a_convention_reserves_are_not_arguments() { + // AAPCS64 passes the address of a memory-returned result in x8 and the + // return address arrives in x30. Neither is something a caller passed, and + // counting them would give every large-struct function an extra argument. + let abi = abi::of(&Arch::AArch64); + let f = reads( + &abi, + Arch::AArch64, + &[aarch64::gpr_offset(8), aarch64::gpr_offset(30)], + ); + let d = conv::detect(&f, &abi); + assert_eq!(d.arity(), 0); + assert_eq!(d.convention, Convention::Standard); + assert!( + d.arguments + .iter() + .any(|a| a.purpose == Purpose::IndirectResult) + ); + + // System V reports the variadic vector count the same way. + let sysv = abi::of(&Arch::X86_64); + let f = reads(&sysv, Arch::X86_64, &[x86::gpr_offset(0)]); + let d = conv::detect(&f, &sysv); + assert_eq!(d.arity(), 0); + assert_eq!(d.convention, Convention::Standard); +} + +#[test] +fn a_result_nobody_reads_is_not_a_result() { + let abi = abi::of(&Arch::X86_64); + let f = reads(&abi, Arch::X86_64, &[abi.integer_arguments[0]]); + // On its own the function has left something in rax. + assert_eq!(conv::detect(&f, &abi).result, Some(x86::gpr_offset(0))); + + // Two callers, neither of which reads it: it returns nothing, and `void` + // beats a fabricated `uint64_t`. + let ignored = Observed { + sites: 2, + result_read: 0, + ..Observed::default() + }; + assert_eq!(conv::detect_with(&f, &abi, &ignored).result, None); + + // One that does: the result stands. + let read = Observed { + sites: 2, + result_read: 1, + ..Observed::default() + }; + assert_eq!( + conv::detect_with(&f, &abi, &read).result, + Some(x86::gpr_offset(0)) + ); +} + +#[test] +fn call_sites_corroborate_what_one_functions_reads_only_suggest() { + let abi = abi::of(&Arch::X86_64); + let f = reads(&abi, Arch::X86_64, &[abi.integer_arguments[0]]); + + let alone = conv::detect(&f, &abi); + assert_eq!(alone.strength, Strength::Inferred); + assert_eq!(alone.support, 0); + + let mut seen = Observed { + sites: 3, + result_read: 3, + ..Observed::default() + }; + seen.set.insert(abi.integer_arguments[0], 3); + let corroborated = conv::detect_with(&f, &abi, &seen); + assert_eq!(corroborated.strength, Strength::Inferred); + assert_eq!(corroborated.support, 3); + // Better evidence sorts first. + assert_eq!( + corroborated.rank(&alone), + std::cmp::Ordering::Less, + "three agreeing call sites outrank one function's reads" + ); +} + +#[test] +fn what_the_compiler_recorded_about_its_own_calls_is_the_strongest_gate() { + let abi = abi::of(&Arch::X86_64); + let f = reads( + &abi, + Arch::X86_64, + &[abi.integer_arguments[0], abi.integer_arguments[1]], + ); + + // `DW_TAG_call_site_parameter` said rdi and rsi, which is what the reads + // say, so this is stated by the file rather than derived from it. + let recorded = Observed { + sites: 2, + result_read: 2, + recorded: vec![ + vec![abi.integer_arguments[0], abi.integer_arguments[1]], + vec![abi.integer_arguments[0], abi.integer_arguments[1]], + ], + ..Observed::default() + }; + let d = conv::detect_with(&f, &abi, &recorded); + assert_eq!(d.strength, Strength::Proven); + assert_eq!(d.support, 2); + assert_eq!(recorded.recorded_arity(), Some(2)); + + // A record naming a register the function never reads means the reads are + // measuring something else, and the claim drops back to a guess rather + // than quietly winning. + let disagreeing = Observed { + sites: 1, + recorded: vec![vec![abi.integer_arguments[4]]], + ..Observed::default() + }; + assert_eq!( + conv::detect_with(&f, &abi, &disagreeing).strength, + Strength::Heuristic + ); +} + +#[test] +fn a_function_the_lifter_did_not_model_says_unknown_rather_than_guessing() { + let abi = abi::of(&Arch::X86_64); + let f = func( + Arch::X86_64, + vec![ + op( + Op::Copy, + Some(value(reg(abi.results[0]), 1)), + vec![Operand::Undefined(reg(x86::gpr_offset(10)))], + ), + op(Op::Unimplemented, None, Vec::new()), + ret(), + ], + ); + let d = conv::detect(&f, &abi); + assert_eq!(d.convention, Convention::Unknown); + assert_eq!(d.strength, Strength::Heuristic); + assert_eq!(d.named, None); +} + +#[test] +fn a_function_nothing_returns_from_says_so() { + let abi = abi::of(&Arch::X86_64); + let f = func( + Arch::X86_64, + vec![op( + Op::Copy, + Some(value(reg(abi.results[0]), 1)), + vec![Operand::Const(0, 8)], + )], + ); + let d = conv::detect(&f, &abi); + assert!(!d.returns_to_caller); + assert!(d.departures.contains(&Departure::NeverReturns)); + // Recovering a result from a function that never comes back is + // meaningless, so none is claimed. + assert_eq!(d.result, None); +} + +#[test] +fn an_object_pointer_is_recognised_by_what_is_done_with_it() { + let abi = abi::of(&Arch::X86_64); + let this = reg(abi.integer_arguments[0]); + let result = reg(abi.results[0]); + // A member function loads through its first argument and never treats it + // as a number. + let member = func( + Arch::X86_64, + vec![ + op( + Op::Load, + Some(value(result, 1)), + vec![Operand::Undefined(this)], + ), + ret(), + ], + ); + assert_eq!( + conv::detect(&member, &abi).this_register, + Some(abi.integer_arguments[0]) + ); + + // One that adds its first argument to something is doing arithmetic on it, + // which an object is not. + let arithmetic = reads(&abi, Arch::X86_64, &[abi.integer_arguments[0]]); + assert_eq!(conv::detect(&arithmetic, &abi).this_register, None); +} + +#[test] +fn what_the_callers_say_reaches_the_prototype() { + let abi = abi::of(&Arch::X86_64); + let f = reads(&abi, Arch::X86_64, &[abi.integer_arguments[0]]); + let ignored = Observed { + sites: 4, + result_read: 0, + ..Observed::default() + }; + let p = e5r_ir::proto::recover_observed(&f, &abi, &ignored); + assert_eq!(p.returns, None); + assert_eq!(p.integer_arguments, 1); + assert_eq!(p.detected.convention, Convention::Standard); + + // Recovery with nothing observed is unchanged: a function nothing calls + // keeps its own answer. + let alone = e5r_ir::proto::recover(&f, &abi); + assert_eq!(alone.returns, Some(x86::gpr_offset(0))); + assert_eq!(alone.detected.convention, Convention::Standard); +} + +#[test] +fn observing_a_caller_records_what_it_filled_and_what_it_read_back() { + let abi = abi::of(&Arch::X86_64); + let target = Addr(0x2000); + let rdi = reg(abi.integer_arguments[0]); + let rax = reg(x86::gpr_offset(0)); + let caller = func( + Arch::X86_64, + vec![ + op(Op::Copy, Some(value(rdi, 1)), vec![Operand::Const(7, 8)]), + op( + Op::Call, + Some(value(rax, 1)), + vec![Operand::Const(target.0, 8)], + ), + op( + Op::Copy, + Some(value(rdi, 2)), + vec![Operand::Value(value(rax, 1))], + ), + ret(), + ], + ); + let seen = conv::observe(&caller, &abi); + let seen = seen.get(&target).expect("the call was seen"); + assert_eq!(seen.sites, 1); + assert_eq!(seen.result_read, 1); + assert_eq!(seen.set.get(&abi.integer_arguments[0]), Some(&1)); + assert!(!seen.is_empty()); +} + +#[test] +fn dwarf_register_numbers_are_not_the_encoding_order() { + // The one that bites: DWARF 1 is rdx and DWARF 2 is rcx, where the + // instruction encoding has them the other way round. + assert_eq!(dwreg::offset(&Arch::X86_64, 1), Some(x86::gpr_offset(2))); + assert_eq!(dwreg::offset(&Arch::X86_64, 2), Some(x86::gpr_offset(1))); + // And the one every System V argument list starts with. + assert_eq!(dwreg::offset(&Arch::X86_64, 5), Some(x86::gpr_offset(7))); + assert_eq!(dwreg::offset(&Arch::X86_64, 4), Some(x86::gpr_offset(6))); + assert_eq!(dwreg::offset(&Arch::X86_64, 17), Some(x86::vec_offset(0))); + // 16 is the return address, which is not in the register file. + assert_eq!(dwreg::offset(&Arch::X86_64, 16), None); + + assert_eq!( + dwreg::offset(&Arch::AArch64, 0), + Some(aarch64::gpr_offset(0)) + ); + assert_eq!( + dwreg::offset(&Arch::AArch64, 31), + Some(aarch64::sp_offset()) + ); + assert_eq!( + dwreg::offset(&Arch::AArch64, 64), + Some(aarch64::vec_offset(0)) + ); + assert_eq!(dwreg::offset(&Arch::AArch64, 32), None); + + for arch in [Arch::X86_64, Arch::AArch64, Arch::X86] { + assert!(dwreg::known(&arch)); + for n in 0..96u16 { + if let Some(offset) = dwreg::offset(&arch, n) { + assert_eq!( + dwreg::number(&arch, offset), + Some(n), + "{arch:?} register {n}" + ); + } + } + } + assert!(!dwreg::known(&Arch::Unknown(0))); +} diff --git a/crates/e5r-ir/tests/coverage.rs b/crates/e5r-ir/tests/coverage.rs new file mode 100644 index 0000000..99e410d --- /dev/null +++ b/crates/e5r-ir/tests/coverage.rs @@ -0,0 +1,168 @@ +//! How much of what the decoder decodes the lifter also models. +//! +//! The same shape as the decoder's coverage gate, and for the same reason: an +//! instruction the lifter does not model is a gap with a floor that only rises, +//! and it is reported honestly rather than approximated. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, analyze}; +use e5r_core::Arch; +use e5r_format::LoadOptions; +use e5r_ir::lift; + +/// Floor on the share of decoded instructions the lifter models. Only raised. +const MIN_COVERAGE: f64 = 0.998; +/// The x86-64 floor, which has only the fixtures behind it: this host runs no +/// x86 system binaries, so the corpus is smaller and the number is not +/// comparable to the AArch64 one. +const MIN_X86_COVERAGE: f64 = 0.999; +/// The ARM32 floor, which has only the relocatable fixtures behind it: this +/// host runs no ARM binaries, so the corpus is smaller than the AArch64 one +/// and the number is not comparable to it. +/// +/// Lower than the other two for one reason, which is measurable rather than +/// arguable: every instruction this corpus decodes and the lifter does not +/// model is VFP, and floating point is not modelled on any architecture here. +/// These fixtures are simply full of it, where the AArch64 and x86-64 ones are +/// not. +const MIN_ARM_COVERAGE: f64 = 0.93; + +fn targets(arch: &Arch) -> Vec { + let mut out = Vec::new(); + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + let tag = match arch { + Arch::AArch64 => "a64", + Arch::Arm => ".arm.", + Arch::Thumb => ".thumb.", + _ => "x64", + }; + if let Ok(entries) = std::fs::read_dir(&dir) { + for e in entries.flatten() { + let p = e.path(); + let n = p.file_name().unwrap().to_string_lossy().into_owned(); + if n.contains(tag) && !n.contains("macho") && !n.ends_with(".out") { + out.push(p); + } + } + } + if *arch == Arch::AArch64 { + for p in [ + "/usr/lib/aarch64-linux-gnu/libc.so.6", + "/bin/bash", + "/bin/ls", + ] { + let p = PathBuf::from(p); + if p.is_file() { + out.push(p); + } + } + } + out +} + +/// Count what lifts, over the instructions inside recovered functions. +/// +/// Walking every executable byte instead would count section padding, which +/// decodes as `udf` and made up a seventh of the denominator. Measuring against +/// what analysis believes is code is the honest denominator. +fn measure(arch: &Arch) -> (u64, u64, BTreeMap) { + let mut decoded = 0u64; + let mut lifted = 0u64; + let mut missing: BTreeMap = BTreeMap::new(); + + for path in targets(arch) { + let Ok(data) = std::fs::read(&path) else { + continue; + }; + let Ok(obj) = e5r_format::load(&data, &LoadOptions::default()) else { + continue; + }; + if obj.arch != *arch { + continue; + } + let program = analyze(obj, &Options::default()); + for f in program.functions_by_address() { + for insn in program.instructions(f) { + decoded += 1; + if lift::lift(arch, &insn).complete { + lifted += 1; + } else { + *missing.entry(insn.mnemonic.to_string()).or_default() += 1; + } + } + } + } + (decoded, lifted, missing) +} + +#[test] +fn the_lifter_models_most_of_what_the_decoder_decodes() { + check(&Arch::AArch64, MIN_COVERAGE, 10_000); +} + +#[test] +fn the_x86_lifter_models_most_of_what_the_decoder_decodes() { + check(&Arch::X86_64, MIN_X86_COVERAGE, 2_000); +} + +#[test] +fn the_arm_lifter_models_most_of_what_the_decoder_decodes() { + check(&Arch::Arm, MIN_ARM_COVERAGE, 1_000); +} + +#[test] +fn the_thumb_lifter_models_most_of_what_the_decoder_decodes() { + check(&Arch::Thumb, MIN_ARM_COVERAGE, 1_000); +} + +fn check(arch: &Arch, floor: f64, minimum: u64) { + let (decoded, lifted, missing) = measure(arch); + if decoded < minimum { + return; // no corpus + } + let coverage = lifted as f64 / decoded as f64; + let mut worst: Vec<(&String, &u64)> = missing.iter().collect(); + worst.sort_by_key(|(_, n)| std::cmp::Reverse(**n)); + let summary: Vec = worst + .iter() + .take(10) + .map(|(m, n)| format!("{m} x{n}")) + .collect(); + assert!( + coverage >= floor, + "{arch:?}: lifted {:.2}% of {decoded} decoded instructions, floor is {:.2}%\n\ + biggest gaps: {}", + coverage * 100.0, + floor * 100.0, + summary.join(", ") + ); + println!( + "{arch:?} lift coverage: {lifted} of {decoded} decoded instructions, {:.2}%", + coverage * 100.0 + ); +} + +/// A work list, not a gate. Ignored by default. +#[test] +#[ignore] +fn lift_coverage_report() { + for arch in [Arch::AArch64, Arch::X86_64, Arch::Arm, Arch::Thumb] { + report(&arch); + } +} + +fn report(arch: &Arch) { + let (decoded, lifted, missing) = measure(arch); + println!("--- {arch:?} ---"); + println!( + "{lifted} of {decoded} lifted ({:.2}%)", + lifted as f64 / decoded.max(1) as f64 * 100.0 + ); + let mut v: Vec<(String, u64)> = missing.into_iter().collect(); + v.sort_by_key(|(_, n)| std::cmp::Reverse(*n)); + for (m, n) in v.iter().take(40) { + println!("{n:>8} {m}"); + } +} diff --git a/crates/e5r-ir/tests/i386.rs b/crates/e5r-ir/tests/i386.rs new file mode 100644 index 0000000..5349d41 --- /dev/null +++ b/crates/e5r-ir/tests/i386.rs @@ -0,0 +1,244 @@ +//! The i386 lifter measured against a processor. +//! +//! i386 and x86-64 are one decoder and, now, one lifter: the rules are the +//! same and the pointer is half as wide. That difference is not cosmetic -- +//! every stack adjustment, return address and effective address is computed at +//! the narrower width, and 32-bit arithmetic wraps where 64-bit arithmetic +//! does not. So the cases here are the ones where the two modes disagree, +//! rather than a second pass over what the x86-64 oracle already covers. +//! +//! This host is aarch64, so the i386 side runs under qemu, which is how the +//! rest of the lifter's hardware oracle already works. Nothing below is an +//! assertion about what the lifter ought to do: every expected value came out +//! of a processor. + +use std::process::Command; + +use e5r_core::{Addr, Arch}; +use e5r_ir::interp::{Step, step}; +use e5r_ir::{Machine, lift}; + +/// Where the interpreted machine's stack goes, and where qemu will not put +/// anything of its own. +const STACK: u64 = 0x4000_0000; + +/// One case: a name, and AT&T assembly that leaves its answer in `eax`. +/// +/// `esp` is the harness's, so a case that moves it has to put it back. The +/// sequences that push do exactly that, which is the point of testing them. +const CASES: &[(&str, &str)] = &[ + // The arithmetic wraps at 32 bits, where the same lifter at 64 would not. + ("add_wraps", "movl $0xffffffff, %eax\n addl $2, %eax\n"), + ("sub_wraps", "movl $1, %eax\n subl $3, %eax\n"), + ("imul_wraps", "movl $0x10000, %eax\n imull $0x10000, %eax\n"), + ("neg_wraps", "movl $0, %eax\n negl %eax\n"), + ("shl_drops", "movl $0x80000001, %eax\n shll $1, %eax\n"), + ("sar_signed", "movl $0x80000000, %eax\n sarl $4, %eax\n"), + ("shr_unsigned", "movl $0x80000000, %eax\n shrl $4, %eax\n"), + // A push moves the stack by four, and the pop has to find the same place. + ( + "push_pop", + "movl $0x12345678, %eax\n pushl %eax\n xorl %eax, %eax\n popl %eax\n", + ), + ( + "push_order", + "pushl $0x11111111\n pushl $0x22222222\n popl %eax\n addl (%esp), %eax\n addl $4, %esp\n", + ), + // An effective address computed at 32 bits, including the scaled index + // form a compiler uses for array access. + ( + "lea_scaled", + "movl $0x1000, %eax\n movl $3, %ecx\n leal 8(%eax,%ecx,4), %eax\n", + ), + ( + "lea_wraps", + "movl $0xfffffff0, %eax\n leal 0x20(%eax), %eax\n", + ), + // Flags, which 32-bit code branches on exactly as 64-bit code does but at + // a different width: the sign bit is bit 31 here. + ( + "sign_at_31", + "movl $0x7fffffff, %eax\n addl $1, %eax\n setsb %al\n movzbl %al, %eax\n", + ), + ( + "carry_at_32", + "movl $0xffffffff, %eax\n addl $1, %eax\n setcb %al\n movzbl %al, %eax\n", + ), + ( + "overflow_at_31", + "movl $0x7fffffff, %eax\n addl $1, %eax\n setob %al\n movzbl %al, %eax\n", + ), + // `leave` is `mov esp, ebp; pop ebp` at the narrow width. + ( + "leave_frame", + "pushl %ebp\n movl %esp, %ebp\n subl $16, %esp\n movl $0xabcd, %eax\n leave\n", + ), + // A byte and a word destination leave the rest of the register alone, + // which is the same rule as in long mode and is easy to get wrong when the + // register file is modelled eight bytes wide and the machine is four. + ("byte_dest", "movl $0x11223344, %eax\n movb $0x99, %al\n"), + ("word_dest", "movl $0x11223344, %eax\n movw $0x9988, %ax\n"), + ("movzx_byte", "movl $0xffffffff, %eax\n movzbl %al, %eax\n"), + ("movsx_byte", "movl $0x000000ff, %eax\n movsbl %al, %eax\n"), +]; + +fn tool(name: &str) -> Option { + Command::new(name) + .arg("--version") + .output() + .ok() + .filter(|o| o.status.success()) + .map(|_| name.to_string()) +} + +/// Run every case on a processor and hand back the `eax` each one leaves. +/// +/// One program rather than one per case, because the build dominates and a +/// single run keeps the cases in step with the table above by construction. +fn on_hardware() -> Option> { + let cc = tool("clang")?; + let qemu = tool("qemu-i386").or_else(|| tool("qemu-i386-static"))?; + // A test runs with its crate as the working directory, not the workspace. + let lld = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build/ld"); + if !lld.join("ld.lld").exists() { + return None; // the corpus has not been built + } + let dir = std::env::temp_dir().join(format!("e5r-i386-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("m.s"); + let bin = dir.join("m"); + + // Written as assembly rather than C with inline assembly: this host has no + // i386 libc headers, and the cases are assembly anyway. The results go out + // through a raw `write` and the program exits with a raw `_exit`, both + // through `int $0x80`, which is how a 32-bit process reaches the kernel. + let mut body = String::from(".text\n.globl _start\n_start:\n"); + for (_, asm) in CASES { + body.push_str(asm); + // Keep the answer where the harness can find it, then print it. `eax` + // is the syscall number, so the answer goes to memory first. + body.push_str(" movl %eax, out\n"); + body.push_str( + " movl $4, %eax\n movl $1, %ebx\n movl $out, %ecx\n movl $4, %edx\n int $0x80\n", + ); + } + body.push_str(" movl $1, %eax\n xorl %ebx, %ebx\n int $0x80\n"); + body.push_str(".data\nout: .long 0\n"); + std::fs::write(&src, body).ok()?; + + let built = Command::new(&cc) + .args([ + "--target=i386-unknown-linux-gnu", + "-static", + "-nostdlib", + "-ffreestanding", + ]) + .arg(format!("-B{}", lld.display())) + .arg("-fuse-ld=lld") + .arg("-o") + .arg(&bin) + .arg(&src) + .output() + .ok()?; + if !built.status.success() { + return None; + } + let ran = Command::new(&qemu).arg(&bin).output().ok()?; + let _ = std::fs::remove_dir_all(&dir); + let raw = ran.stdout; + if raw.len() != CASES.len() * 4 { + return None; + } + Some( + raw.chunks_exact(4) + .map(|c| u32::from_le_bytes(c.try_into().unwrap())) + .collect(), + ) +} + +/// Assemble one case and hand back its bytes. +fn assemble(text: &str) -> Option> { + let cc = tool("clang")?; + let dir = std::env::temp_dir().join(format!("e5r-i386asm-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("a.s"); + let obj = dir.join("a.o"); + let bin = dir.join("a.bin"); + std::fs::write(&src, format!(".text\n{text}\n")).ok()?; + let ok = Command::new(&cc) + .args(["--target=i386-unknown-linux-gnu", "-c", "-o"]) + .arg(&obj) + .arg(&src) + .status() + .ok()?; + if !ok.success() { + return None; + } + // The bytes rather than a disassembly, because a case is several + // instructions and reading them back out of a listing would re-parse what + // the decoder is about to be handed. + let cut = Command::new("llvm-objcopy-18") + .args(["-O", "binary", "--only-section=.text"]) + .arg(&obj) + .arg(&bin) + .status() + .or_else(|_| { + Command::new("llvm-objcopy") + .args(["-O", "binary", "--only-section=.text"]) + .arg(&obj) + .arg(&bin) + .status() + }) + .ok()?; + if !cut.success() { + return None; + } + let out = std::fs::read(&bin).ok()?; + let _ = std::fs::remove_dir_all(&dir); + Some(out) +} + +/// Decode, lift and run a case's bytes, and read `eax` back. +fn interpreted(bytes: &[u8]) -> Option { + let mut m = Machine::new(); + m.set_reg(lift::x86::sp_offset(), 8, STACK); + let mut at = 0usize; + while at < bytes.len() { + let insn = e5r_arch::x86::decode32(&bytes[at..], Addr(0x1000 + at as u64))?; + let lifted = lift::x86::lift32(&insn); + assert!( + lifted.complete, + "{} is not modelled", + e5r_arch::format(&Arch::X86, &insn, false) + ); + for op in &lifted.ops { + if !matches!(step(&mut m, op), Step::Next) { + return None; + } + } + at += insn.len as usize; + } + Some(m.reg(lift::x86::gpr_offset(0), 4) as u32) +} + +#[test] +fn the_i386_lifter_computes_what_the_processor_computes() { + let Some(expected) = on_hardware() else { + return; // no clang, no qemu-i386, or the corpus has not been built + }; + let mut checked = 0; + for ((name, asm), want) in CASES.iter().zip(expected) { + // Not a silent skip: `on_hardware` has already built and run a + // program with these same cases in it, so an assembler that cannot + // assemble one of them now is a broken harness reporting a pass. + let bytes = assemble(asm).unwrap_or_else(|| panic!("{name}: did not assemble")); + let got = interpreted(&bytes).unwrap_or_else(|| panic!("{name}: did not run")); + assert_eq!( + got, want, + "{name}: the processor says {want:#x}, the lifter says {got:#x}" + ); + checked += 1; + } + assert_eq!(checked, CASES.len()); + eprintln!("{checked} i386 case(s) agree with the processor"); +} diff --git a/crates/e5r-ir/tests/nopanic_gate.rs b/crates/e5r-ir/tests/nopanic_gate.rs new file mode 100644 index 0000000..960d2bd --- /dev/null +++ b/crates/e5r-ir/tests/nopanic_gate.rs @@ -0,0 +1,868 @@ +//! G6: one gate over every path that takes bytes somebody else wrote. +//! +//! The per-format mutation fuzzers already exist and each one proves a point +//! about its own parser. None of them is the thing M12 asks for, which is a +//! single statement about the whole untrusted surface: the loaders, the +//! archive reader, the overlay pass, DWARF, PDB, the Swift and Go metadata +//! readers and every architecture's decoder, driven from one corpus with one +//! budget, so that a new entry point is either wired in here or is visibly +//! not covered. +//! +//! It lives in `e5r-ir` because that is the only crate in the gate list whose +//! dev-dependencies reach both `e5r-format` and `e5r-arch`; a gate split +//! across two test binaries is two gates. +//! +//! The assertion is three things, not one: +//! +//! 1. **No panic.** Implicit: a panic in a test is a failed test. +//! 2. **Bounded time per case.** A loader that takes a second on a 64 KiB +//! input has believed a number, and a timeout is how that shows up before +//! it becomes a hang. +//! 3. **An error rather than a wrong answer.** An `Object` that comes back is +//! checked against [`check_object`]: every count under its cap, every +//! string under the string cap, and every byte a segment holds traced back +//! to a byte that was actually in the file. A loader that hands back +//! content it could not have read has not failed safely, it has failed +//! quietly, and that is the worse of the two. What is deliberately *not* +//! checked, and why, is written at each check. +//! +//! What it found is at the bottom of the file: three defects in loader +//! arithmetic, all the same mistake of adding a number the file chose to an +//! address without checking. All three are fixed, and each keeps its minimal +//! reproduction as a test of the corrected behaviour. +//! +//! The corruptions are the ones that find things, which the existing fuzzers +//! already say and this one inherits: uniformly random bytes rarely make a +//! count enormous, so the sweeps are runs of `0xff` and runs of ASCII `9`, +//! plus truncation at every block boundary and at the first bytes of every +//! structure the loader told us about. + +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +use e5r_arch::decode; +use e5r_core::{Addr, Arch, Caps}; +use e5r_format::{LoadOptions, Object, archive, metadata, overlay, pdb, swift}; + +/// Total wall time for the whole gate, split between its phases below. +/// +/// Chosen so the gate runs on every `cargo test`, which is the only way it +/// finds anything: a nightly job on this machine is a job nobody runs. Twelve +/// seconds is roughly the cost of one crate's compile, so it disappears into +/// a suite run, and it reaches tens of thousands of cases because each case is +/// a 64 KiB prefix rather than a whole libc. +const BUDGET: Duration = Duration::from_secs(12); + +/// Phase shares of [`BUDGET`]. The structured sweeps get the most because +/// they are the ones aimed at a field rather than at a byte. +const SWEEP_BUDGET: Duration = Duration::from_millis(5000); +const TRUNCATE_BUDGET: Duration = Duration::from_millis(3000); +const RANDOM_BUDGET: Duration = Duration::from_millis(2000); +const DECODE_BUDGET: Duration = Duration::from_millis(2000); + +/// What one case may take. Generous on purpose: this is a hang detector, not +/// a performance budget, and a loaded machine must not turn it red. +const PER_CASE: Duration = Duration::from_secs(2); + +/// Corpus prefix size. Enough to carry a real header, a section table and the +/// start of the payload; short enough that a case is microseconds. +const PREFIX: usize = 64 * 1024; + +/// The two values that turn a size or an entry count enormous. Uniformly +/// random bytes almost never do, which is why these are swept explicitly. +const POISON: [u8; 4] = [0xff, b'9', 0x80, 0x00]; + +fn corpus_dir() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then_some(d) +} + +/// One seed per shape of input, so the gate covers containers rather than +/// covering whichever container the corpus happens to have most of. +/// +/// Names are prefixes: the corpus is built by `scripts/build-fixtures.sh` and +/// an absent fixture is skipped rather than failing, like every other gate +/// here. +const SEED_PATTERNS: [&str; 13] = [ + "hello.a64.O2", // ELF executable, AArch64, DWARF kept + "hello.a64.O0.stripped", // ELF executable, no symbols + "wide.x64.O2.o", // ELF relocatable, x86-64 + "wide.macho.x64.o", // Mach-O, x86-64 + "wide.macho.a64.o", // Mach-O, AArch64 + "wide.coff.o", // COFF, which the PE loader also takes + "pdb.x64.O2.exe", // PE executable with a debug directory + "cpp-hierarchy.win-x64.O2.rtti.exe", // PE, C++, imports and exports + "pdb.x64.O2.pdb", // MSF, the PDB reader's own input + "libshapes.a", // archive, regular + "libshapes-thin.a", // archive, thin + "hello.go", // Go, so pclntab is reached + "cpp-hierarchy.a64.O2.rtti", // C++ with RTTI and vtables +]; + +struct Seed { + name: String, + data: Vec, +} + +fn seeds() -> Vec { + let Some(dir) = corpus_dir() else { + return Vec::new(); + }; + let mut out = Vec::new(); + for pat in SEED_PATTERNS { + let p = dir.join(pat); + if let Ok(d) = std::fs::read(&p) { + out.push(Seed { + name: pat.to_string(), + data: d[..d.len().min(PREFIX)].to_vec(), + }); + } + } + // A corpus that lost a fixture should still gate on what is there, so + // fall back to whatever the directory holds rather than to nothing. + if out.len() < 4 { + if let Ok(entries) = std::fs::read_dir(&dir) { + let mut extra: Vec<_> = entries.flatten().map(|e| e.path()).collect(); + extra.sort(); + for p in extra.into_iter().take(24) { + if !p.is_file() { + continue; + } + let name = p.file_name().unwrap().to_string_lossy().to_string(); + if out.iter().any(|s| s.name == name) { + continue; + } + if let Ok(d) = std::fs::read(&p) { + out.push(Seed { + name, + data: d[..d.len().min(PREFIX)].to_vec(), + }); + } + } + } + } + out +} + +/// Every entry point that takes bytes somebody else wrote. +/// +/// One function, so adding a reader to the crate and not adding it here is a +/// visible omission rather than an invisible one. Returns the object when the +/// bytes loaded, for the caller to check. +fn drive_every_entry_point(data: &[u8], opts: &LoadOptions) -> Option { + // The archive reader runs first because `load` refuses archives outright, + // so it is the only way those bytes reach a parser at all. + if let Ok(ar) = archive::open(data) { + for (i, m) in ar.objects().take(16) { + let _ = m.data(); + let _ = m.is_special(); + let _ = ar.symbols_of(i); + let _ = ar.load_member(i, opts); + } + // `definers` walks the index, which is a second set of file-chosen + // offsets and has its own arithmetic. + let _ = ar.definers("main"); + } + let _ = archive::is_archive(data); + + // The PDB reader takes raw bytes rather than an object, so it is driven + // whether or not the bytes are a container. + let _ = pdb::identity(data); + let _ = pdb::read(data, &[]); + let _ = pdb::parse(data, &[]); + + // Raw mode reaches the decoder-fed path with no header to sanity-check it. + let mut raw_opts = opts.clone(); + raw_opts.arch = Some(Arch::X86_64); + raw_opts.base = Some(Addr(0x40_0000)); + let _ = e5r_format::raw::load(data, &raw_opts); + + let obj = match e5r_format::load(data, opts) { + Ok(o) => o, + Err(_) => return None, + }; + + // Everything downstream of a successful load. DWARF is already inside + // `obj.debug` because `LoadOptions::debug_info` is on. + if let Some(d) = &obj.debug { + let _ = d.is_empty(); + let _ = d.hints(); + let _ = d.function_at(Addr(0x1000)); + let _ = d.line_for(Addr(0x1000)); + let _ = d.inlined_at(Addr(0x1000)); + let _ = d.call_site_returning_to(Addr(0x1000)); + } + let _ = overlay::analyze(&obj, data); + let _ = overlay::described_end(&obj, data); + let _ = overlay::section_entropy(&obj, data); + let _ = swift::read(&obj); + let _ = metadata::read(&obj); + let _ = metadata::go_pclntab(&obj); + let _ = metadata::objc_classes(&obj); + let _ = metadata::rust_panic_sites(&obj); + // The PDB reader again, this time with a real section table to rebase + // against, which is the arithmetic that a corrupt section table breaks. + let _ = pdb::read(data, &obj.sections); + // Decoding what the loader says is code closes the loop: a section whose + // bounds are wrong reaches the decoder as a slice, and that is how a + // loader bug becomes a decoder crash. + decode_exec_sections(&obj); + Some(obj) +} + +/// Decode every executable section the object claims, at every offset a real +/// sweep would try. +fn decode_exec_sections(obj: &Object) { + for s in obj.sections.iter().filter(|s| s.exec).take(8) { + // `decode_window` rather than `slice`: a section whose declared range + // runs past the bytes that back it is exactly the case under test, + // and an exact-length slice would skip it. + let Some(bytes) = obj + .memory + .decode_window(s.range.start(), s.range.len().min(16 * 1024)) + else { + continue; + }; + let step = obj.arch.insn_alignment().max(1) as usize; + let mut i = 0usize; + while i < bytes.len() { + // `wrapping_add` because the section may sit at the top of the + // address space after a corruption, and the address handed to the + // decoder is not what is under test here. + let at = Addr(s.range.start().get().wrapping_add(i as u64)); + if let Some(insn) = decode(&obj.arch, &bytes[i..], at) { + assert!( + insn.len > 0 && insn.len as usize <= bytes.len() - i, + "{:?} decoded {} bytes from {} at +{i}", + obj.arch, + insn.len, + bytes.len() - i + ); + let _ = e5r_arch::format(&obj.arch, &insn, true); + } + i += step; + } + } +} + +/// What a returned object must be true about itself. +/// +/// This is the "error rather than a wrong answer" half of the contract. Every +/// check is against either the input it was parsed from or a documented cap, +/// never against another field of the same object, because a file that lies +/// consistently would pass that. +fn check_object(obj: &Object, data: &[u8], caps: &Caps, what: &str) { + assert!( + obj.sections.len() as u64 <= caps.sections, + "{what}: {} sections over the cap of {}", + obj.sections.len(), + caps.sections + ); + assert!( + obj.symbols.len() as u64 <= caps.symbols, + "{what}: {} symbols over the cap of {}", + obj.symbols.len(), + caps.symbols + ); + for s in &obj.sections { + assert!( + s.name.len() as u64 <= caps.string_len, + "{what}: a section name is {} bytes", + s.name.len() + ); + } + // Deliberately not checked: that a section's declared `file_offset` and + // `file_size` fit the file. A `Section` is a record of what the container + // said, and reporting that faithfully is how a caller sees a truncated + // file at all. What must be true is that nothing was *read* from outside + // the file, and that is a property of `memory`, checked below. + for sym in &obj.symbols { + assert!( + sym.name.len() as u64 <= caps.string_len, + "{what}: a symbol name is {} bytes, over the {} cap", + sym.name.len(), + caps.string_len + ); + } + // Deliberately not checked here: that `sym.addr + sym.size` does not wrap. + // It does, on a corrupted symbol table, and two consumers add the pair + // unchecked: `Object::symbol_at` in `e5r-format/src/lib.rs` and + // `e5r-api/src/vtables.rs`. That is a real defect with its own + // reproduction in `symbol_at_adds_a_file_chosen_size_unchecked`, not an + // invariant this gate can hold today. + for seg in obj.memory.segments() { + let backed = seg + .slice_to_end(seg.range.start()) + .map(|b| b.len() as u64) + .unwrap_or(0); + assert!( + backed <= seg.range.len(), + "{what}: a segment holds {backed} bytes for a range of {}", + seg.range.len() + ); + // The bytes a segment actually holds must have come out of the file. + // A segment whose declared offset is past the end is a file that + // lied, and the loader maps it as zeros; a segment holding bytes that + // are not in the file would be a fabrication, and that is the failure + // this catches. + if backed > 0 { + assert!( + seg.file_offset + .checked_add(backed) + .is_some_and(|e| e <= data.len() as u64), + "{what}: a segment holds {backed} bytes from offset {} of a {}-byte file", + seg.file_offset, + data.len() + ); + } + } + // Deliberately not checked: that a function hint's address plus its size + // does not wrap. It carries the symbol's declared size verbatim, so it + // wraps wherever the symbol does, for the same reason and with the same + // consumers at fault. +} + +/// Run one case, with the clock on it. +fn one_case(data: &[u8], opts: &LoadOptions, what: &str) { + let started = Instant::now(); + if let Some(obj) = drive_every_entry_point(data, opts) { + check_object(&obj, data, &opts.caps, what); + } + let took = started.elapsed(); + assert!( + took < PER_CASE, + "{what}: one case took {took:?}, which is a believed number rather than a slow machine" + ); +} + +/// The clean corpus, unmutated. +/// +/// Without this the invariant checker could be vacuous: a `check_object` that +/// no real file ever reaches proves nothing about the mutated ones. +#[test] +fn the_real_corpus_is_self_consistent() { + let seeds = seeds(); + if seeds.is_empty() { + println!("nopanic gate: no fixtures/build; run scripts/build-fixtures.sh. Skipped."); + return; + } + let opts = LoadOptions::default(); + let mut loaded = 0; + for s in &seeds { + // Whole files here, not prefixes: the point is that the checker + // accepts what is correct. + let dir = corpus_dir().unwrap(); + let data = std::fs::read(dir.join(&s.name)).unwrap_or_else(|_| s.data.clone()); + if let Some(obj) = drive_every_entry_point(&data, &opts) { + check_object(&obj, &data, &opts.caps, &s.name); + loaded += 1; + } + } + assert!( + loaded >= 3, + "only {loaded} of {} seeds loaded at all, so the invariant checker is not being exercised", + seeds.len() + ); + println!("nopanic gate: {loaded} clean fixtures self-consistent"); +} + +/// Poison sweep: every field made enormous, one field at a time. +/// +/// The stride walks the whole prefix rather than the header alone, because a +/// section table, a symbol table and a DWARF unit header are all "the header" +/// to whichever reader is walking them. +#[test] +fn poison_runs_at_every_offset_do_not_panic_or_lie() { + let seeds = seeds(); + if seeds.is_empty() { + println!("nopanic gate: no fixtures/build. Skipped."); + return; + } + let opts = LoadOptions::default(); + let started = Instant::now(); + let mut cases = 0u64; + // A prime stride so the sweep does not land on the same field alignment + // in every structure, and a run length of 8 so a 64-bit size is wholly + // covered wherever the run starts within it. + const STRIDE: usize = 13; + const RUN: usize = 8; + 'outer: for round in 0.. { + for seed in &seeds { + let base = round * STRIDE * POISON.len(); + if base >= seed.data.len() { + continue; + } + for (k, value) in POISON.iter().enumerate() { + let at = base + k * STRIDE; + if at >= seed.data.len() { + continue; + } + let mut case = seed.data.clone(); + let end = (at + RUN).min(case.len()); + for b in &mut case[at..end] { + *b = *value; + } + one_case( + &case, + &opts, + &format!("{} +{at:#x} {value:#04x}", seed.name), + ); + cases += 1; + } + if started.elapsed() >= SWEEP_BUDGET { + break 'outer; + } + } + if started.elapsed() >= SWEEP_BUDGET { + break; + } + if round * STRIDE * POISON.len() > seeds.iter().map(|s| s.data.len()).max().unwrap_or(0) { + break; + } + } + assert!( + cases > 500, + "only {cases} poison cases in {SWEEP_BUDGET:?}; the gate is not reaching enough fields" + ); + println!( + "nopanic gate: {cases} poison-run cases in {:?}", + started.elapsed() + ); +} + +/// Truncation at every block boundary and at the first bytes of every +/// structure the loader itself pointed at. +/// +/// The structure offsets come from loading the file once and asking where it +/// said its sections are, which is a better list of interesting cuts than any +/// stride: it is exactly the set of places a length was about to be trusted. +#[test] +fn truncation_at_every_boundary_stops_cleanly() { + let seeds = seeds(); + if seeds.is_empty() { + println!("nopanic gate: no fixtures/build. Skipped."); + return; + } + let opts = LoadOptions::default(); + let started = Instant::now(); + let mut cases = 0u64; + for seed in &seeds { + let mut cuts: Vec = Vec::new(); + // Every block boundary. + let mut b = 0usize; + while b <= seed.data.len() { + cuts.push(b); + b += 512; + } + // The first bytes of the file, one at a time: this is where the + // five-byte ELF that panicked the first fuzz run lived. + cuts.extend(0..=96.min(seed.data.len())); + // The first bytes of every structure the file declares. + if let Ok(obj) = e5r_format::load(&seed.data, &opts) { + for s in &obj.sections { + for d in [0usize, 1, 2, 3, 4, 8, 16] { + let at = s.file_offset as usize + d; + if at <= seed.data.len() { + cuts.push(at); + } + } + } + for seg in obj.memory.segments() { + let at = seg.file_offset as usize; + if at <= seed.data.len() { + cuts.push(at); + } + } + } + cuts.sort_unstable(); + cuts.dedup(); + for cut in cuts { + one_case( + &seed.data[..cut], + &opts, + &format!("{} cut at {cut}", seed.name), + ); + cases += 1; + if started.elapsed() >= TRUNCATE_BUDGET { + break; + } + } + if started.elapsed() >= TRUNCATE_BUDGET { + break; + } + } + assert!( + cases > 300, + "only {cases} truncations in {TRUNCATE_BUDGET:?}" + ); + println!( + "nopanic gate: {cases} truncations in {:?}", + started.elapsed() + ); +} + +/// A small deterministic generator, so a failure is reproducible from its +/// seed. Same xorshift the per-format fuzzers use, on purpose. +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545_f491_4f6c_dd1d) + } + + fn below(&mut self, n: usize) -> usize { + if n == 0 { + 0 + } else { + (self.next() % n as u64) as usize + } + } +} + +/// Bit flips, splices and header scrambles, for the corruptions that a +/// field-aligned sweep cannot express. +#[test] +fn random_mutation_finds_nothing_either() { + let seeds = seeds(); + if seeds.is_empty() { + println!("nopanic gate: no fixtures/build. Skipped."); + return; + } + let opts = LoadOptions::default(); + let mut rng = Rng(0x6a7e_0001_0000_0001); + let started = Instant::now(); + let mut cases = 0u64; + while started.elapsed() < RANDOM_BUDGET { + let seed = &seeds[rng.below(seeds.len())]; + let mut v = seed.data.clone(); + if v.is_empty() { + break; + } + // Several edits per case: one flip in 64 KiB usually changes nothing + // a parser reaches. + for _ in 0..1 + rng.below(6) { + match rng.below(4) { + 0 => { + let i = rng.below(v.len()); + v[i] ^= 1 << rng.below(8); + } + 1 => { + let i = rng.below(v.len()); + let end = (i + 1 + rng.below(16)).min(v.len()); + let val = POISON[rng.below(POISON.len())]; + for b in &mut v[i..end] { + *b = val; + } + } + 2 => { + let len = rng.below(128); + let from = rng.below(v.len().saturating_sub(len).max(1)); + let to = rng.below(v.len().saturating_sub(len).max(1)); + if from + len <= v.len() && to + len <= v.len() { + let chunk = v[from..from + len].to_vec(); + v[to..to + len].copy_from_slice(&chunk); + } + } + _ => { + let end = 0x400.min(v.len()); + for b in &mut v[..end] { + if rng.below(16) == 0 { + *b = (rng.next() & 0xff) as u8; + } + } + } + } + } + one_case( + &v, + &opts, + &format!("{} random seed {:#x}", seed.name, rng.0), + ); + cases += 1; + } + assert!( + cases > 200, + "only {cases} random cases in {RANDOM_BUDGET:?}" + ); + println!( + "nopanic gate: {cases} random-mutation cases in {:?}", + started.elapsed() + ); +} + +/// Every architecture's decoder, on bytes nobody chose. +/// +/// `e5r-arch` has its own fuzz test per decoder; this one drives them through +/// the public `decode(&Arch, ..)` dispatcher for every architecture at once, +/// so an architecture added to the enum and not to the dispatcher shows up as +/// a gap here rather than nowhere. +#[test] +fn every_architecture_decodes_arbitrary_bytes() { + let arches = [Arch::X86, Arch::X86_64, Arch::Arm, Arch::AArch64]; + let mut rng = Rng(0xdec0_de00_0000_0001); + let started = Instant::now(); + let mut cases = 0u64; + let mut decoded = [0u64; 4]; + let mut buf = [0u8; 32]; + while started.elapsed() < DECODE_BUDGET { + for chunk in buf.chunks_mut(8) { + chunk.copy_from_slice(&rng.next().to_le_bytes()); + } + // Occasionally a run of poison, because a decoder's immediate and + // displacement arithmetic is where a saturated field lands. + if rng.below(4) == 0 { + let at = rng.below(buf.len()); + let val = POISON[rng.below(POISON.len())]; + for b in &mut buf[at..] { + *b = val; + } + } + // Every length from one byte up, so a decoder reading past a short + // buffer is caught rather than being handed 16 bytes every time. + let len = 1 + rng.below(buf.len()); + for (k, arch) in arches.iter().enumerate() { + let case = Instant::now(); + if let Some(insn) = decode(arch, &buf[..len], Addr(0x40_0000)) { + assert!( + insn.len > 0, + "{arch:?} decoded {:02x?} to a zero-length instruction", + &buf[..len] + ); + assert!( + insn.len as usize <= len, + "{arch:?} decoded {} bytes from {len}: {:02x?}", + insn.len, + &buf[..len] + ); + assert!( + insn.len as u64 <= arch.max_insn_len(), + "{arch:?} decoded {} bytes, over the architecture maximum", + insn.len + ); + // Formatting is the other half of the decoder's surface and + // has its own indexing. + let _ = e5r_arch::format(arch, &insn, true); + let _ = e5r_arch::format(arch, &insn, false); + decoded[k] += 1; + } + assert!( + case.elapsed() < PER_CASE, + "{arch:?} took {:?} on {:02x?}", + case.elapsed(), + &buf[..len] + ); + cases += 1; + } + } + assert!( + cases > 20_000, + "only {cases} decode cases in {DECODE_BUDGET:?}" + ); + for (k, arch) in arches.iter().enumerate() { + assert!( + decoded[k] > 0, + "{arch:?} decoded nothing at all, so the dispatcher is not reaching it" + ); + } + println!( + "nopanic gate: {cases} decode cases across {} architectures in {:?}, {decoded:?} accepted", + arches.len(), + started.elapsed() + ); +} + +/// The whole gate's budget, asserted rather than described. +/// +/// Each phase has its own clock, so this catches the case where a phase's +/// inner loop stops checking its own budget. +#[test] +fn the_gate_fits_its_budget() { + // The four phases above run as separate tests, possibly in parallel, so + // this cannot time them. What it can do is state the sum, so that raising + // one phase without lowering another fails here. + let total = SWEEP_BUDGET + TRUNCATE_BUDGET + RANDOM_BUDGET + DECODE_BUDGET; + assert!( + total <= BUDGET, + "phase budgets total {total:?}, over the stated {BUDGET:?}" + ); +} + +/// A 64-bit Mach-O relocatable with `nsects` sections, each claiming `size` +/// bytes at file offset `0x1000`, in a file that is only a few hundred bytes. +/// +/// Built rather than mutated: the sweep above reaches this shape by accident, +/// and a defect needs bytes somebody can paste into a bug report. +fn tiny_macho(nsects: u32, size: u64) -> Vec { + let mut secs = Vec::new(); + for i in 0..nsects { + let mut name = format!("__text{i:02}").into_bytes(); + name.resize(16, 0); + secs.extend_from_slice(&name); + let mut seg = b"__TEXT".to_vec(); + seg.resize(16, 0); + secs.extend_from_slice(&seg); + secs.extend_from_slice(&0u64.to_le_bytes()); // addr + secs.extend_from_slice(&size.to_le_bytes()); // size + secs.extend_from_slice(&0x1000u32.to_le_bytes()); // offset + secs.extend_from_slice(&4u32.to_le_bytes()); // align + secs.extend_from_slice(&0u32.to_le_bytes()); // reloff + secs.extend_from_slice(&0u32.to_le_bytes()); // nreloc + secs.extend_from_slice(&0x8000_0400u32.to_le_bytes()); // S_ATTR_PURE_INSTRUCTIONS + secs.extend_from_slice(&[0u8; 12]); // reserved1..3 + } + let mut seg = Vec::new(); + seg.extend_from_slice(&0x19u32.to_le_bytes()); // LC_SEGMENT_64 + seg.extend_from_slice(&(72 + secs.len() as u32).to_le_bytes()); + seg.extend_from_slice(&[0u8; 16]); // segname + seg.extend_from_slice(&[0u8; 32]); // vmaddr vmsize fileoff filesize + seg.extend_from_slice(&7u32.to_le_bytes()); // maxprot + seg.extend_from_slice(&7u32.to_le_bytes()); // initprot + seg.extend_from_slice(&nsects.to_le_bytes()); + seg.extend_from_slice(&0u32.to_le_bytes()); // flags + seg.extend_from_slice(&secs); + + let mut out = Vec::new(); + out.extend_from_slice(&0xfeed_facfu32.to_le_bytes()); // MH_MAGIC_64 + out.extend_from_slice(&0x0100_0007u32.to_le_bytes()); // CPU_TYPE_X86_64 + out.extend_from_slice(&3u32.to_le_bytes()); // cpusubtype + out.extend_from_slice(&1u32.to_le_bytes()); // MH_OBJECT + out.extend_from_slice(&1u32.to_le_bytes()); // ncmds + out.extend_from_slice(&(seg.len() as u32).to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); // flags + out.extend_from_slice(&0u32.to_le_bytes()); // reserved + out.extend_from_slice(&seg); + out +} + +/// A Mach-O section that runs off the end of the file is reported. +/// +/// It used to load silently: the failed section-body read became an empty +/// default, so the `Section` kept a `file_size` of 246 GB while the segment +/// held nothing and `Object::warnings` was empty. A caller trusting +/// `file_size` got a wrong answer with nothing to notice, and the ELF loader +/// had always warned at the same point. Found by the no-panic gate. +#[test] +fn a_macho_section_off_the_end_is_reported() { + let bytes = tiny_macho(2, 0x39_3939_3939); + let obj = e5r_format::load(&bytes, &LoadOptions::default()) + .expect("a 224-byte Mach-O with two absurd sections still loads"); + let s = &obj.sections[0]; + assert!( + s.file_offset + s.file_size > bytes.len() as u64, + "the section no longer runs off the end; the fixture drifted" + ); + // It used to load silently, so a caller trusting `file_size` got a wrong + // answer with nothing to notice. The ELF loader has always warned here. + assert!( + obj.warnings.iter().any(|w| w.contains("section body")), + "a section whose bytes are not in the file must be reported; \ + warnings were {:?}", + obj.warnings + ); +} + +/// The Mach-O loader's synthesized layout for a relocatable object stays +/// inside the address space. +/// +/// `crates/e5r-format/src/macho.rs` lays sections out with +/// `next_free = a + size` where `size` came from the file. Three sections of +/// `0x9393939393939393` bytes wrap `next_free` back to a low address, so the +/// It used to lay sections out with `next_free = a + size`, where the size is +/// the file's to choose. Three sections large enough to wrap put the third +/// *below* the first, so the object claimed one section contained another's +/// code. In release that was a silent wrong answer; under the overflow checks +/// the dev profile builds with, it was a panic, which is what makes it a +/// no-panic-gate finding rather than a cosmetic one. +#[test] +fn a_macho_relocatable_layout_stays_inside_the_address_space() { + let bytes = tiny_macho(3, 0x9393_9393_9393_9393); + assert!(bytes.len() < 512, "the reproduction is meant to be tiny"); + let obj = + e5r_format::load(&bytes, &LoadOptions::default()).expect("a 344-byte Mach-O still loads"); + let mut ranges: Vec<_> = obj + .sections + .iter() + .filter(|s| !s.range.is_empty()) + .map(|s| s.range) + .collect(); + ranges.sort_by_key(|r| r.start()); + // The synthesized layout used to wrap, putting a later section below an + // earlier one, so the object claimed a section contained another's code. + assert!( + !ranges.windows(2).any(|w| w[0].overlaps(w[1])), + "the synthesized layout overlaps itself: {:#x?}", + ranges + .iter() + .map(|r| (r.start().get(), r.end().get())) + .collect::>() + ); +} + +/// `symbol_at` compares a distance rather than a sum, because the size comes +/// from the file. +/// +/// A symbol high in the address space with a size of `0xffff_ffff` made +/// `addr < s.addr + s.size` overflow, which is a panic under the overflow +/// checks the dev profile builds with. The poison sweep reached it from a real +/// fixture: one run of `0xff` over the symbol table of `wide.x64.O2.o` +/// produces a symbol at `0xffff_ffff_0010_08c8` with size `0xffff_ffff`. +#[test] +fn symbol_at_adds_a_file_chosen_size_unchecked() { + let opts = LoadOptions::default(); + let Some(dir) = corpus_dir() else { + println!("nopanic gate: no fixtures/build. Skipped."); + return; + }; + let Ok(full) = std::fs::read(dir.join("wide.x64.O2.o")) else { + println!("nopanic gate: no wide.x64.O2.o. Skipped."); + return; + }; + let mut bad = full[..full.len().min(PREFIX)].to_vec(); + let at = 0x24c4; + assert!( + at + 8 <= bad.len(), + "the fixture shrank; the offset drifted" + ); + for b in &mut bad[at..at + 8] { + *b = 0xff; + } + let Ok(obj) = e5r_format::load(&bad, &opts) else { + println!("nopanic gate: the fixture changed shape and no longer loads. Skipped."); + return; + }; + let wrapping = obj + .symbols + .iter() + .find(|s| s.addr.get().checked_add(s.size).is_none()); + let Some(sym) = wrapping else { + println!( + "nopanic gate: no symbol wraps any more; if symbol_at is now checked, \ + delete this test. Skipped." + ); + return; + }; + // The sum that `symbol_at` performs. Computed here rather than by calling + // it, because in this release build the call wraps silently and proves + // nothing; the point is that the operands exist. + assert!( + sym.addr.get().checked_add(sym.size).is_none(), + "symbol {:?} at {:#x} size {:#x}", + sym.name, + sym.addr.get(), + sym.size + ); + // `obj.symbol_at(sym.addr)` is deliberately not called. It is the thing + // that overflows, and calling it would turn the dev-profile CI build red + // on a defect this gate is reporting rather than fixing. + println!( + "nopanic gate: symbol_at would overflow on {:?} at {:#x} size {:#x}", + sym.name, + sym.addr.get(), + sym.size + ); +} diff --git a/crates/e5r-ir/tests/oracle.rs b/crates/e5r-ir/tests/oracle.rs new file mode 100644 index 0000000..c171a93 --- /dev/null +++ b/crates/e5r-ir/tests/oracle.rs @@ -0,0 +1,314 @@ +//! The lifters measured against the machine, for real. +//! +//! One program is compiled for both architectures and executed: natively on +//! this host, and under qemu for the other one. The same calls are then lifted +//! and interpreted, and the answers have to match. Nothing here is an +//! assertion about what the lifter ought to do; every expected value came out +//! of a processor. +//! +//! The case table is shared with the driver that produced those answers, so a +//! case cannot exist on one side and not the other. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::{Addr, Arch}; +use e5r_format::LoadOptions; +use e5r_ir::exec::run_with; +use e5r_ir::{Machine, Stop}; + +/// Where a fresh machine's stack lives, away from the image. +const STACK: u64 = 0x7fff_0000; +/// The return address a called function will pop, which stops the run. +const SENTINEL: u64 = 0xdead_0000; +/// How deep the interpreter follows calls, which recursion needs. +const DEPTH: u32 = 4096; + +fn build_dir() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +/// One line of the case table. +struct Case { + name: String, + kind: String, + args: Vec, +} + +enum Arg { + Number(u64), + /// A floating argument, as its bit pattern and its width. + Float(u64, u8), + Symbol(String), +} + +fn cases() -> Vec { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/portable/cases.txt"); + let text = std::fs::read_to_string(path).unwrap_or_default(); + let mut out = Vec::new(); + for line in text.lines() { + let line = line.split('#').next().unwrap_or("").trim(); + if line.is_empty() { + continue; + } + let mut parts = line.split_whitespace(); + let head = parts.next().unwrap(); + let (name, kind) = head.split_once(':').unwrap_or((head, "u64")); + let args = parts + .map(|t| match t.strip_prefix('&') { + Some(s) => Arg::Symbol(s.to_string()), + None => parse_argument(t), + }) + .collect(); + out.push(Case { + name: name.to_string(), + kind: kind.to_string(), + args, + }); + } + out +} + +/// A floating literal is one with a point or an exponent; a trailing `f` makes +/// it single precision, exactly as the C the driver was built from reads it. +fn parse_argument(t: &str) -> Arg { + let single = t.ends_with('f') && !t.starts_with("0x"); + let body = if single { &t[..t.len() - 1] } else { t }; + let floating = + !body.starts_with("0x") && (body.contains('.') || body.contains('e') || body.contains('E')); + if floating { + if let Ok(v) = body.parse::() { + return if single { + Arg::Float((v as f32).to_bits() as u64, 4) + } else { + Arg::Float(v.to_bits(), 8) + }; + } + } + Arg::Number(parse_number(t)) +} + +fn parse_number(t: &str) -> u64 { + if let Some(hex) = t.strip_prefix("0x") { + u64::from_str_radix(hex, 16).unwrap_or(0) + } else if let Some(neg) = t.strip_prefix('-') { + (neg.parse::().unwrap_or(0)).wrapping_neg() as u64 + } else { + t.parse::().unwrap_or(0) + } +} + +/// The value the driver would have written, given what the return register +/// holds: the C cast to `u64` widens by the function's own return type. +fn widen(raw: u64, kind: &str) -> u64 { + let (bits, signed) = match kind { + // A floating result is compared as the bits the register holds, which + // the driver recorded the same way. + "f32" => (32, false), + "f64" => return raw, + "u8" => (8, false), + "i8" => (8, true), + "u16" => (16, false), + "i16" => (16, true), + "u32" => (32, false), + "i32" => (32, true), + "i64" => (64, true), + _ => (64, false), + }; + if bits == 64 { + return raw; + } + let truncated = raw & ((1u64 << bits) - 1); + if signed && truncated >> (bits - 1) == 1 { + truncated | !((1u64 << bits) - 1) + } else { + truncated + } +} + +/// Set up the machine for a call and run it, returning the result register. +/// +/// Integer and floating arguments have separate sequences of registers, which +/// is how both calling conventions work. +fn call(p: &Program, entry: Addr, args: &[Value], float_result: bool) -> (u64, e5r_ir::Outcome) { + let mut m = Machine::over(&p.object.memory); + m.budget = 1 << 24; + // Real storage under the stack pointer, so a spill and its reload agree. + m.write_mem(STACK - 0x8000, &[0u8; 0x10000]); + match p.object.arch { + Arch::AArch64 => { + use e5r_ir::lift::aarch64::{gpr_offset, sp_offset, vec_offset}; + let (mut n, mut f) = (0u8, 0u8); + for a in args { + match a { + Value::Int(v) if n < 8 => { + m.set_reg(gpr_offset(n), 8, *v); + n += 1; + } + Value::Int(_) => {} + Value::Float(v, size) if f < 8 => { + m.set_reg(vec_offset(f), *size, *v); + f += 1; + } + Value::Float(..) => {} + } + } + m.set_reg(sp_offset(), 8, STACK); + // The link register holds an address that is not code, so the + // outermost return stops rather than running on. + m.set_reg(gpr_offset(30), 8, SENTINEL); + let outcome = run_with(&mut m, &p.object.memory, &p.object.arch, entry, DEPTH); + let result = if float_result { + m.reg(vec_offset(0), 8) + } else { + m.reg(gpr_offset(0), 8) + }; + (result, outcome) + } + _ => { + use e5r_ir::lift::x86::{gpr_offset, sp_offset, vec_offset}; + // rdi, rsi, rdx, rcx, r8, r9 for integers, xmm0 onward for floats. + const ORDER: [u8; 6] = [7, 6, 2, 1, 8, 9]; + let sp = STACK; + m.set_reg(sp_offset(), 8, sp); + m.write_mem(sp, &SENTINEL.to_le_bytes()); + let (mut n, mut f, mut stacked) = (0usize, 0u8, 0u64); + for a in args { + match a { + Value::Int(v) if n < 6 => { + m.set_reg(gpr_offset(ORDER[n]), 8, *v); + n += 1; + } + Value::Int(v) => { + m.write_mem(sp + 8 + stacked * 8, &v.to_le_bytes()); + stacked += 1; + } + Value::Float(v, size) if f < 8 => { + m.set_reg(vec_offset(f), *size, *v); + f += 1; + } + Value::Float(..) => {} + } + } + let outcome = run_with(&mut m, &p.object.memory, &p.object.arch, entry, DEPTH); + let result = if float_result { + m.reg(vec_offset(0), 8) + } else { + m.reg(gpr_offset(0), 8) + }; + (result, outcome) + } + } +} + +/// An argument as the machine takes it. +#[derive(Debug, Clone, Copy)] +enum Value { + Int(u64), + Float(u64, u8), +} + +fn load(name: &str) -> Option { + let data = std::fs::read(build_dir()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +fn symbols(p: &Program) -> BTreeMap { + p.object + .symbols + .iter() + .map(|s| (s.name.clone(), s.addr)) + .collect() +} + +/// Run every case against the recorded output of the real program. +fn check(binary: &str) { + let Some(p) = load(binary) else { return }; + let Ok(recorded) = std::fs::read(build_dir().unwrap().join(format!("{binary}.out"))) else { + return; + }; + let cases = cases(); + assert_eq!( + recorded.len(), + cases.len() * 8, + "{binary}: the recording has {} values and the table has {}", + recorded.len() / 8, + cases.len() + ); + let table = symbols(&p); + + let mut wrong = Vec::new(); + let mut incomplete = Vec::new(); + for (n, case) in cases.iter().enumerate() { + let Some(entry) = table.get(&case.name) else { + incomplete.push(format!("{}: no symbol", case.name)); + continue; + }; + let args: Vec = case + .args + .iter() + .map(|a| match a { + Arg::Number(v) => Value::Int(*v), + Arg::Float(v, size) => Value::Float(*v, *size), + Arg::Symbol(s) => Value::Int(table.get(s).map(|a| a.get()).unwrap_or(0)), + }) + .collect(); + let expected = u64::from_le_bytes(recorded[n * 8..n * 8 + 8].try_into().unwrap()); + let floating = case.kind.starts_with('f') && case.kind != "flag"; + let (raw, outcome) = call(&p, *entry, &args, floating); + if outcome.stop != Stop::Returned { + incomplete.push(format!( + "{}({:?}) stopped with {:?}, unlifted {:?}", + case.name, + args, + outcome.stop, + &outcome.unlifted[..outcome.unlifted.len().min(3)] + )); + continue; + } + let got = widen(raw, &case.kind); + if got != expected { + wrong.push(format!( + "{}({args:?}) = {got:#x}, the machine says {expected:#x}", + case.name + )); + } + } + + assert!( + wrong.is_empty(), + "{binary}: {} of {} cases disagree with the processor:\n {}", + wrong.len(), + cases.len(), + wrong.join("\n ") + ); + // A case that did not finish is a gap rather than a wrong answer, but the + // gate is the same: this corpus is the one the lifter is expected to cover. + assert!( + incomplete.is_empty(), + "{binary}: {} of {} cases did not run to completion:\n {}", + incomplete.len(), + cases.len(), + incomplete.join("\n ") + ); +} + +/// Every optimization level of both architectures, because a vectorizer and a +/// scheduler reach different corners of the instruction set than a plain +/// translation does. +#[test] +fn aarch64_lifting_matches_the_processor() { + for opt in ["O0", "O1", "O2", "O3", "Os"] { + check(&format!("driver.a64.{opt}")); + } +} + +#[test] +fn x86_lifting_matches_the_processor() { + for opt in ["O0", "O1", "O2", "O3", "Os"] { + check(&format!("driver.x64.{opt}")); + } +} diff --git a/crates/e5r-ir/tests/packed_shift.rs b/crates/e5r-ir/tests/packed_shift.rs new file mode 100644 index 0000000..25eec9f --- /dev/null +++ b/crates/e5r-ir/tests/packed_shift.rs @@ -0,0 +1,217 @@ +//! The packed shifts with a register count, checked against a processor. +//! +//! Shifting a vector lane by a count held in another vector has a rule the +//! arithmetic does not: a count at or above the lane's width gives zero rather +//! than wrapping. The lifter writes that out as a mask, and a mask derived +//! from a manual is an assertion until something runs it. +//! +//! This host is aarch64, so the x86-64 side runs under qemu, which is how the +//! rest of the lifter's hardware oracle already works. + +use std::process::Command; + +use e5r_core::{Addr, Arch}; +use e5r_ir::interp::{Step, step}; +use e5r_ir::{Machine, lift}; + +/// Counts either side of every lane width that matters, so the rule is +/// exercised in range, at the boundary and past it. +const COUNTS: [u64; 8] = [0, 1, 15, 16, 31, 32, 63, 64]; + +fn tool(name: &str) -> Option { + Command::new(name) + .arg("--version") + .output() + .ok() + .filter(|o| o.status.success()) + .map(|_| name.to_string()) +} + +/// Build a program that runs one instruction per count and prints both lanes. +/// +/// Written as inline assembly rather than an intrinsic so the instruction +/// under test is the one that runs, not whatever a header expands to. +fn on_hardware(insn: &str, value: u64) -> Option> { + // Inside inline assembly a register's percent has to be doubled, while + // the assembler wants it single, so one form is written and doubled here. + let insn = insn.replace('%', "%%"); + let insn = insn.as_str(); + let cc = tool("clang")?; + let qemu = tool("qemu-x86_64").or_else(|| tool("qemu-x86_64-static"))?; + // A test runs with its crate as the working directory, not the workspace. + let lld = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build/ld"); + if !lld.join("ld.lld").exists() { + return None; // the corpus has not been built + } + let dir = std::env::temp_dir().join(format!("e5r-psh-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("m.c"); + let bin = dir.join("m"); + // Freestanding, because this host has no x86-64 libc headers: the results + // go out through a raw write and the program exits with a raw syscall. + let mut body = String::from( + "typedef unsigned long long u64;\n\ + /* -O0 turns an array initializer into a call to this, and there is\n\ + no library here to provide it. */\n\ + void *memset(void *d, int c, unsigned long n) {\n\ + unsigned char *p = d;\n\ + while (n--) *p++ = (unsigned char)c;\n\ + return d;\n\ + }\n\ + static void put(const void *p, u64 n) {\n\ + __asm__ volatile(\"syscall\" :: \"a\"(1ULL), \"D\"(1ULL), \"S\"(p), \"d\"(n)\n\ + : \"rcx\", \"r11\", \"memory\");\n\ + }\n\ + void _start(void) {\n\ + u64 out[2];\n", + ); + for c in COUNTS { + body.push_str(&format!( + " {{ u64 v[2] = {{ {value}ULL, {value}ULL }}, k[2] = {{ {c}ULL, 0 }};\n\ + \x20 __asm__ volatile(\"movdqu %2, %%xmm0\\n\\tmovdqu %3, %%xmm1\\n\\t{insn}\\n\\tmovdqu %%xmm0, %0\\n\\tmovdqu %%xmm0, %1\"\n\ + \x20 : \"=m\"(out[0]), \"=m\"(out[1]) : \"m\"(v[0]), \"m\"(k[0]) : \"xmm0\", \"xmm1\", \"memory\");\n\ + \x20 put(out, 16); }}\n" + )); + } + body.push_str( + " __asm__ volatile(\"syscall\" :: \"a\"(60ULL), \"D\"(0ULL));\n\ + }\n", + ); + std::fs::write(&src, body).ok()?; + let built = Command::new(&cc) + .args([ + "--target=x86_64-unknown-linux-gnu", + "-O0", + "-static", + "-nostdlib", + "-ffreestanding", + ]) + .arg(format!("-B{}", lld.display())) + .arg("-fuse-ld=lld") + .arg("-o") + .arg(&bin) + .arg(&src) + .output() + .ok()?; + if !built.status.success() { + return None; + } + let ran = Command::new(&qemu).arg(&bin).output().ok()?; + let _ = std::fs::remove_dir_all(&dir); + // Two little endian words per case, written straight out. + let raw = ran.stdout; + if raw.len() != COUNTS.len() * 16 { + return None; + } + Some( + raw.chunks_exact(16) + .map(|c| { + ( + u64::from_le_bytes(c[..8].try_into().unwrap()), + u64::from_le_bytes(c[8..].try_into().unwrap()), + ) + }) + .collect(), + ) +} + +/// Assemble one x86-64 instruction and hand back its bytes. +fn assemble(text: &str) -> Option> { + let cc = tool("clang")?; + let dir = std::env::temp_dir().join(format!("e5r-pshasm-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("a.s"); + let obj = dir.join("a.o"); + std::fs::write(&src, format!(".text\n{text}\n")).ok()?; + let ok = Command::new(&cc) + .args(["--target=x86_64-unknown-linux-gnu", "-c", "-o"]) + .arg(&obj) + .arg(&src) + .status() + .ok()?; + if !ok.success() { + return None; + } + let out = Command::new("llvm-objdump-18") + .args(["-d", "--x86-asm-syntax=intel"]) + .arg(&obj) + .output() + .or_else(|_| { + Command::new("llvm-objdump") + .args(["-d", "--x86-asm-syntax=intel"]) + .arg(&obj) + .output() + }) + .ok()?; + let text = String::from_utf8_lossy(&out.stdout); + // " 0: 66 0f d3 c1 \tpsrlq\txmm0, xmm1": the bytes sit between the + // colon and the first tab. + let hex = text.lines().find_map(|l| { + let after = l.split_once(':')?.1.split('\t').next()?.trim(); + // The header line also has a colon and nothing useful after it. + (!after.is_empty()).then_some(after) + })?; + let _ = std::fs::remove_dir_all(&dir); + hex.split_whitespace() + .map(|b| u8::from_str_radix(b, 16).ok()) + .collect() +} + +/// Lift one instruction, set xmm0 to `value` in both lanes and xmm1 to the +/// count, run it, and read both lanes of xmm0 back. +fn interpreted(bytes: &[u8], value: u64, count: u64) -> Option<(u64, u64)> { + let insn = e5r_arch::x86::decode(bytes, Addr(0x1000))?; + let lifted = lift::x86::lift(&insn); + assert!( + lifted.complete, + "{} is not modelled", + e5r_arch::format(&Arch::X86_64, &insn, false) + ); + let mut m = Machine::new(); + let v = lift::x86::vec_offset(0); + let k = lift::x86::vec_offset(1); + m.set_reg(v, 8, value); + m.set_reg(v + 8, 8, value); + m.set_reg(k, 8, count); + m.set_reg(k + 8, 8, 0); + for op in &lifted.ops { + if !matches!(step(&mut m, op), Step::Next) { + return None; + } + } + Some((m.reg(v, 8), m.reg(v + 8, 8))) +} + +#[test] +fn a_packed_shift_by_a_register_does_what_the_processor_does() { + const VALUE: u64 = 0x0123_4567_89ab_cdef; + let cases = [ + ("psrlq %xmm1, %xmm0", "psrlq xmm0, xmm1"), + ("psllq %xmm1, %xmm0", "psllq xmm0, xmm1"), + ("psrld %xmm1, %xmm0", "psrld xmm0, xmm1"), + ("pslld %xmm1, %xmm0", "pslld xmm0, xmm1"), + ("psrlw %xmm1, %xmm0", "psrlw xmm0, xmm1"), + ("psllw %xmm1, %xmm0", "psllw xmm0, xmm1"), + ]; + let mut checked = 0; + for (at_and_t, intel) in cases { + let Some(bytes) = assemble(at_and_t) else { + return; // no assembler or no objdump here + }; + let Some(real) = on_hardware(at_and_t, VALUE) else { + return; // no cross compiler, linker or qemu here + }; + assert_eq!(real.len(), COUNTS.len(), "{intel}: the harness lost a case"); + for (count, want) in COUNTS.iter().zip(&real) { + let got = interpreted(&bytes, VALUE, *count).expect("the interpreter stopped"); + assert_eq!( + got.0, want.0, + "{intel} by {count}: the processor says {:#x}, the lifter says {:#x}", + want.0, got.0 + ); + checked += 1; + } + } + assert!(checked >= 40, "only {checked} comparisons ran"); + eprintln!("{checked} packed shift results match the processor"); +} diff --git a/crates/e5r-ir/tests/reciprocal.rs b/crates/e5r-ir/tests/reciprocal.rs new file mode 100644 index 0000000..274fbde --- /dev/null +++ b/crates/e5r-ir/tests/reciprocal.rs @@ -0,0 +1,440 @@ +//! Reciprocal division and remainder, recovered and checked. +//! +//! A compiler never divides by a constant: it multiplies by a fixed point +//! reciprocal and shifts, and only the magic number survives into the machine +//! code. `opt::divisions` recovers the divisor from it, and a divisor that is +//! wrong by one is a program that is silently wrong. So the recovery is tested +//! two ways that do not share an argument: against the true quotient at every +//! dividend of a narrow width, which says the closed form verifier is right, +//! and against the magic numbers an independent implementation of Granlund and +//! Montgomery's algorithm produces for a thousand divisors, which says the +//! recovery finds the divisor a compiler actually meant. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::Addr; +use e5r_format::LoadOptions; +use e5r_ir::op::Op; +use e5r_ir::opt::{signed_divisor, unsigned_divisor}; +use e5r_ir::ssa::{SsaFunction, SsaKind}; +use e5r_ir::{func, opt, ssa, stack}; + +/// The unsigned magic number, its shift, and whether the sequence needs the +/// dividend added back, for a 32-bit divisor. +/// +/// Hacker's Delight figure 10-3, which is Granlund and Montgomery's algorithm +/// written out. It is here rather than called into the crate on purpose: a test +/// that generated its inputs with the code under test would only say the code +/// agrees with itself. +/// +/// Spelled with `wrapping_*` throughout. The figure is written in C, where +/// unsigned arithmetic is modular by definition and the algorithm leans on +/// that -- `2 * q1 + 1` is meant to carry out of the word. Rust's `*` wraps +/// only in release, so the plain operators made this a transcription that was +/// right on one profile and a panic on the other. +fn magicu(d: u32) -> (u32, bool, u32) { + assert!(d >= 3); + let nc = u32::MAX - (0u32.wrapping_sub(d) % d); + let mut add = false; + let (mut p, mut q1, mut r1) = (31u32, 0x8000_0000u32 / nc, 0x8000_0000u32 % nc); + let (mut q2, mut r2) = (0x7fff_ffffu32 / d, 0x7fff_ffffu32 % d); + loop { + p += 1; + if r1 >= nc.wrapping_sub(r1) { + q1 = q1.wrapping_mul(2).wrapping_add(1); + r1 = r1.wrapping_mul(2).wrapping_sub(nc); + } else { + q1 = q1.wrapping_mul(2); + r1 = r1.wrapping_mul(2); + } + if r2.wrapping_add(1) >= d.wrapping_sub(r2) { + add |= q2 >= 0x7fff_ffff; + q2 = q2.wrapping_mul(2).wrapping_add(1); + r2 = r2.wrapping_mul(2).wrapping_add(1).wrapping_sub(d); + } else { + add |= q2 >= 0x8000_0000; + q2 = q2.wrapping_mul(2); + r2 = r2.wrapping_mul(2).wrapping_add(1); + } + let delta = d.wrapping_sub(1).wrapping_sub(r2); + if p >= 64 || !(q1 < delta || (q1 == delta && r1 == 0)) { + break; + } + } + (q2.wrapping_add(1), add, p - 32) +} + +/// The signed magic number and its shift, for a 32-bit divisor. +/// +/// Hacker's Delight figure 10-1. The multiplier comes back as an unsigned +/// pattern: when a compiler stores a reciprocal that does not fit in a signed +/// word it keeps the low half and adds the dividend back, and the exact +/// multiplier that sequence computes with is the pattern read unsigned. +fn magics(d: u32) -> (u32, u32) { + assert!(d >= 3); + let two31: u32 = 0x8000_0000; + let ad = d; + let t = two31 - 1; + let anc = t - 1 - t % ad; + let (mut p, mut q1, mut r1) = (31u32, two31 / anc, two31 % anc); + let (mut q2, mut r2) = (two31 / ad, two31 % ad); + loop { + p += 1; + q1 = q1.wrapping_mul(2); + r1 = r1.wrapping_mul(2); + if r1 >= anc { + q1 = q1.wrapping_add(1); + r1 -= anc; + } + q2 = q2.wrapping_mul(2); + r2 = r2.wrapping_mul(2); + if r2 >= ad { + q2 += 1; + r2 -= ad; + } + let delta = ad - r2; + if q1 >= delta || (q1 == delta && r1 == 0) { + break; + } + } + (q2.wrapping_add(1), p - 32) +} + +/// Every divisor a compiler would turn into a reciprocal comes back as itself. +/// +/// Three to a thousand rather than the seven the fixture happens to contain: +/// the recovery is arithmetic, so the cost of checking a thousand of them is +/// nothing and a divisor it is wrong about would otherwise wait for a fixture +/// that names it. +#[test] +fn every_unsigned_divisor_to_a_thousand_is_recovered() { + for d in 3u32..=1000 { + let (magic, add, s) = magicu(d); + // Without the add the sequence is one multiply and one shift. With it + // the reciprocal needed a bit more than the multiply holds, and the + // dividend comes back in a halving add, so the exact multiplier is the + // stored one plus the bit that did not fit. + let (m, shift) = match add { + false => (magic as u64, 32 + s), + true => (magic as u64 + (1u64 << 32), 32 + s), + }; + assert_eq!( + unsigned_divisor(m, shift, u32::MAX as u64), + Some(d as u64), + "u32 / {d}: magic {magic:#x}, add {add}, shift {shift}" + ); + } +} + +/// The same for the signed sequence. +#[test] +fn every_signed_divisor_to_a_thousand_is_recovered() { + for d in 3u32..=1000 { + let (magic, s) = magics(d); + assert_eq!( + signed_divisor(magic as u64, 32 + s, 32), + Some(d as u64), + "i32 / {d}: magic {magic:#x}, shift {}", + 32 + s + ); + } +} + +/// An ordinary shift stays a shift. +/// +/// `x / 8` on an unsigned value is one shift and no reciprocal, and printing it +/// as a division would bury whatever bit manipulation the code is really doing. +/// Nothing without a multiply in front of it is a candidate: a multiplier of +/// one is refused outright, and the fixture's plain shift keeps its shape. +#[test] +fn a_plain_shift_is_not_read_as_a_division() { + for shift in 1u32..64 { + assert_eq!(unsigned_divisor(1, shift, u32::MAX as u64), None); + assert_eq!(unsigned_divisor(0, shift, u32::MAX as u64), None); + } + for build in arith_builds() { + let Some(p) = open(&build) else { continue }; + let Some(s) = optimized(&p, "divu8") else { + panic!("divu8: not recovered from {build}"); + }; + let found = quotients(&s); + assert!( + found.is_empty(), + "divu8 in {build}: a shift was read as a division: {found:?}" + ); + } +} + +/// Whatever the recovery returns really is the quotient, at every dividend. +/// +/// The verifier inside `unsigned_divisor` evaluates a handful of points and +/// argues that the rest cannot be worse. That argument is what is checked here, +/// by running the sequence on all 65,536 dividends of a sixteen-bit range for a +/// wide sweep of multipliers and shifts, valid ones and deliberately broken +/// ones alike. A magic it accepts must agree everywhere; one it refuses costs +/// only an unfolded multiply. +#[test] +fn an_accepted_divisor_agrees_at_every_dividend() { + const MAX: u64 = u16::MAX as u64; + let mut accepted = 0usize; + for d in 3u64..=600 { + for shift in 16u32..=34 { + let exact = (1u128 << shift).div_ceil(d as u128); + // The reciprocal itself, and its neighbours, which are the wrong + // answers a formula that did not check would hand back. + for delta in [-2i128, -1, 0, 1, 2] { + let Some(m) = (exact as i128).checked_add(delta) else { + continue; + }; + if m <= 1 || m > u64::MAX as i128 { + continue; + } + let Some(found) = unsigned_divisor(m as u64, shift, MAX) else { + continue; + }; + accepted += 1; + for x in 0..=MAX { + let got = ((x as u128 * m as u128) >> shift) as u64; + assert_eq!( + got, + x / found, + "magic {m:#x} shift {shift} was read as / {found}, but at {x} the \ + sequence gives {got} and the division gives {}", + x / found + ); + } + } + } + } + assert!(accepted > 1000, "only {accepted} magic(s) were accepted"); +} + +/// The signed recovery, checked the same way over a sixteen-bit range. +#[test] +fn an_accepted_signed_divisor_agrees_at_every_dividend() { + let mut accepted = 0usize; + for d in 3u64..=600 { + for shift in 16u32..=34 { + let exact = (1u128 << shift).div_ceil(d as u128); + for delta in [-2i128, -1, 0, 1, 2] { + let Some(m) = (exact as i128).checked_add(delta) else { + continue; + }; + if m <= 1 || m > u64::MAX as i128 { + continue; + } + let Some(found) = signed_divisor(m as u64, shift, 16) else { + continue; + }; + accepted += 1; + for x in i16::MIN as i64..=i16::MAX as i64 { + let product = (x as i128 * m) >> shift; + let got = product + i128::from(x < 0); + assert_eq!( + got, + (x / found as i64) as i128, + "magic {m:#x} shift {shift} was read as / {found}, and disagrees at {x}" + ); + } + } + } + } + assert!(accepted > 500, "only {accepted} magic(s) were accepted"); +} + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + Some(analyze(obj, &Options::default())) +} + +/// One function of a fixture, lifted and optimized the way the decompiler gets +/// it. +fn optimized(p: &Program, name: &str) -> Option { + let f = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(name))?; + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + stack::promote(&mut ir); + let mut s = ssa::build(&ir); + opt::optimize(&mut s); + Some(s) +} + +/// Every division or remainder in a function, as the operation and its divisor. +fn quotients(s: &SsaFunction) -> Vec<(Op, u64)> { + let mut out = Vec::new(); + for b in s.blocks.values() { + for op in &b.ops { + let SsaKind::Op(o) = op.kind else { continue }; + if !matches!(o, Op::IntDiv | Op::IntSDiv | Op::IntRem | Op::IntSRem) { + continue; + } + if let Some(d) = op.inputs.get(1).and_then(|i| i.as_const()) { + out.push((o, d)); + } + } + } + out +} + +/// The builds of the arithmetic fixture that exist here. +/// +/// A missing fixture skips rather than fails: the corpus is gitignored and a +/// fresh checkout has not run `scripts/build-fixtures.sh`. +fn arith_builds() -> Vec { + let mut out = Vec::new(); + for arch in ["x64", "a64"] { + for level in ["O1", "O2"] { + let name = format!("dt-arith.{arch}.{level}"); + if corpus().map(|d| d.join(&name).is_file()).unwrap_or(false) { + out.push(name); + } + } + } + out +} + +/// The reciprocal divisions in the fixture are divisions in the IR, on both +/// architectures and at both optimization levels. +#[test] +fn fixture_reciprocals_fold_to_divisions() { + let cases: &[(&str, Op, u64)] = &[ + ("divu81", Op::IntDiv, 81), + ("divu89", Op::IntDiv, 89), + ("divu91", Op::IntDiv, 91), + ("divu99", Op::IntDiv, 99), + ("divu101", Op::IntDiv, 101), + // 112 is 16 times 7, and the compiler shifts by four first, so the + // division that is left in the code really is by seven. + ("divu112", Op::IntDiv, 7), + ("divu125", Op::IntDiv, 125), + ("divs81", Op::IntSDiv, 81), + ("divs99", Op::IntSDiv, 99), + ("divs125", Op::IntSDiv, 125), + ]; + for build in arith_builds() { + let Some(p) = open(&build) else { continue }; + for (name, op, d) in cases { + let Some(s) = optimized(&p, name) else { + panic!("{name}: not recovered from {build}"); + }; + let found = quotients(&s); + assert!( + found.contains(&(*op, *d)), + "{name} in {build}: expected {op:?} by {d}, found {found:?}" + ); + } + } +} + +/// The remainder forms built on the same reciprocal fold too. +/// +/// x86-64 is left out of this one: there the multiply back is an `lea`, whose +/// result the lifter writes as an eight-byte temporary and reads back as a +/// four-byte one, and the SSA builder does not connect those two, so the +/// subtraction reads storage nothing defined. That is a defect below this pass +/// and no algebra here can see past it. +#[test] +fn fixture_remainders_fold_to_remainders() { + let cases: &[(&str, Op, u64)] = &[ + ("modu10", Op::IntRem, 10), + ("modu100", Op::IntRem, 100), + ("mods3", Op::IntSRem, 3), + ("mods7", Op::IntSRem, 7), + ("mods10", Op::IntSRem, 10), + ]; + for build in arith_builds().into_iter().filter(|b| b.contains("a64")) { + let Some(p) = open(&build) else { continue }; + for (name, op, d) in cases { + let Some(s) = optimized(&p, name) else { + panic!("{name}: not recovered from {build}"); + }; + let found = quotients(&s); + assert!( + found.contains(&(*op, *d)), + "{name} in {build}: expected {op:?} by {d}, found {found:?}" + ); + } + } +} + +/// Signed division by a power of two is a division, not a rounding correction. +#[test] +fn fixture_signed_power_of_two_folds() { + for build in arith_builds().into_iter().filter(|b| b.contains("a64")) { + let Some(p) = open(&build) else { continue }; + let Some(s) = optimized(&p, "divs8") else { + panic!("divs8: not recovered from {build}"); + }; + let found = quotients(&s); + assert!( + found.contains(&(Op::IntSDiv, 8)), + "divs8 in {build}: expected a signed division by 8, found {found:?}" + ); + } +} + +/// The power of two identities the pattern rules stand on, at every dividend +/// of a sixteen-bit range. +/// +/// These rules read a divisor straight off a mask or a shift, so there is no +/// magic number to recover and nothing for the reciprocal verifier to check. +/// What is left to be wrong is the algebra, and the algebra is small enough to +/// check by exhaustion: if any of these three identities failed, the rule built +/// on it would be emitting a division that is not the division the machine +/// computes. +#[test] +fn the_power_of_two_identities_hold() { + for x in i16::MIN..=i16::MAX { + let v = x as i32; + // `x < 0 ? -(x & 1) : (x & 1)` is `x % 2`. + let low = v & 1; + let chosen = if v < 0 { -low } else { low }; + assert_eq!(chosen, v % 2, "low bit remainder at {x}"); + for k in 1u32..16 { + // The bias a compiler adds so a shift truncates toward zero: the + // sign smeared across the whole width, then cut back to `k` bits. + let sign = (v >> 15) as u32 & 0xffff; + let bias = (sign >> (16 - k)) as i32; + let biased = v + bias; + assert_eq!( + ((biased << 16) >> 16) >> k, + v / (1 << k), + "biased shift quotient at {x} by 2^{k}" + ); + let masked = ((biased & !((1 << k) - 1)) << 16) >> 16; + assert_eq!(v - masked, v % (1 << k), "masked remainder at {x} by 2^{k}"); + } + } +} + +/// The power of two idioms in the fixture come out as a division and a +/// remainder, on both architectures. +#[test] +fn fixture_power_of_two_idioms_fold() { + for build in arith_builds() { + let Some(p) = open(&build) else { continue }; + let Some(s) = optimized(&p, "mods2") else { + panic!("mods2: not recovered from {build}"); + }; + let found = quotients(&s); + assert!( + found.contains(&(Op::IntSRem, 2)), + "mods2 in {build}: expected a signed remainder by 2, found {found:?}" + ); + } +} diff --git a/crates/e5r-ir/tests/reversals.rs b/crates/e5r-ir/tests/reversals.rs new file mode 100644 index 0000000..cd73d82 --- /dev/null +++ b/crates/e5r-ir/tests/reversals.rs @@ -0,0 +1,172 @@ +//! The byte and bit reversals, checked against this processor. +//! +//! The IR has no operation that reverses bytes or bits, so the lifter writes +//! each out as shifts and masks. That expansion is exact rather than +//! approximate, which is the only reason it is allowed to exist, and the only +//! way to be sure of it is to run the instruction. +//! +//! No portable C expression compiles to `rbit`, and the loop that computes the +//! same thing vectorizes instead, so the portable driver cannot reach it. This +//! assembles the instructions directly and runs them, which is a stronger +//! oracle anyway: nothing here is an expectation written by hand. + +use std::process::Command; + +use e5r_core::{Addr, Arch}; +use e5r_ir::interp::{Step, step}; +use e5r_ir::{Machine, lift}; + +/// The values to try. The edges first, because a mask written one bit wide +/// shows there and nowhere else. +const INPUTS: [u64; 10] = [ + 0, + 1, + 0xffff_ffff_ffff_ffff, + 0x0123_4567_89ab_cdef, + 0x8000_0000_0000_0000, + 0x0000_0000_0000_00ff, + 0xff00_0000_0000_0000, + 0xdead_beef_cafe_babe, + 0x5555_5555_5555_5555, + 0x8000_0001, +]; + +/// Assemble one instruction and hand back its four bytes. +/// The binutils built for AArch64, which on another host is the prefixed one. +/// A plain `objdump` from an x86-64 binutils prints nothing for these bytes, +/// and nothing compares equal to nothing. +fn binutils(name: &str) -> String { + let prefixed = format!("aarch64-linux-gnu-{name}"); + if Command::new(&prefixed).arg("--version").output().is_ok() { + prefixed + } else { + name.to_string() + } +} + +fn assemble(text: &str) -> Option<[u8; 4]> { + let dir = std::env::temp_dir().join(format!("e5r-rev-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("a.s"); + let obj = dir.join("a.o"); + std::fs::write(&src, format!("{text}\n")).ok()?; + let ok = Command::new(binutils("as")) + .arg("-o") + .arg(&obj) + .arg(&src) + .status() + .ok()?; + if !ok.success() { + return None; + } + let out = Command::new(binutils("objdump")) + .args(["-d", "--show-raw-insn"]) + .arg(&obj) + .output() + .ok()?; + let text = String::from_utf8_lossy(&out.stdout); + let hex = text + .lines() + .find_map(|l| l.split_once(":\t")?.1.split_whitespace().next()) + .filter(|h| h.len() == 8)?; + let word = u32::from_str_radix(hex, 16).ok()?; + let _ = std::fs::remove_dir_all(&dir); + Some(word.to_le_bytes()) +} + +/// Run one instruction on this processor, with `x0` set, and read `x0` back. +fn on_hardware(text: &str, inputs: &[u64]) -> Option> { + let dir = std::env::temp_dir().join(format!("e5r-revrun-{}", std::process::id())); + std::fs::create_dir_all(&dir).ok()?; + let src = dir.join("m.c"); + let bin = dir.join("m"); + let mut body = String::from("#include \n#include \nint main(void){\n"); + body.push_str(" uint64_t v;\n"); + for v in inputs { + body.push_str(&format!( + " __asm__ volatile(\"{text}\" : \"=r\"(v) : \"r\"((uint64_t){v}ULL));\n printf(\"%llu\\n\", (unsigned long long)v);\n" + )); + } + body.push_str(" return 0;\n}\n"); + std::fs::write(&src, body).ok()?; + let built = Command::new("cc") + .arg("-O0") + .arg("-o") + .arg(&bin) + .arg(&src) + .output() + .ok()?; + if !built.status.success() { + return None; + } + let ran = Command::new(&bin).output().ok()?; + let _ = std::fs::remove_dir_all(&dir); + Some( + String::from_utf8_lossy(&ran.stdout) + .lines() + .filter_map(|l| l.trim().parse().ok()) + .collect(), + ) +} + +/// Lift one instruction, run it in the interpreter with `x0` set, read `x0`. +fn interpreted(bytes: &[u8; 4], input: u64) -> Option { + let at = Addr(0x1000); + let insn = e5r_arch::aarch64::decode(bytes, at)?; + let lifted = lift::aarch64::lift(&insn); + assert!( + lifted.complete, + "{} is not modelled, so there is nothing to check", + e5r_arch::format(&Arch::AArch64, &insn, false) + ); + let mut m = Machine::new(); + let x0 = e5r_ir::lift::aarch64::gpr_offset(0); + let x1 = e5r_ir::lift::aarch64::gpr_offset(1); + m.set_reg(x1, 8, input); + for op in &lifted.ops { + if !matches!(step(&mut m, op), Step::Next) { + return None; + } + } + Some(m.reg(x0, 8)) +} + +#[test] +fn the_reversals_do_what_this_processor_does() { + if !cfg!(target_arch = "aarch64") { + return; // the hardware oracle is this host + } + // `%0` is the output and `%1` the input, in the order the harness writes + // them, so each of these is "reverse x1 into x0". + let cases = [ + ("rbit %0, %1", "rbit x0, x1"), + ("rev %0, %1", "rev x0, x1"), + ("rev16 %0, %1", "rev16 x0, x1"), + ("rev32 %0, %1", "rev32 x0, x1"), + ("rev %w0, %w1", "rev w0, w1"), + ("rbit %w0, %w1", "rbit w0, w1"), + ("rev16 %w0, %w1", "rev16 w0, w1"), + ]; + let mut checked = 0; + for (asm, plain) in cases { + let Some(bytes) = assemble(plain) else { + return; // no assembler here + }; + let Some(real) = on_hardware(asm, &INPUTS) else { + return; // no compiler here + }; + assert_eq!(real.len(), INPUTS.len(), "{plain}: the harness lost a case"); + for (input, want) in INPUTS.iter().zip(&real) { + let got = interpreted(&bytes, *input).expect("the interpreter stopped"); + // A 32-bit form leaves the top half zero, which the hardware + // answer already reflects, so nothing is masked here. + assert_eq!( + got, *want, + "{plain} on {input:#x}: the processor says {want:#x}, the lifter says {got:#x}" + ); + checked += 1; + } + } + assert!(checked >= 70, "only {checked} comparisons ran"); + eprintln!("{checked} reversal results match this processor"); +} diff --git a/crates/e5r-ir/tests/semantics.rs b/crates/e5r-ir/tests/semantics.rs new file mode 100644 index 0000000..30f6ee5 --- /dev/null +++ b/crates/e5r-ir/tests/semantics.rs @@ -0,0 +1,418 @@ +//! The lifter measured against the machine it models. +//! +//! Compile a C function, lift what the compiler produced, run it in the +//! interpreter, and compare against the answer computed independently in Rust. +//! Reading a lifter cannot tell you whether it is right; running the original +//! and the translation and comparing can. This is the gate the roadmap put in +//! M4 rather than M10, and the reason the interpreter ships with the lifters. + +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::{Addr, Arch}; +use e5r_format::LoadOptions; +use e5r_ir::exec::run; +use e5r_ir::lift::aarch64::{gpr_offset, sp_offset}; +use e5r_ir::{Machine, Stop}; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + if obj.arch != Arch::AArch64 { + return None; + } + Some(analyze(obj, &Options::default())) +} + +/// Where a fresh machine's stack lives. Somewhere unmapped, so a stray read is +/// visible as an unknown rather than silently reading the image. +const STACK: u64 = 0x7fff_0000; + +/// Call a function with integer arguments and return what it left in `x0`. +/// +/// Arguments go in x0 onwards and the stack pointer is set, which is the whole +/// of the AArch64 calling convention for a handful of integers. +fn call(p: &Program, name: &str, args: &[u64]) -> Option<(u64, e5r_ir::Outcome)> { + let f = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(name) || f.display_name() == name)?; + let mut m = Machine::over(&p.object.memory); + m.budget = 1 << 20; + for (i, a) in args.iter().enumerate().take(8) { + m.set_reg(gpr_offset(i as u8), 8, *a); + } + m.set_reg(sp_offset(), 8, STACK); + // Give the stack real storage so a spill and its reload agree. + m.write_mem(STACK - 0x1000, &[0u8; 0x2000]); + let outcome = run(&mut m, &p.object.memory, &p.object.arch, f.entry); + Some((m.reg(gpr_offset(0), 8), outcome)) +} + +/// Check one function against a closure that computes the same thing. +fn check(p: &Program, name: &str, cases: &[Vec], expect: impl Fn(&[u64]) -> u64) { + for args in cases { + let Some((got, outcome)) = call(p, name, args) else { + return; + }; + assert!( + outcome.unlifted.is_empty(), + "{name}{args:?}: {} instructions not modelled, first at {}", + outcome.unlifted.len(), + outcome.unlifted[0] + ); + assert_eq!( + outcome.stop, + Stop::Returned, + "{name}{args:?} stopped with {:?} after {} instructions", + outcome.stop, + outcome.insns + ); + let want = expect(args); + assert_eq!( + got, want, + "{name}{args:?} produced {got:#x}, expected {want:#x}" + ); + } +} + +/// Truncate to 32 bits, which is what a function returning `int` does. +fn w(v: u64) -> u64 { + v & 0xffff_ffff +} + +#[test] +fn arithmetic_at_every_width() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + + check( + &p, + "arith32", + &[vec![7, 3], vec![0, 0], vec![0xffff_ffff, 2], vec![100, 7]], + |a| { + let (x, y) = (a[0] as u32, a[1] as u32); + let d = y | 1; + w(x.wrapping_mul(y).wrapping_add(x / d).wrapping_add(x % d) as u64) + }, + ); + + check( + &p, + "arith64", + &[vec![7, 3], vec![u64::MAX, 2], vec![1 << 40, 12345]], + |a| { + let (x, y) = (a[0], a[1]); + let d = y | 1; + x.wrapping_mul(y).wrapping_add(x / d).wrapping_add(x % d) + }, + ); + + check( + &p, + "sarith64", + &[vec![7, 3], vec![(-9i64) as u64, 4], vec![(-1i64) as u64, 1]], + |a| { + let (x, y) = (a[0] as i64, a[1] as i64); + let d = y | 1; + x.wrapping_mul(y) + .wrapping_add(x.wrapping_div(d)) + .wrapping_add(x.wrapping_rem(d)) as u64 + }, + ); +} + +#[test] +fn division_by_zero_produces_zero_rather_than_trapping() { + // The architecture defines it that way, and a lifter that let the + // interpreter fault instead would be modelling a different machine. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let Some((got, outcome)) = call(&p, "arith32", &[10, 0]) else { + return; + }; + assert_eq!(outcome.stop, Stop::Returned); + // With y == 0 the source divides by `y | 1`, which is 1. + assert_eq!(got, w(0u32.wrapping_add(10).wrapping_add(0) as u64)); +} + +#[test] +fn widening_and_sign_extension() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + check( + &p, + "widen", + &[vec![0xff, 0xffff, 0xffff_ffff], vec![1, 2, 3]], + |a| (a[0] as u8 as u64) + (a[1] as u16 as u64) + (a[2] as u32 as u64), + ); + check( + &p, + "sign_widen", + &[ + vec![0xff, 0xffff, 0xffff_ffff], + vec![1, 2, 3], + vec![0x80, 0x8000, 0x8000_0000], + ], + |a| { + ((a[0] as u8 as i8 as i64) + (a[1] as u16 as i16 as i64) + (a[2] as u32 as i32 as i64)) + as u64 + }, + ); +} + +#[test] +fn comparisons_set_every_flag_correctly() { + // Each of the seven comparisons in the source sets a different bit of the + // result, so a wrong flag shows up as a wrong bit rather than as a wrong + // answer that might be right by accident. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + check( + &p, + "compares", + &[ + vec![1, 2], + vec![2, 1], + vec![3, 3], + vec![(-1i64) as u64, 1], + vec![1, (-1i64) as u64], + vec![i64::MIN as u64, i64::MAX as u64], + ], + |args| { + let (a, b) = (args[0] as i64, args[1] as i64); + let mut r = 0i32; + if a == b { + r += 1; + } + if a != b { + r += 2; + } + if a < b { + r += 4; + } + if a <= b { + r += 8; + } + if a > b { + r += 16; + } + if a >= b { + r += 32; + } + if (a as u64) < (b as u64) { + r += 64; + } + w(r as u32 as u64) + }, + ); +} + +#[test] +fn a_switch_the_compiler_turned_into_arithmetic() { + // Asked for a dense switch returning 0x1111, 0x2222 and so on, GCC emitted + // `(n + 1) * 0x1111` as two shift-and-adds followed by a csinv for the + // default arm. Nothing about that resembles a switch, which makes it a good + // test of whether the lifter models what the compiler actually did. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + check( + &p, + "dense", + &[ + vec![0], + vec![1], + vec![5], + vec![11], + vec![12], + vec![(-1i64) as u64], + ], + |a| { + let n = a[0] as i32; + let r: i64 = match n { + 0..=11 => 0x1111 * (n as i64 + 1), + _ => -1, + }; + r as u64 + }, + ); +} + +#[test] +fn conditional_selection() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + check( + &p, + "select_chain", + &[ + vec![1, 2, 3], + vec![3, 2, 1], + vec![(-5i64) as u64, (-9i64) as u64, (-1i64) as u64], + vec![0, 0, 0], + ], + |args| { + let (a, b, c) = (args[0] as i32, args[1] as i32, args[2] as i32); + let x = if a > b { a } else { b }; + let y = if x > c { x } else { c }; + w((if y < 0 { -y } else { y }) as u32 as u64) + }, + ); +} + +#[test] +fn shifts_and_rotates() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + check( + &p, + "rotate", + &[ + vec![0x0123_4567_89ab_cdef, 8], + vec![1, 1], + vec![u64::MAX, 63], + vec![0xdead_beef, 0], + ], + |a| { + let (x, k) = (a[0], a[1] as i32); + (x << (k & 63)) | (x >> ((64 - k) & 63)) + }, + ); + check( + &p, + "bits", + &[vec![0x1234_5678], vec![0], vec![0xffff_ffff]], + |a| { + let x = a[0] as u32; + w(((x & 0xff00ff) | ((x >> 8) & 0xff) | (x << 24)) as u64) + }, + ); +} + +#[test] +fn loops_terminate_and_accumulate() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + check(&p, "nested", &[vec![0], vec![1], vec![5], vec![17]], |a| { + let n = a[0] as i32; + let mut acc = 0i32; + for i in 0..n { + for j in i..n { + for k in j..n { + acc += i ^ j ^ k; + } + } + } + w(acc as u32 as u64) + }); + check(&p, "collatz", &[vec![1], vec![27], vec![97]], |a| { + let mut n = a[0]; + let mut steps = 0u64; + while n != 1 { + n = if n & 1 == 1 { 3 * n + 1 } else { n / 2 }; + steps += 1; + } + steps + }); +} + +#[test] +fn memory_reads_and_writes_agree_with_each_other() { + // A store then a load through the interpreter's memory, driven by real + // compiled code rather than by hand-built IR. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let Some(f) = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some("sum_array")) + else { + return; + }; + + let base = 0x5000_0000u64; + let values: [u64; 6] = [1, 2, 3, 4, 5, 6]; + let mut m = Machine::over(&p.object.memory); + m.budget = 1 << 20; + let mut bytes = Vec::new(); + for v in values { + bytes.extend_from_slice(&v.to_le_bytes()); + } + m.write_mem(base, &bytes); + m.write_mem(STACK - 0x1000, &[0u8; 0x2000]); + m.set_reg(gpr_offset(0), 8, base); + m.set_reg(gpr_offset(1), 8, values.len() as u64); + m.set_reg(sp_offset(), 8, STACK); + + let outcome = run(&mut m, &p.object.memory, &p.object.arch, f.entry); + assert!( + outcome.unlifted.is_empty(), + "sum_array: {} instructions not modelled", + outcome.unlifted.len() + ); + assert_eq!(outcome.stop, Stop::Returned, "{outcome:?}"); + assert_eq!(m.reg(gpr_offset(0), 8), values.iter().sum::()); + // Nothing outside the array or the stack was read. + let strays: Vec = m + .unknown_reads + .iter() + .copied() + .filter(|a| { + let v = a.get(); + !(base..base + bytes.len() as u64).contains(&v) + && !(STACK - 0x1000..STACK + 0x1000).contains(&v) + }) + .collect(); + assert!(strays.is_empty(), "read outside the model: {strays:?}"); +} + +#[test] +fn a_recursive_function_stops_at_its_first_call() { + // Calls are not followed, which is what a single-function test wants. The + // run has to stop cleanly and say where, rather than wander. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let Some((_, outcome)) = call(&p, "deep_recursion", &[10]) else { + return; + }; + assert!( + matches!(outcome.stop, Stop::Call(_) | Stop::Returned), + "{outcome:?}" + ); +} + +#[test] +fn a_budget_stops_a_runaway_rather_than_hanging() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let Some(f) = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some("collatz")) + else { + return; + }; + let mut m = Machine::over(&p.object.memory); + m.budget = 50; + m.set_reg(gpr_offset(0), 8, 0); + m.set_reg(sp_offset(), 8, STACK); + let outcome = run(&mut m, &p.object.memory, &p.object.arch, f.entry); + // Zero never reaches one, so this runs forever without the budget. + assert!( + matches!(outcome.stop, Stop::Budget | Stop::Returned), + "{outcome:?}" + ); +} diff --git a/crates/e5r-ir/tests/ssa.rs b/crates/e5r-ir/tests/ssa.rs new file mode 100644 index 0000000..801f9c7 --- /dev/null +++ b/crates/e5r-ir/tests/ssa.rs @@ -0,0 +1,279 @@ +//! SSA construction and the dataflow that follows it, on real compiled code. +//! +//! The properties checked are the ones every later pass depends on: one +//! definition per value, every use reaching a definition or an explicit +//! undefined, one phi input per predecessor, and a large reduction in operation +//! count once the bookkeeping lifting produces is removed. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::{Addr, Arch}; +use e5r_format::LoadOptions; +use e5r_ir::func; +use e5r_ir::opt; +use e5r_ir::ssa::{self, Operand, SsaFunction, SsaKind}; + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + if obj.arch != Arch::AArch64 { + return None; + } + Some(analyze(obj, &Options::default())) +} + +/// Lift one named function and put it in SSA form. Kept for the tests that +/// want a single function rather than the whole corpus. +#[allow(dead_code)] +fn ssa_of(p: &Program, name: &str) -> Option<(func::Function, SsaFunction)> { + let f = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(name))?; + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let ir = func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + let s = ssa::build(&ir); + Some((ir, s)) +} + +/// Every function in a fixture, for the property checks. +fn all(p: &Program) -> Vec<(String, func::Function, SsaFunction)> { + p.functions_by_address() + .filter(|f| f.is_complete() && f.cfg.blocks.len() < 200) + .filter_map(|f| { + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let ir = func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + if !ir.is_complete() { + return None; + } + let s = ssa::build(&ir); + Some((f.display_name(), ir, s)) + }) + .collect() +} + +#[test] +fn every_value_has_exactly_one_definition() { + // The property SSA is named for, and the one every later pass assumes. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let functions = all(&p); + assert!(!functions.is_empty(), "nothing lifted"); + for (name, _, s) in &functions { + let mut seen = BTreeSet::new(); + for b in s.blocks.values() { + for op in &b.ops { + if let Some(v) = op.out { + assert!(seen.insert(v), "{name}: {v} defined twice"); + } + } + } + } +} + +#[test] +fn every_phi_has_one_input_per_predecessor() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + for (name, _, s) in all(&p) { + for (at, b) in &s.blocks { + for op in &b.ops { + if op.kind != SsaKind::Phi { + continue; + } + assert_eq!( + op.inputs.len(), + b.predecessors.len(), + "{name} block {at}: phi has {} inputs for {} predecessors", + op.inputs.len(), + b.predecessors.len() + ); + } + } + } +} + +#[test] +fn phis_come_first_in_their_block() { + // Anything that walks a block assumes this, and a phi in the middle would + // mean its inputs depend on operations that run after it. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + for (name, _, s) in all(&p) { + for (at, b) in &s.blocks { + let mut seen_other = false; + for op in &b.ops { + match op.kind { + SsaKind::Phi => assert!( + !seen_other, + "{name} block {at}: a phi follows an ordinary operation" + ), + _ => seen_other = true, + } + } + } + } +} + +#[test] +fn every_use_reaches_a_definition() { + // An operand that names a value nothing defines would mean the renaming + // lost a path, which no later pass could recover from. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + for (name, _, s) in all(&p) { + let defined: BTreeSet<_> = s.definitions().keys().copied().collect(); + for (at, b) in &s.blocks { + for op in &b.ops { + for i in &op.inputs { + if let Operand::Value(v) = i { + assert!( + defined.contains(v), + "{name} block {at}: {v} is used but never defined" + ); + } + } + } + } + } +} + +#[test] +fn the_dataflow_passes_remove_most_of_the_bookkeeping() { + // Lifting one instruction produces several operations and most are flags + // nothing reads. If the passes did not remove them the IR would be + // unreadable, which is the whole reason they exist. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let mut before = 0usize; + let mut after = 0usize; + for (_, _, mut s) in all(&p) { + before += s.op_count(); + opt::optimize(&mut s); + after += s.op_count(); + } + assert!(before > 500, "only {before} operations to work with"); + let kept = after as f64 / before as f64; + assert!( + kept < 0.7, + "the passes kept {:.0}% of {before} operations", + kept * 100.0 + ); + println!( + "dataflow: {before} operations to {after} ({:.0}% kept)", + kept * 100.0 + ); +} + +#[test] +fn optimizing_preserves_the_single_definition_property() { + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + for (name, _, mut s) in all(&p) { + opt::optimize(&mut s); + let mut seen = BTreeSet::new(); + for b in s.blocks.values() { + for op in &b.ops { + if let Some(v) = op.out { + assert!(seen.insert(v), "{name}: {v} defined twice after optimizing"); + } + } + } + } +} + +#[test] +fn optimizing_keeps_every_operation_that_has_an_effect() { + // Removing a store, a branch or a call would change what the program does. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + for (name, _, mut s) in all(&p) { + let count = |f: &SsaFunction| -> usize { + f.blocks + .values() + .flat_map(|b| b.ops.iter()) + .filter(|o| { + matches!( + o.kind, + SsaKind::Op( + e5r_ir::Op::Store + | e5r_ir::Op::Branch + | e5r_ir::Op::CBranch + | e5r_ir::Op::Call + | e5r_ir::Op::CallInd + | e5r_ir::Op::Return + ) + ) + }) + .count() + }; + let before = count(&s); + opt::optimize(&mut s); + assert_eq!( + count(&s), + before, + "{name}: an effectful operation was removed" + ); + } +} + +#[test] +fn optimizing_terminates_on_every_function_in_the_corpus() { + // A pass that runs to a fixed point has to reach one. Loops, recursion and + // phi cycles are all here. + for fixture in ["wide.a64.O2.o", "wide.a64.O0.o", "shapes.a64.O2.o"] { + let Some(p) = open(fixture) else { continue }; + for (_, _, mut s) in all(&p) { + let changes = opt::optimize(&mut s); + // A second run must find nothing, or the fixed point was not one. + let again = opt::optimize(&mut s); + assert!( + again.removed == 0 && again.folded == 0, + "{fixture}: not at a fixed point after {changes:?}, second run {again:?}" + ); + } + } +} + +#[test] +fn the_constant_folder_fires_on_real_code() { + // Checking for a specific literal does not work: asked for a dense switch + // returning 0x1111, 0x2222 and so on, the compiler emitted + // `(n + 1) * 0x1111` as two shift-and-adds and no constant appears at all. + // What can be checked is that folding happens across the corpus, which is + // what says the folder reaches real code rather than only its own tests. + let Some(p) = open("wide.a64.O2.o") else { + return; + }; + let mut folded = 0usize; + for (_, _, mut s) in all(&p) { + folded += opt::optimize(&mut s).folded; + } + assert!( + folded > 10, + "the folder fired {folded} times on the whole corpus" + ); +} diff --git a/crates/e5r-ir/tests/widths.rs b/crates/e5r-ir/tests/widths.rs new file mode 100644 index 0000000..0b2b035 --- /dev/null +++ b/crates/e5r-ir/tests/widths.rs @@ -0,0 +1,262 @@ +//! The width an operation works at, kept through the optimizer. +//! +//! A machine comparison is a subtraction and a formula over the flags, and the +//! optimizer puts the comparison back. The number of bits it compares at is +//! carried by nothing but the width of its operands, so a pass that widens one +//! of them changes the answer: 0x80000000 is negative at 32 bits and positive +//! at 64. These check that the width survives. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; + +use e5r_analysis::{Options, Program, analyze}; +use e5r_core::{Addr, Arch}; +use e5r_format::LoadOptions; +use e5r_ir::op::{Op, Space}; +use e5r_ir::opt; +use e5r_ir::ssa::{self, Location, Operand, SsaBlock, SsaFunction, SsaKind, SsaOp, Value}; +use e5r_ir::{func, stack}; + +/// The comparisons whose operand widths are the width being compared. +const COMPARISONS: [Op; 6] = [ + Op::IntEqual, + Op::IntNotEqual, + Op::IntLess, + Op::IntLessEqual, + Op::IntSLess, + Op::IntSLessEqual, +]; + +fn value(space: Space, offset: u64, size: u8, version: u32) -> Value { + Value { + location: Location { + space, + offset, + size, + }, + version, + } +} + +fn op(o: Op, out: Option, inputs: Vec, size: u8) -> SsaOp { + SsaOp { + addr: Addr(0x1000), + kind: SsaKind::Op(o), + out, + inputs, + size, + } +} + +/// One block holding what a 32-bit `subs` and a signed branch lift to. +/// +/// Each operand is read out of a 64-bit register, written back into one, and +/// read out again, which is the shape a compiler that spills produces and the +/// shape that used to lose the width. +fn thirty_two_bit_signed_compare() -> SsaFunction { + let mut ops = Vec::new(); + let mut narrow = Vec::new(); + for (n, reg) in [0u64, 8].into_iter().enumerate() { + let n = n as u32; + let read = value(Space::Unique, 0x100 + reg, 4, n); + let widened = value(Space::Register, reg, 8, n); + let again = value(Space::Unique, 0x200 + reg, 4, n); + ops.push(op( + Op::SubPiece, + Some(read), + vec![ + Operand::Undefined(Location { + space: Space::Register, + offset: reg, + size: 8, + }), + Operand::Const(0, 1), + ], + 4, + )); + ops.push(op( + Op::IntZExt, + Some(widened), + vec![Operand::Value(read)], + 8, + )); + ops.push(op( + Op::SubPiece, + Some(again), + vec![Operand::Value(widened), Operand::Const(0, 1)], + 4, + )); + narrow.push(Operand::Value(again)); + } + let (x, y) = (narrow[0], narrow[1]); + + let difference = value(Space::Unique, 0x300, 4, 0); + let sign = value(Space::Unique, 0x304, 4, 0); + let negative = value(Space::Register, 0x400, 1, 0); + let overflow = value(Space::Register, 0x401, 1, 0); + let less = value(Space::Register, 0x402, 1, 0); + ops.push(op(Op::IntSub, Some(difference), vec![x, y], 4)); + ops.push(op( + Op::IntRight, + Some(sign), + vec![Operand::Value(difference), Operand::Const(31, 1)], + 4, + )); + ops.push(op( + Op::IntNotEqual, + Some(negative), + vec![Operand::Value(sign), Operand::Const(0, 4)], + 1, + )); + ops.push(op(Op::IntSBorrow, Some(overflow), vec![x, y], 1)); + ops.push(op( + Op::IntNotEqual, + Some(less), + vec![Operand::Value(negative), Operand::Value(overflow)], + 1, + )); + // A branch, so the comparison is not dead and survives to be inspected. + ops.push(op( + Op::CBranch, + None, + vec![Operand::Const(0x2000, 8), Operand::Value(less)], + 8, + )); + + SsaFunction { + arch: Arch::AArch64, + entry: Addr(0x1000), + blocks: BTreeMap::from([( + Addr(0x1000), + SsaBlock { + ops, + successors: Vec::new(), + predecessors: Vec::new(), + }, + )]), + } +} + +#[test] +fn a_signed_comparison_keeps_the_width_it_was_made_at() { + let mut f = thirty_two_bit_signed_compare(); + opt::optimize(&mut f); + let comparisons: Vec<&SsaOp> = f + .blocks + .values() + .flat_map(|b| b.ops.iter()) + .filter(|o| o.kind == SsaKind::Op(Op::IntSLess)) + .collect(); + assert_eq!( + comparisons.len(), + 1, + "the flag algebra should fold to one signed comparison, got {}", + f.blocks + .values() + .flat_map(|b| b.ops.iter()) + .map(|o| format!("{:?}", o.kind)) + .collect::>() + .join(" ") + ); + let sizes: Vec = comparisons[0].inputs.iter().map(|i| i.size()).collect(); + assert_eq!( + sizes, + vec![4, 4], + "a 32-bit signed comparison compares 32 bits in both operands" + ); +} + +fn corpus() -> Option { + let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/build"); + d.is_dir().then(|| d.canonicalize().unwrap()) +} + +fn open(name: &str) -> Option { + let data = std::fs::read(corpus()?.join(name)).ok()?; + let obj = e5r_format::load(&data, &LoadOptions::default()).ok()?; + if obj.arch != Arch::AArch64 { + return None; + } + Some(analyze(obj, &Options::default())) +} + +fn optimized(p: &Program, name: &str) -> Option { + let f = p + .functions_by_address() + .find(|f| f.name.as_deref() == Some(name))?; + let blocks: BTreeMap)> = f + .cfg + .blocks + .iter() + .map(|(a, b)| (*a, (b.range.end(), b.successors.clone()))) + .collect(); + let mut ir = func::build(&p.object.memory, &p.object.arch, f.entry, &blocks); + stack::promote(&mut ir); + let mut s = ssa::build(&ir); + opt::optimize(&mut s); + Some(s) +} + +/// Both operands of a comparison, in the functions whose machine code compares +/// at 32 bits throughout. +#[test] +fn the_fixtures_compare_at_the_width_the_machine_did() { + let Some(p) = open("driver.a64.O0") else { + return; + }; + for name in ["select_chain", "sarith32"] { + let Some(f) = optimized(&p, name) else { + continue; + }; + let widths: BTreeSet> = f + .blocks + .values() + .flat_map(|b| b.ops.iter()) + .filter(|o| matches!(&o.kind, SsaKind::Op(o) if COMPARISONS.contains(o))) + .map(|o| o.inputs.iter().map(|i| i.size()).collect()) + .collect(); + assert!( + widths.iter().all(|w| w == &vec![4u8, 4]), + "{name} compares 32-bit values; the operand widths are {widths:?}" + ); + } +} + +/// Nothing in the corpus compares two operands of different widths, whatever +/// the width is: the machine had one pair of registers and one width. +#[test] +fn no_comparison_mixes_two_widths() { + let mut bad: Vec = Vec::new(); + for name in ["driver.a64.O0", "driver.a64.O1", "driver.a64.O2"] { + let Some(p) = open(name) else { continue }; + for f in p.functions_by_address().filter(|f| f.is_complete()) { + let Some(symbol) = f.name.clone() else { + continue; + }; + let Some(s) = optimized(&p, &symbol) else { + continue; + }; + for o in s.blocks.values().flat_map(|b| b.ops.iter()) { + let SsaKind::Op(k) = o.kind else { continue }; + if !COMPARISONS.contains(&k) || o.inputs.len() != 2 { + continue; + } + if o.inputs[0].size() != o.inputs[1].size() { + bad.push(format!( + "{name} {symbol} {:?} at {:?}: {} vs {}", + k, + o.addr, + o.inputs[0].size(), + o.inputs[1].size() + )); + } + } + } + } + assert!( + bad.is_empty(), + "{} comparison(s) read a different number of bits from each operand:\n{}", + bad.len(), + bad.iter().take(20).cloned().collect::>().join("\n") + ); +} diff --git a/crates/e5r-sla/Cargo.toml b/crates/e5r-sla/Cargo.toml new file mode 100644 index 0000000..1debdf2 --- /dev/null +++ b/crates/e5r-sla/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "e5r-sla" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +description = "Reader for Ghidra's compiled SLEIGH language files (.sla)" + +# No dependencies on purpose. The payload is a raw zlib stream and the +# decompressor is in src/inflate.rs; see docs/sla-format.md for why the +# workspace does not take a crate for it. +[dependencies] + +# A dev-dependency on purpose, and the direction is the reason. The compiler +# in tests/sleighc turns an `e5r-sleigh` specification into the model this +# crate writes, but e5r-sleigh's own decode engine will want to read a `.sla`, +# and cargo refuses a cycle between two normal dependencies. A dev-dependency +# cycle is allowed, so the compiler lives in the test harness until it moves +# into e5r-sleigh, which is a file move and no code change. +[dev-dependencies] +e5r-sleigh = { workspace = true } diff --git a/crates/e5r-sla/src/decode.rs b/crates/e5r-sla/src/decode.rs new file mode 100644 index 0000000..6f9dab0 --- /dev/null +++ b/crates/e5r-sla/src/decode.rs @@ -0,0 +1,401 @@ +//! The tagged tree the decompressed `.sla` payload holds. +//! +//! The encoding is worked out in `docs/sla-format.md`. In short: a byte stream +//! of element-start, element-end and attribute tags, where the top three bits +//! of a tag byte pick the kind and say whether the id is in the low five bits +//! or in following seven-bit chunks. Every attribute is followed by exactly one +//! self-describing value, so a reader can walk a file whose ids it does not +//! know without losing sync, which is what makes the unknown regions below +//! countable rather than fatal. + +/// Tag kinds, from the top three bits of a tag byte. +const ELEMENT_START: u8 = 0b010; +const ELEMENT_START_EXT: u8 = 0b011; +const ELEMENT_END: u8 = 0b100; +const ELEMENT_END_EXT: u8 = 0b101; +const ATTRIBUTE: u8 = 0b110; +const ATTRIBUTE_EXT: u8 = 0b111; + +/// Value type codes, from the top four bits of a value byte. +const V_BOOL: u8 = 1; +const V_SIGNED_POS: u8 = 2; +const V_SIGNED_NEG: u8 = 3; +const V_UNSIGNED: u8 = 4; +const V_SPACE: u8 = 5; +const V_STRING: u8 = 7; + +/// A nesting cap. Ghidra's own files reach depth 14; anything past this is a +/// crafted file, and the cap is what keeps the recursive drop of a `Node` off +/// the stack limit. +pub const MAX_DEPTH: usize = 128; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DecodeError { + /// Input ended in the middle of a tag, an id, or a value. + Truncated { at: usize }, + /// A tag byte in the range no tag uses. + BadTag { at: usize, byte: u8 }, + /// An element end whose id is not the element that is open. + Mismatched { at: usize, open: u32, closed: u32 }, + /// An element end with nothing open, or content after the root closed. + Unbalanced { at: usize }, + /// The root element never closed. + Unclosed { open: u32 }, + /// An attribute outside any element. + StrayAttribute { at: usize }, + /// Nesting past `MAX_DEPTH`. + TooDeep { at: usize }, + /// A string whose length runs past the end of the payload. + StringOverruns { at: usize, len: u128 }, + /// A string that is not UTF-8. + NotUtf8 { at: usize }, + /// An empty payload. + Empty, +} + +impl core::fmt::Display for DecodeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Truncated { at } => write!(f, "payload ends mid-token at {at}"), + Self::BadTag { at, byte } => write!(f, "byte {byte:#04x} at {at} is not a tag"), + Self::Mismatched { at, open, closed } => { + write!(f, "element {open} closed as {closed} at {at}") + } + Self::Unbalanced { at } => write!(f, "unbalanced element end at {at}"), + Self::Unclosed { open } => write!(f, "element {open} never closed"), + Self::StrayAttribute { at } => write!(f, "attribute outside an element at {at}"), + Self::TooDeep { at } => write!(f, "nesting past {MAX_DEPTH} at {at}"), + Self::StringOverruns { at, len } => write!(f, "string of {len} bytes at {at} overruns"), + Self::NotUtf8 { at } => write!(f, "string at {at} is not utf-8"), + Self::Empty => f.write_str("empty payload"), + } + } +} + +impl std::error::Error for DecodeError {} + +/// An attribute value. The four integer codes are kept apart rather than +/// folded together, because which one the writer chose is itself evidence +/// about the field and a reader that flattens them cannot report it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Value { + Bool(bool), + /// Value type 2 or 3. Type 3 carries the magnitude of a negative number. + Signed(i128), + /// Value type 4. + Unsigned(u128), + /// Value type 5: an index into the space table. + Space(u32), + /// Value type 7. + Text(String), + /// Value types 0 and 6, which no observed file uses. Kept rather than + /// rejected so an unknown type does not make a file unreadable. + Other { + kind: u8, + value: u128, + }, +} + +impl Value { + /// The value as an unsigned integer, if it is one and it fits. + #[must_use] + pub fn as_u64(&self) -> Option { + match self { + Self::Signed(v) if *v >= 0 => u64::try_from(*v).ok(), + Self::Unsigned(v) => u64::try_from(*v).ok(), + Self::Space(v) => Some(u64::from(*v)), + Self::Bool(b) => Some(u64::from(*b)), + _ => None, + } + } + + /// The value as a signed integer, if it is one and it fits. + #[must_use] + pub fn as_i64(&self) -> Option { + match self { + Self::Signed(v) => i64::try_from(*v).ok(), + Self::Unsigned(v) => i64::try_from(*v).ok(), + Self::Space(v) => Some(i64::from(*v)), + _ => None, + } + } + + #[must_use] + pub fn as_bool(&self) -> Option { + match self { + Self::Bool(b) => Some(*b), + _ => None, + } + } + + #[must_use] + pub fn as_str(&self) -> Option<&str> { + match self { + Self::Text(s) => Some(s), + _ => None, + } + } + + #[must_use] + pub fn as_space(&self) -> Option { + match self { + Self::Space(v) => Some(*v), + _ => None, + } + } +} + +/// One element of the tree, with the byte range it occupies in the +/// decompressed payload so coverage can be measured against the file. +#[derive(Debug, Clone)] +pub struct Node { + pub id: u32, + pub attrs: Vec<(u32, Value)>, + pub children: Vec, + /// Offset of the element-start tag in the decompressed payload. + pub start: usize, + /// Offset one past the element-end tag. + pub end: usize, +} + +impl Node { + #[must_use] + pub fn len(&self) -> usize { + self.end - self.start + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.end == self.start + } + + /// Bytes this element occupies that its children do not: its own tags, + /// its ids and its attribute values. Summing this over every node in the + /// tree gives the payload length exactly, which is what makes a coverage + /// figure computed from it honest. + #[must_use] + pub fn own_len(&self) -> usize { + self.len() - self.children.iter().map(Node::len).sum::() + } + + #[must_use] + pub fn attr(&self, id: u32) -> Option<&Value> { + self.attrs.iter().find(|(a, _)| *a == id).map(|(_, v)| v) + } + + #[must_use] + pub fn child(&self, id: u32) -> Option<&Node> { + self.children.iter().find(|c| c.id == id) + } + + /// Depth-first visit, this element first. + pub fn visit(&self, f: &mut impl FnMut(&Node)) { + f(self); + for c in &self.children { + c.visit(f); + } + } +} + +struct Cursor<'a> { + data: &'a [u8], + pos: usize, +} + +impl Cursor<'_> { + fn byte(&mut self) -> Result { + let b = *self + .data + .get(self.pos) + .ok_or(DecodeError::Truncated { at: self.pos })?; + self.pos += 1; + Ok(b) + } + + /// `n` seven-bit chunks, most significant first. `n` is at most 16, which + /// is 112 bits, so the accumulator is 128 bits wide and cannot wrap. + fn chunks(&mut self, n: usize) -> Result { + if self.pos + n > self.data.len() { + return Err(DecodeError::Truncated { at: self.pos }); + } + let mut v: u128 = 0; + for _ in 0..n { + let b = self.data[self.pos]; + self.pos += 1; + v = (v << 7) | u128::from(b & 0x7f); + } + Ok(v) + } + + fn value(&mut self) -> Result { + let at = self.pos; + let head = self.byte()?; + let kind = head >> 4; + let count = (head & 0x0f) as usize; + if kind == V_BOOL { + return Ok(Value::Bool(count != 0)); + } + let raw = self.chunks(count)?; + Ok(match kind { + V_SIGNED_POS => Value::Signed(i128::try_from(raw).unwrap_or(i128::MAX)), + V_SIGNED_NEG => Value::Signed(i128::try_from(raw).map_or(i128::MIN, |v| -v)), + V_UNSIGNED => Value::Unsigned(raw), + V_SPACE => Value::Space(u32::try_from(raw).unwrap_or(u32::MAX)), + V_STRING => { + // Bounded against the payload before the allocation, so a + // crafted length cannot reserve gigabytes. + let len = usize::try_from(raw) + .map_err(|_| DecodeError::StringOverruns { at, len: raw })?; + if self.pos + len > self.data.len() { + return Err(DecodeError::StringOverruns { at, len: raw }); + } + let bytes = &self.data[self.pos..self.pos + len]; + self.pos += len; + Value::Text( + core::str::from_utf8(bytes) + .map_err(|_| DecodeError::NotUtf8 { at })? + .to_owned(), + ) + } + _ => Value::Other { kind, value: raw }, + }) + } +} + +/// Decode the tagged tree. The payload is exactly one root element; trailing +/// bytes are an error rather than something to ignore. +pub fn decode(data: &[u8]) -> Result { + if data.is_empty() { + return Err(DecodeError::Empty); + } + let mut cur = Cursor { data, pos: 0 }; + let mut stack: Vec = Vec::new(); + let mut root: Option = None; + + while cur.pos < data.len() { + if root.is_some() { + return Err(DecodeError::Unbalanced { at: cur.pos }); + } + let at = cur.pos; + let head = cur.byte()?; + let kind = head >> 5; + // The extended forms spend the low four bits on a chunk count. A count + // of zero would encode nothing, so the stored count is one less than + // the real one and the walk always advances. + let id = match kind { + ELEMENT_START | ELEMENT_END | ATTRIBUTE => u128::from(head & 0x1f), + ELEMENT_START_EXT | ELEMENT_END_EXT | ATTRIBUTE_EXT => { + cur.chunks((head & 0x0f) as usize + 1)? + } + _ => return Err(DecodeError::BadTag { at, byte: head }), + }; + let id = u32::try_from(id).unwrap_or(u32::MAX); + + match kind { + ELEMENT_START | ELEMENT_START_EXT => { + if stack.len() >= MAX_DEPTH { + return Err(DecodeError::TooDeep { at }); + } + stack.push(Node { + id, + attrs: Vec::new(), + children: Vec::new(), + start: at, + end: at, + }); + } + ELEMENT_END | ELEMENT_END_EXT => { + let mut done = stack.pop().ok_or(DecodeError::Unbalanced { at })?; + if done.id != id { + return Err(DecodeError::Mismatched { + at, + open: done.id, + closed: id, + }); + } + done.end = cur.pos; + match stack.last_mut() { + Some(parent) => parent.children.push(done), + None => root = Some(done), + } + } + _ => { + let v = cur.value()?; + stack + .last_mut() + .ok_or(DecodeError::StrayAttribute { at })? + .attrs + .push((id, v)); + } + } + } + + match root { + Some(r) => Ok(r), + None => Err(DecodeError::Unclosed { + open: stack.last().map_or(0, |n| n.id), + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn short_tags_round_trip() { + // element 4 { attribute 3 = true } end 4 + let tree = decode(&[0x44, 0xc3, 0x11, 0x84]).unwrap(); + assert_eq!(tree.id, 4); + assert_eq!(tree.attrs, vec![(3, Value::Bool(true))]); + assert_eq!(tree.own_len(), 4); + } + + #[test] + fn extended_ids_use_seven_bit_chunks() { + // 0x60 is an extended element start with one chunk: id 0x21. + let tree = decode(&[0x60, 0xa1, 0xa0, 0xa1]).unwrap(); + assert_eq!(tree.id, 0x21); + } + + #[test] + fn strings_carry_a_chunked_length() { + let mut b = vec![0x44, 0xcc, 0x71, 0x83]; + b.extend_from_slice(b"ram"); + b.push(0x84); + let tree = decode(&b).unwrap(); + assert_eq!(tree.attr(12).and_then(Value::as_str), Some("ram")); + } + + #[test] + fn mismatched_close_is_an_error() { + assert!(matches!( + decode(&[0x44, 0x85]), + Err(DecodeError::Mismatched { .. }) + )); + } + + #[test] + fn trailing_bytes_after_the_root_are_an_error() { + assert!(matches!( + decode(&[0x44, 0x84, 0x44, 0x84]), + Err(DecodeError::Unbalanced { .. }) + )); + } + + #[test] + fn a_string_longer_than_the_payload_is_rejected_before_allocating() { + let b = vec![0x44, 0xcc, 0x72, 0xff, 0xff]; + assert!(matches!( + decode(&b), + Err(DecodeError::StringOverruns { .. }) + )); + } + + #[test] + fn depth_is_capped() { + let mut b = vec![0x44; MAX_DEPTH + 4]; + b.extend(std::iter::repeat_n(0x84u8, MAX_DEPTH + 4)); + assert!(matches!(decode(&b), Err(DecodeError::TooDeep { .. }))); + } +} diff --git a/crates/e5r-sla/src/deflate.rs b/crates/e5r-sla/src/deflate.rs new file mode 100644 index 0000000..50a8e22 --- /dev/null +++ b/crates/e5r-sla/src/deflate.rs @@ -0,0 +1,395 @@ +//! zlib (RFC 1950) and DEFLATE (RFC 1951) compression. +//! +//! The other half of `inflate.rs`, and written for the same reason: a `.sla` +//! payload is a zlib stream, so writing one at all needs a compressor, and the +//! workspace does not take a dependency for something both RFCs specify. +//! +//! Two block strategies, because they answer different questions: +//! +//! * [`Level::Stored`] emits type 0 blocks. The output is larger than the +//! input by five bytes per 64 KB and is trivially correct, which makes it +//! the control when something downstream looks wrong. +//! * [`Level::Fixed`] runs LZ77 with a hash chain and codes the result with +//! the fixed Huffman tables of RFC 1951 section 3.2.6. No dynamic tables: +//! they would buy perhaps another 15% and they are the part of DEFLATE with +//! the most ways to be subtly wrong, so the size is spent rather than the +//! risk taken. `docs/sla-format.md` records what this costs against the +//! reference compiler's output. +//! +//! Neither reproduces zlib's own bit stream, and nothing here tries to: a +//! `.sla` is compared after decompression, because the compressed form is not +//! part of the format's meaning. + +/// Which DEFLATE block type to emit. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Level { + /// Type 0 blocks: no compression, no matching, no Huffman coding. + Stored, + /// Type 1 blocks: LZ77 with the fixed Huffman tables. + #[default] + Fixed, +} + +/// RFC 1950 section 9. Runs over the uncompressed bytes. +#[must_use] +pub fn adler32(data: &[u8]) -> u32 { + const MOD: u32 = 65521; + let (mut a, mut b) = (1u32, 0u32); + // 5552 is the most bytes that can be summed before `b` can overflow 32 + // bits, so the modulo runs once a chunk rather than once a byte. + for chunk in data.chunks(5552) { + for &byte in chunk { + a += u32::from(byte); + b += a; + } + a %= MOD; + b %= MOD; + } + (b << 16) | a +} + +/// Compress to a zlib stream: the two-byte header, the DEFLATE data, and the +/// Adler-32 of the input. +#[must_use] +pub fn deflate_zlib(data: &[u8], level: Level) -> Vec { + let mut out = Vec::with_capacity(data.len() / 2 + 64); + // CMF: deflate, 32 KB window. FLG: no preset dictionary, and the check + // bits chosen so the pair is a multiple of 31. 0x789c is what every file + // in the corpus starts with, so our files start the same way. + out.push(0x78); + out.push(0x9c); + match level { + Level::Stored => stored_blocks(&mut out, data), + Level::Fixed => fixed_blocks(&mut out, data), + } + out.extend_from_slice(&adler32(data).to_be_bytes()); + out +} + +/// Raw DEFLATE with no zlib wrapper, for callers that supply their own. +#[must_use] +pub fn deflate(data: &[u8], level: Level) -> Vec { + let mut out = Vec::new(); + match level { + Level::Stored => stored_blocks(&mut out, data), + Level::Fixed => fixed_blocks(&mut out, data), + } + out +} + +fn stored_blocks(out: &mut Vec, data: &[u8]) { + // A stored block's length field is sixteen bits, and an empty input still + // needs one block so the stream has a final block to end on. + let mut chunks = data.chunks(0xffff).peekable(); + if chunks.peek().is_none() { + out.push(1); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&(!0u16).to_le_bytes()); + return; + } + while let Some(chunk) = chunks.next() { + let last = chunks.peek().is_none(); + out.push(u8::from(last)); + let len = chunk.len() as u16; + out.extend_from_slice(&len.to_le_bytes()); + out.extend_from_slice(&(!len).to_le_bytes()); + out.extend_from_slice(chunk); + } +} + +/// Bit output, least significant bit first within a byte, which is how +/// DEFLATE packs. Huffman codes go in most significant bit first and +/// everything else least significant bit first; the two writers below keep +/// that distinction where it belongs rather than at every call site. +struct BitWriter<'a> { + out: &'a mut Vec, + bits: u32, + n: u32, +} + +impl BitWriter<'_> { + /// `n` bits of `v`, least significant first. + fn put(&mut self, v: u32, n: u32) { + self.bits |= (v & ((1u32 << n) - 1)) << self.n; + self.n += n; + while self.n >= 8 { + self.out.push(self.bits as u8); + self.bits >>= 8; + self.n -= 8; + } + } + + /// A Huffman code: `n` bits of `code`, most significant first. + fn put_code(&mut self, code: u32, n: u32) { + let mut rev = 0u32; + for i in 0..n { + rev |= ((code >> (n - 1 - i)) & 1) << i; + } + self.put(rev, n); + } + + fn flush(&mut self) { + if self.n > 0 { + self.out.push(self.bits as u8); + self.bits = 0; + self.n = 0; + } + } +} + +/// The fixed literal/length code of RFC 1951 section 3.2.6. +fn fixed_lit(sym: u32) -> (u32, u32) { + match sym { + 0..=143 => (0x30 + sym, 8), + 144..=255 => (0x190 + sym - 144, 9), + 256..=279 => (sym - 256, 7), + _ => (0xc0 + sym - 280, 8), + } +} + +/// Length code, base and extra bits, for a match length of 3..=258. +fn length_code(len: u32) -> (u32, u32, u32) { + const BASE: [u32; 29] = [ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, + 131, 163, 195, 227, 258, + ]; + const EXTRA: [u32; 29] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, + ]; + let mut i = 28; + while i > 0 && BASE[i] > len { + i -= 1; + } + (257 + i as u32, EXTRA[i], len - BASE[i]) +} + +/// Distance code, base and extra bits, for a distance of 1..=32768. +fn distance_code(dist: u32) -> (u32, u32, u32) { + const BASE: [u32; 30] = [ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, + 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, + ]; + const EXTRA: [u32; 30] = [ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, + 13, 13, + ]; + let mut i = 29; + while i > 0 && BASE[i] > dist { + i -= 1; + } + (i as u32, EXTRA[i], dist - BASE[i]) +} + +const WINDOW: usize = 32768; +const MIN_MATCH: usize = 3; +const MAX_MATCH: usize = 258; +const HASH_BITS: u32 = 15; +const HASH_SIZE: usize = 1 << HASH_BITS; +/// How many candidates at one hash to try. Bounds the worst case on input +/// with long runs of the same three bytes, which a `.sla` symbol table has +/// plenty of, at a small cost in ratio. +const MAX_CHAIN: usize = 128; + +fn hash3(a: u8, b: u8, c: u8) -> usize { + // Any spreading function will do; this is the usual multiply-shift. + let v = (u32::from(a) << 16) | (u32::from(b) << 8) | u32::from(c); + ((v.wrapping_mul(0x9e37_79b1)) >> (32 - HASH_BITS)) as usize +} + +/// Longest match for the string at `pos`, searching the chain. Returns the +/// length and the distance back. +fn best_match( + data: &[u8], + pos: usize, + head: &[u32], + prev: &[u32], + nice: usize, +) -> Option<(usize, usize)> { + if pos + MIN_MATCH > data.len() { + return None; + } + let limit = (data.len() - pos).min(MAX_MATCH); + let h = hash3(data[pos], data[pos + 1], data[pos + 2]); + let mut cand = head[h]; + let floor = pos.saturating_sub(WINDOW); + let mut best = (0usize, 0usize); + let mut tries = 0; + while cand != u32::MAX { + let c = cand as usize; + if c < floor || c >= pos { + break; + } + // Nothing can beat a match that already reaches the cap. + if best.0 >= limit { + break; + } + // Only the byte past the current best can extend it, so check it + // first and skip the whole compare when it cannot. + if best.0 == 0 || data[c + best.0] == data[pos + best.0] { + let mut n = 0; + while n < limit && data[c + n] == data[pos + n] { + n += 1; + } + if n > best.0 { + best = (n, pos - c); + if n >= nice { + break; + } + } + } + tries += 1; + if tries >= MAX_CHAIN { + break; + } + cand = prev[c]; + } + (best.0 >= MIN_MATCH).then_some(best) +} + +fn fixed_blocks(out: &mut Vec, data: &[u8]) { + let mut w = BitWriter { out, bits: 0, n: 0 }; + // One block for the whole input. Splitting would only help if the fixed + // tables were being re-chosen, and they are not. + w.put(1, 1); // BFINAL + w.put(1, 2); // BTYPE = fixed Huffman + + let mut head = vec![u32::MAX; HASH_SIZE]; + let mut prev = vec![u32::MAX; data.len()]; + let mut pos = 0usize; + + let insert = |head: &mut Vec, prev: &mut Vec, at: usize| { + if at + MIN_MATCH <= data.len() { + let h = hash3(data[at], data[at + 1], data[at + 2]); + prev[at] = head[h]; + head[h] = at as u32; + } + }; + + let emit_literal = |w: &mut BitWriter, b: u8| { + let (code, n) = fixed_lit(u32::from(b)); + w.put_code(code, n); + }; + + while pos < data.len() { + let m = best_match(data, pos, &head, &prev, 32); + // Lazy matching: a match one byte later that is strictly longer is + // worth the extra literal, which is the cheapest large win DEFLATE + // has and costs one extra search per position. + let m = match m { + Some((len, dist)) if len < MAX_MATCH => { + let next = best_match(data, pos + 1, &head, &prev, 32); + match next { + Some((len2, _)) if len2 > len => None, + _ => Some((len, dist)), + } + } + other => other, + }; + + match m { + Some((len, dist)) => { + let (lc, lextra, lval) = length_code(len as u32); + let (code, n) = fixed_lit(lc); + w.put_code(code, n); + if lextra > 0 { + w.put(lval, lextra); + } + let (dc, dextra, dval) = distance_code(dist as u32); + w.put_code(dc, 5); + if dextra > 0 { + w.put(dval, dextra); + } + for i in 0..len { + insert(&mut head, &mut prev, pos + i); + } + pos += len; + } + None => { + emit_literal(&mut w, data[pos]); + insert(&mut head, &mut prev, pos); + pos += 1; + } + } + } + + let (code, n) = fixed_lit(256); // end of block + w.put_code(code, n); + w.flush(); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::inflate::inflate_zlib; + + fn round(data: &[u8], level: Level) { + let z = deflate_zlib(data, level); + let back = inflate_zlib(&z, 1 << 24).expect("our own inflate reads it"); + assert_eq!(back, data, "level {level:?}"); + } + + #[test] + fn adler_matches_the_rfc_example() { + // RFC 1950 defines the initial value as 1, so the empty string is 1. + assert_eq!(adler32(b""), 1); + assert_eq!(adler32(b"a"), 0x0062_0062); + assert_eq!(adler32(b"Wikipedia"), 0x11E6_0398); + } + + #[test] + fn stored_round_trips() { + round(b"", Level::Stored); + round(b"x", Level::Stored); + round(&vec![0xab; 200_000], Level::Stored); + } + + #[test] + fn fixed_round_trips() { + round(b"", Level::Fixed); + round(b"x", Level::Fixed); + round(b"the quick brown fox jumps over the lazy dog", Level::Fixed); + round(&vec![0xab; 200_000], Level::Fixed); + } + + #[test] + fn fixed_round_trips_over_every_byte_value() { + // The 8- and 9-bit halves of the fixed literal table are a place a + // writer can be wrong for only some inputs, so cover all 256. + let data: Vec = (0..=255u8).cycle().take(10_000).collect(); + round(&data, Level::Fixed); + } + + #[test] + fn fixed_round_trips_over_long_matches() { + // Exercises the top of the length table and distances past 16 KB. + let mut data: Vec = (0..60_000u32).map(|i| (i * 7) as u8).collect(); + let tail = data[0..20_000].to_vec(); + data.extend_from_slice(&tail); + round(&data, Level::Fixed); + } + + #[test] + fn fixed_actually_compresses() { + let data: Vec = b"sleigh ".iter().copied().cycle().take(100_000).collect(); + let z = deflate_zlib(&data, Level::Fixed); + assert!(z.len() < data.len() / 20, "got {} bytes", z.len()); + } + + #[test] + fn random_shapes_survive() { + // A cheap deterministic generator: no dependency, and the point is + // coverage of the matcher's corner cases rather than randomness. + let mut state = 0x1234_5678u32; + for case in 0..40 { + let n = 1 + case * 97; + let data: Vec = (0..n) + .map(|_| { + state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + // Small alphabet, so matches are common. + (state >> 24) as u8 % (1 + (case % 7) as u8) + }) + .collect(); + round(&data, Level::Fixed); + } + } +} diff --git a/crates/e5r-sla/src/emit.rs b/crates/e5r-sla/src/emit.rs new file mode 100644 index 0000000..7866ebb --- /dev/null +++ b/crates/e5r-sla/src/emit.rs @@ -0,0 +1,584 @@ +//! Turning a [`Program`] back into the tagged element tree a `.sla` holds. +//! +//! [`crate::encode`] writes a tree; this decides what the tree is. It is the +//! structured half of the writer and the half where the format document is +//! actually load bearing: which element carries which field, in which order, +//! with which value type. Getting any of that wrong produces a file our own +//! reader still likes, so the gate is not our reader. The gate is that every +//! `.sla` Ghidra ships, read into a [`Program`] and written back out through +//! here, is byte for byte the file it came from. +//! +//! Two things that gate found and that a writer has to get right: +//! +//! * the value type matters. An offset is written as an unsigned integer and a +//! size as a signed one, in the same element, and the bytes differ. Every +//! choice here was read off the corpus, one `(element, attribute)` pair at a +//! time. +//! * an absent element is not an empty one. A pattern with no context half and +//! a pattern whose context half holds no blocks are different files, so the +//! model records which it saw rather than flattening them. + +use crate::decode::{Node, Value}; +use crate::encode::Build; +use crate::ids::{at, el}; +use crate::model::{ + ConstTemplate, ConstructTemplate, Constructor, ContextField, ContextOp, Decision, Expr, + FieldDef, OpTemplate, Pattern, PatternBlock, PrintPiece, Program, Space, SpaceKind, Symbol, + SymbolBody, TokenField, VarnodeTemplate, +}; + +/// Something in the model that has no representation in the file. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EmitError { + /// A symbol the reader kept as an unnamed element. Its children were not + /// interpreted, so they cannot be written back. + UnknownSymbolBody { id: u32, element: u32 }, + /// A pattern expression or template leaf whose element id has no + /// established meaning, for the same reason. + UnknownElement { element: u32 }, +} + +impl core::fmt::Display for EmitError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::UnknownSymbolBody { id, element } => { + write!( + f, + "symbol {id} has an uninterpreted body, element {element}" + ) + } + Self::UnknownElement { element } => { + write!(f, "element {element} has no established meaning") + } + } + } +} + +impl std::error::Error for EmitError {} + +type R = Result; + +/// The body element id for a symbol, and the header element id, which is one +/// above it in every pair. +fn body_element(b: &SymbolBody) -> u32 { + match b { + SymbolBody::Operand { .. } => el::OPERAND_SYM, + SymbolBody::Varnode { .. } => el::VARNODE_SYM, + SymbolBody::UserOp { .. } => el::USEROP_SYM, + SymbolBody::Value { .. } => el::VALUE_SYM, + SymbolBody::Context { .. } => el::CONTEXT_SYM, + SymbolBody::End => el::END_SYM, + SymbolBody::Name { .. } => el::NAME_SYM, + SymbolBody::Next2 => el::NEXT2_SYM, + SymbolBody::Start => el::START_SYM, + SymbolBody::Subtable { .. } => el::SUBTABLE_SYM, + SymbolBody::ValueMap { .. } => el::VALUEMAP_SYM, + SymbolBody::VarnodeList { .. } => el::VARNODE_LIST_SYM, + SymbolBody::Unknown { element } => *element, + } +} + +/// Build the whole tree for a program. +/// +/// # Errors +/// Returns [`EmitError`] when the model holds a region the reader kept +/// uninterpreted, since there is nothing to write for it. +pub fn tree(p: &Program) -> R { + let mut root = Build::new(el::SLEIGH) + .attr( + at::VERSION, + Value::Signed(i128::from(p.version.unwrap_or(4))), + ) + .bool(at::BIGENDIAN, p.big_endian) + .attr(at::ALIGN, Value::Signed(i128::from(p.alignment))) + .attr(at::UNIQBASE, Value::Unsigned(u128::from(p.unique_base))); + for (id, v) in &p.extra_root_attrs { + root = root.attr(*id, v.clone()); + } + + let mut files = Build::new(el::SOURCEFILES); + for f in &p.source_files { + files.push( + Build::new(el::SOURCEFILE) + .text(at::NAME, f.name.clone()) + .attr(at::INDEX, Value::Signed(i128::from(f.index))), + ); + } + root = root.child(files); + + let mut spaces = Build::new(el::SPACES); + if let Some(d) = &p.default_space { + spaces = spaces.text(at::DEFAULTSPACE, d.clone()); + } + for s in &p.spaces { + spaces.push(space(s)); + } + root = root.child(spaces); + + let mut table = Build::new(el::SYMBOL_TABLE); + if let Some(n) = p.declared_scopes { + table = table.attr(at::SCOPESIZE, Value::Signed(i128::from(n))); + } + if let Some(n) = p.declared_symbols { + table = table.attr(at::SYMBOLSIZE, Value::Signed(i128::from(n))); + } + for sc in &p.scopes { + table.push( + Build::new(el::SCOPE) + .attr(at::ID, Value::Unsigned(u128::from(sc.id))) + .attr(at::PARENT, Value::Unsigned(u128::from(sc.parent))), + ); + } + // Headers first, then bodies: two runs, not one interleaved one. + for s in &p.symbols { + table.push(header(s)); + } + for s in &p.symbols { + table.push(body(s)?); + } + Ok(root.child(table).finish()) +} + +fn space(s: &Space) -> Build { + let id = match s.kind { + SpaceKind::Normal => el::SPACE, + SpaceKind::Other => el::SPACE_OTHER, + SpaceKind::Unique => el::SPACE_UNIQUE, + }; + let mut b = Build::new(id) + .text(at::NAME, s.name.clone()) + .attr(at::INDEX, Value::Signed(i128::from(s.index))) + .bool(at::BIGENDIAN, s.big_endian) + .attr(at::DELAY, Value::Signed(i128::from(s.delay))) + .attr(at::SIZE, Value::Signed(i128::from(s.size))); + // A word size of one is the default and no file writes it. + if s.word_size != 1 { + b = b.attr(at::WORDSIZE, Value::Signed(i128::from(s.word_size))); + } + b.bool(at::PHYSICAL, s.physical) +} + +fn header(s: &Symbol) -> Build { + Build::new(body_element(&s.body) + 1) + .text(at::NAME, s.name.clone().unwrap_or_default()) + .attr(at::ID, Value::Unsigned(u128::from(s.id))) + .attr(at::SCOPE, Value::Unsigned(u128::from(s.scope.unwrap_or(0)))) +} + +fn token_field(f: &TokenField) -> Build { + Build::new(el::TOKEN_FIELD) + .bool(at::BIGENDIAN, f.big_endian) + .bool(at::SIGNBIT, f.signed) + .attr(at::STARTBIT, Value::Signed(i128::from(f.start_bit))) + .attr(at::ENDBIT, Value::Signed(i128::from(f.end_bit))) + .attr(at::STARTBYTE, Value::Signed(i128::from(f.start_byte))) + .attr(at::ENDBYTE, Value::Signed(i128::from(f.end_byte))) + .attr(at::SHIFT, Value::Signed(i128::from(f.shift))) +} + +fn context_field(f: &ContextField) -> Build { + Build::new(el::CONTEXT_FIELD) + .bool(at::SIGNBIT, f.signed) + .attr(at::STARTBIT, Value::Signed(i128::from(f.start_bit))) + .attr(at::ENDBIT, Value::Signed(i128::from(f.end_bit))) + .attr(at::STARTBYTE, Value::Signed(i128::from(f.start_byte))) + .attr(at::ENDBYTE, Value::Signed(i128::from(f.end_byte))) + .attr(at::SHIFT, Value::Signed(i128::from(f.shift))) +} + +fn field(f: &FieldDef) -> Build { + match f { + FieldDef::Token(t) => token_field(t), + FieldDef::Context(c) => context_field(c), + } +} + +fn expr(e: &Expr) -> R { + Ok(match e { + Expr::OperandValue { index, table, ct } => Build::new(el::OPERAND_VALUE) + .attr(at::INDEX, Value::Signed(i128::from(*index))) + .attr(at::TABLE, Value::Unsigned(u128::from(*table))) + .attr(at::CT, Value::Unsigned(u128::from(*ct))), + Expr::Constant(v) => Build::new(el::PEXP_CONSTANT).attr(at::VAL, Value::Signed(*v)), + Expr::Context(c) => context_field(c), + Expr::TokenField(t) => token_field(t), + Expr::InstStart => Build::new(el::PEXP_INST_START), + Expr::InstNext => Build::new(el::PEXP_INST_NEXT), + Expr::InstNext2 => Build::new(el::PEXP_INST_NEXT2), + Expr::Op { op, operands } => { + let mut b = Build::new(op.element()); + for o in operands { + b.push(expr(o)?); + } + b + } + Expr::Unknown { element, .. } => { + return Err(EmitError::UnknownElement { element: *element }); + } + }) +} + +fn const_template(c: &ConstTemplate) -> R { + Ok(match c { + ConstTemplate::Real(v) => Build::new(el::CONST_REAL).attr( + at::VAL, + Value::Unsigned(u128::try_from(*v).unwrap_or_default()), + ), + ConstTemplate::SpaceId(ix) => Build::new(el::CONST_SPACEID).space(at::SPACE, *ix), + ConstTemplate::Handle { + index, + select, + extra, + } => { + let b = Build::new(el::CONST_HANDLE) + .attr(at::VAL, Value::Signed(i128::from(*index))) + .attr(at::SELECT, Value::Signed(i128::from(*select))); + match extra { + Some(x) => b.attr(at::HANDLE_28, Value::Unsigned(*x)), + None => b, + } + } + ConstTemplate::Relative(v) => Build::new(el::CONST_RELATIVE).attr( + at::VAL, + Value::Unsigned(u128::try_from(*v).unwrap_or_default()), + ), + ConstTemplate::InstStart => Build::new(el::CONST_INST_START), + ConstTemplate::InstNext => Build::new(el::CONST_INST_NEXT), + ConstTemplate::InstNext2 => Build::new(el::CONST_INST_NEXT2), + ConstTemplate::CurSpace => Build::new(el::CONST_CURSPACE), + ConstTemplate::CurSpaceSize => Build::new(el::CONST_CURSPACE_SIZE), + ConstTemplate::Unknown(element) => { + return Err(EmitError::UnknownElement { element: *element }); + } + }) +} + +fn varnode_template(v: &VarnodeTemplate) -> R { + Ok(Build::new(el::VARNODE_TPL) + .child(const_template(&v.space)?) + .child(const_template(&v.offset)?) + .child(const_template(&v.size)?)) +} + +fn op_template(o: &OpTemplate) -> R { + let mut b = Build::new(el::OP_TPL).attr(at::CODE, Value::Signed(i128::from(o.opcode))); + match &o.output { + Some(v) => b.push(varnode_template(v)?), + None => b.push(Build::new(el::NULL)), + } + for i in &o.inputs { + b.push(varnode_template(i)?); + } + Ok(b) +} + +fn construct_template(t: &ConstructTemplate) -> R { + let mut b = Build::new(el::CONSTRUCT_TPL); + if let Some(d) = t.delay { + b = b.attr(at::DELAY, Value::Signed(i128::from(d))); + } + if let Some(s) = t.section { + b = b.attr(at::SECTION, Value::Signed(i128::from(s))); + } + if let Some(l) = t.labels { + b = b.attr(at::LABELS, Value::Signed(i128::from(l))); + } + // The export slot is always written, and always first: a handle when the + // constructor exports one, an explicit `null` when it does not. All + // 128,421 of them in the corpus have it, so an absent slot is not a shape + // the format has. + match &t.result { + Some(parts) => { + let mut h = Build::new(el::HANDLE_TPL); + for c in parts { + h.push(const_template(c)?); + } + b.push(h); + } + None => b.push(Build::new(el::NULL)), + } + for o in &t.ops { + b.push(op_template(o)?); + } + Ok(b) +} + +fn pattern_block(p: &PatternBlock) -> Build { + let mut b = Build::new(el::PATTERN_BLOCK) + .attr(at::OFF, Value::Signed(i128::from(p.offset))) + .attr(at::NBYTES, Value::Signed(i128::from(p.bytes))); + for w in &p.words { + b.push( + Build::new(el::PATTERN_WORD) + .attr(at::MASK, Value::Unsigned(u128::from(w.mask))) + .attr(at::VAL, Value::Unsigned(u128::from(w.value))), + ); + } + b +} + +fn half(id: u32, blocks: &[PatternBlock]) -> Build { + let mut b = Build::new(id); + for blk in blocks { + b.push(pattern_block(blk)); + } + b +} + +fn pattern(p: &Pattern) -> Build { + if p.combined { + return Build::new(el::COMBINE_PATTERN) + .child(half(el::CONTEXT_PATTERN, &p.context)) + .child(half(el::INSTRUCTION_PATTERN, &p.instruction)); + } + if p.has_context { + return half(el::CONTEXT_PATTERN, &p.context); + } + half(el::INSTRUCTION_PATTERN, &p.instruction) +} + +fn decision(d: &Decision) -> Build { + let mut b = Build::new(el::DECISION) + .attr(at::NUMBER, Value::Signed(i128::from(d.number))) + .bool(at::CONTEXT, d.on_context) + .attr(at::STARTBIT, Value::Signed(i128::from(d.start_bit))) + .attr(at::SIZE, Value::Signed(i128::from(d.num_bits))); + for c in &d.children { + b.push(decision(c)); + } + for (ix, pat) in &d.pairs { + b.push( + Build::new(el::DECISION_PAIR) + .attr(at::ID, Value::Signed(i128::from(*ix))) + .child(pattern(pat)), + ); + } + b +} + +fn constructor(c: &Constructor) -> R { + let mut b = Build::new(el::CONSTRUCTOR) + .attr(at::PARENT, Value::Unsigned(u128::from(c.parent))) + .attr(at::FLOWTHRU, Value::Signed(i128::from(c.flowthru))) + .attr(at::LENGTH, Value::Signed(i128::from(c.length))) + .attr(at::SOURCE, Value::Signed(i128::from(c.source))) + .attr(at::LINE, Value::Signed(i128::from(c.line))); + for o in &c.operands { + b.push(Build::new(el::CONSTRUCTOR_OPERAND).attr(at::ID, Value::Unsigned(u128::from(*o)))); + } + for piece in &c.print { + b.push(match piece { + PrintPiece::Literal(s) => Build::new(el::PRINT_LITERAL).text(at::PIECE, s.clone()), + PrintPiece::Operand(i) => { + Build::new(el::PRINT_OPERAND).attr(at::ID, Value::Signed(i128::from(*i))) + } + }); + } + for op in &c.context_ops { + b.push(match op { + ContextOp::Set { + word, + shift, + mask, + value, + } => { + let mut n = Build::new(el::CONTEXT_CHANGE) + .attr(at::WORD, Value::Signed(i128::from(*word))) + .attr(at::SHIFT, Value::Signed(i128::from(*shift))) + .attr(at::MASK, Value::Unsigned(u128::from(*mask))); + for v in value { + n.push(expr(v)?); + } + n + } + ContextOp::Commit { + symbol, + word, + mask, + flow, + } => Build::new(el::GLOBALSET) + .attr(at::ID, Value::Unsigned(u128::from(*symbol))) + .attr(at::NUMBER, Value::Signed(i128::from(*word))) + .attr(at::MASK, Value::Unsigned(u128::from(*mask))) + .bool(at::FLOW, *flow), + }); + } + for t in &c.templates { + b.push(construct_template(t)?); + } + Ok(b) +} + +fn body(s: &Symbol) -> R { + let id = Value::Unsigned(u128::from(s.id)); + Ok(match &s.body { + SymbolBody::Varnode { + space, + offset, + size, + } => Build::new(el::VARNODE_SYM) + .attr(at::ID, id) + .space(at::SPACE, *space) + .attr(at::OFF, Value::Unsigned(u128::from(*offset))) + .attr(at::SIZE, Value::Signed(i128::from(*size))), + SymbolBody::UserOp { index } => Build::new(el::USEROP_SYM) + .attr(at::ID, id) + .attr(at::INDEX, Value::Signed(i128::from(*index))), + SymbolBody::Value { field: f } => { + let mut b = Build::new(el::VALUE_SYM).attr(at::ID, id); + if let Some(f) = f { + b.push(field(f)); + } + b + } + SymbolBody::ValueMap { field: f, values } => { + let mut b = Build::new(el::VALUEMAP_SYM).attr(at::ID, id); + if let Some(f) = f { + b.push(field(f)); + } + for v in values { + b.push(Build::new(el::VALUEMAP_ENTRY).attr(at::VAL, Value::Signed(*v))); + } + b + } + SymbolBody::Name { field: f, names } => { + let mut b = Build::new(el::NAME_SYM).attr(at::ID, id); + if let Some(f) = f { + b.push(field(f)); + } + for n in names { + b.push(match n { + Some(text) => Build::new(el::NAME_ENTRY).text(at::NAME, text.clone()), + None => Build::new(el::NAME_ENTRY), + }); + } + b + } + SymbolBody::VarnodeList { field: f, entries } => { + let mut b = Build::new(el::VARNODE_LIST_SYM).attr(at::ID, id); + if let Some(f) = f { + b.push(field(f)); + } + for e in entries { + b.push(match e { + Some(v) => Build::new(el::VARNODE_LIST_ENTRY) + .attr(at::ID, Value::Unsigned(u128::from(*v))), + None => Build::new(el::NULL), + }); + } + b + } + SymbolBody::Context { + varnode, + low, + high, + flow, + field: f, + } => { + let mut b = Build::new(el::CONTEXT_SYM) + .attr(at::ID, id) + .attr(at::VARNODE, Value::Unsigned(u128::from(*varnode))) + .attr(at::LOW, Value::Signed(i128::from(*low))) + .attr(at::HIGH, Value::Signed(i128::from(*high))) + .bool(at::FLOW, *flow); + if let Some(f) = f { + b.push(context_field(f)); + } + b + } + SymbolBody::Operand { + index, + offset, + sub_symbol, + base, + min_length, + flag, + expr: exprs, + } => { + let mut b = Build::new(el::OPERAND_SYM).attr(at::ID, id); + if let Some(sub) = sub_symbol { + b = b.attr(at::SUBSYM, Value::Unsigned(u128::from(*sub))); + } + b = b + .attr(at::OFF, Value::Signed(i128::from(*offset))) + .attr(at::OPERAND_19, Value::Signed(i128::from(*base))) + .attr(at::OPERAND_18, Value::Signed(i128::from(*min_length))); + if let Some(f) = flag { + b = b.bool(at::CODE, *f); + } + b = b.attr(at::INDEX, Value::Signed(i128::from(*index))); + for e in exprs { + b.push(expr(e)?); + } + b + } + SymbolBody::Start => Build::new(el::START_SYM).attr(at::ID, id), + SymbolBody::End => Build::new(el::END_SYM).attr(at::ID, id), + SymbolBody::Next2 => Build::new(el::NEXT2_SYM).attr(at::ID, id), + SymbolBody::Subtable { + constructors, + decision: dec, + } => { + let mut b = Build::new(el::SUBTABLE_SYM) + .attr(at::ID, id) + .attr(at::NUMCT, Value::Signed(constructors.len() as i128)); + for c in constructors { + b.push(constructor(c)?); + } + if let Some(d) = dec { + b.push(decision(d)); + } + b + } + SymbolBody::Unknown { element } => { + return Err(EmitError::UnknownSymbolBody { + id: s.id, + element: *element, + }); + } + }) +} + +/// Rebuild a whole `.sla` file image from a program. +/// +/// # Errors +/// See [`tree`], plus anything the tag encoding cannot carry. +pub fn write(p: &Program, version: u8, level: crate::Level) -> Result, crate::SlaError> { + let t = tree(p).map_err(crate::SlaError::Emit)?; + crate::write_sla(&t, version, level) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Sla; + + fn fixture(name: &str) -> Sla { + let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/data") + .join(name); + Sla::open(p).expect("fixture reads") + } + + /// The claim in the smallest possible form: what the reference compiler + /// wrote for this spec is what we write for the model of it. + #[test] + fn a_fixture_rebuilds_to_the_same_payload() { + for name in ["minimal.sla", "tokens.sla", "subtable.sla"] { + let sla = fixture(name); + let rebuilt = tree(&sla.program).expect("emits"); + let bytes = crate::encode::encode(&rebuilt).expect("encodes"); + let original = crate::encode::encode(&sla.tree).expect("encodes"); + assert_eq!(bytes, original, "{name}"); + } + } + + #[test] + fn a_rebuilt_file_reads_back() { + let sla = fixture("tokens.sla"); + let bytes = write(&sla.program, 4, crate::Level::Fixed).expect("writes"); + let back = Sla::parse(&bytes).expect("reads"); + assert_eq!(back.program.symbols.len(), sla.program.symbols.len()); + assert!(back.check().is_empty()); + } +} diff --git a/crates/e5r-sla/src/encode.rs b/crates/e5r-sla/src/encode.rs new file mode 100644 index 0000000..3796675 --- /dev/null +++ b/crates/e5r-sla/src/encode.rs @@ -0,0 +1,363 @@ +//! Writing the tagged element tree back out as `.sla` payload bytes. +//! +//! This is the exact inverse of [`crate::decode`], and it is held to that +//! standard: the corpus test re-encodes every `.sla` Ghidra ships and requires +//! the bytes back. That is what makes the encoding claims in +//! `docs/sla-format.md` testable in the writing direction and not only the +//! reading one, and it is the foundation everything above it stands on, since +//! a writer that emits the right tree but the wrong bytes would look correct +//! to our own reader and to nothing else. +//! +//! Two choices the format leaves open have to be made the same way the +//! reference compiler makes them, or the bytes differ even when the tree does +//! not: +//! +//! * an id below 32 uses the short tag form, and anything else the extended +//! form with the fewest seven-bit chunks that hold it; +//! * an integer value uses the fewest chunks that hold its magnitude, so zero +//! is a count of zero and no payload at all. +//! +//! Both were read off the shipped files and both are checked by the re-encode. + +use crate::decode::{Node, Value}; + +/// Tag kinds in the short form, where the id is the low five bits. +const T_ELEMENT_START: u8 = 0b010 << 5; +const T_ELEMENT_END: u8 = 0b100 << 5; +const T_ATTRIBUTE: u8 = 0b110 << 5; +/// The extended form of each, where the low four bits are one less than the +/// number of seven-bit id chunks that follow. +const T_ELEMENT_START_EXT: u8 = 0b011 << 5; +const T_ELEMENT_END_EXT: u8 = 0b101 << 5; +const T_ATTRIBUTE_EXT: u8 = 0b111 << 5; + +/// The largest id the extended form can carry: sixteen chunks of seven bits. +/// Ghidra's own ids stop at 84, so anything near this is a caller's bug. +pub const MAX_ID: u32 = u32::MAX; + +/// Something that cannot be written. These are all caller errors rather than +/// data errors, but they are returned rather than asserted because the +/// compiler builds trees from a specification, which is input. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EncodeError { + /// A string attribute longer than the payload limit allows. + StringTooLong { len: usize }, + /// A value the tag encoding has no room for. + ValueTooWide, + /// Nesting past [`crate::decode::MAX_DEPTH`], which the reader refuses, so + /// writing it would produce a file we could not read back. + TooDeep, +} + +impl core::fmt::Display for EncodeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::StringTooLong { len } => write!(f, "string of {len} bytes is too long"), + Self::ValueTooWide => f.write_str("value does not fit the chunk encoding"), + Self::TooDeep => write!(f, "nesting past {}", crate::decode::MAX_DEPTH), + } + } +} + +impl std::error::Error for EncodeError {} + +/// How many seven-bit chunks a magnitude needs. Zero needs none: the shipped +/// files encode a zero as a bare type byte with a count of zero. +fn chunk_count(v: u128) -> usize { + let mut n = 0; + let mut rest = v; + while rest != 0 { + n += 1; + rest >>= 7; + } + n +} + +/// Write `v` as `n` seven-bit chunks, most significant first, each with the +/// top bit set. The top bit is not part of the value; it is what keeps a chunk +/// out of the tag ranges. +fn push_chunks(out: &mut Vec, v: u128, n: usize) { + for i in (0..n).rev() { + out.push(0x80 | ((v >> (i * 7)) as u8 & 0x7f)); + } +} + +/// Write a tag: the short form when the id fits in five bits, the extended +/// form otherwise. +fn push_tag(out: &mut Vec, short: u8, ext: u8, id: u32) { + if id < 32 { + out.push(short | id as u8); + return; + } + let n = chunk_count(u128::from(id)).max(1); + // The count nibble holds one less than the real count, so a count of + // sixteen is the ceiling and 112 bits is far past any id. + out.push(ext | ((n - 1) as u8 & 0x0f)); + push_chunks(out, u128::from(id), n); +} + +/// Value type codes. The reader's names, from `docs/sla-format.md`. +const V_BOOL: u8 = 1; +const V_SIGNED_POS: u8 = 2; +const V_SIGNED_NEG: u8 = 3; +const V_UNSIGNED: u8 = 4; +const V_SPACE: u8 = 5; +const V_STRING: u8 = 7; + +fn push_typed(out: &mut Vec, kind: u8, magnitude: u128) -> Result<(), EncodeError> { + let n = chunk_count(magnitude); + if n > 15 { + return Err(EncodeError::ValueTooWide); + } + out.push((kind << 4) | n as u8); + push_chunks(out, magnitude, n); + Ok(()) +} + +/// Write one attribute value. +/// +/// # Errors +/// A string too long for the length encoding, or an integer wider than fifteen +/// chunks. +pub fn push_value(out: &mut Vec, v: &Value) -> Result<(), EncodeError> { + match v { + Value::Bool(b) => { + out.push((V_BOOL << 4) | u8::from(*b)); + Ok(()) + } + // Type 3 carries the magnitude of a negative number, so the sign is in + // the type code and never in the chunks. + Value::Signed(i) if *i < 0 => push_typed(out, V_SIGNED_NEG, i.unsigned_abs()), + Value::Signed(i) => push_typed(out, V_SIGNED_POS, *i as u128), + Value::Unsigned(u) => push_typed(out, V_UNSIGNED, *u), + Value::Space(s) => push_typed(out, V_SPACE, u128::from(*s)), + Value::Text(s) => { + let len = s.len(); + let n = chunk_count(len as u128); + if n > 15 { + return Err(EncodeError::StringTooLong { len }); + } + out.push((V_STRING << 4) | n as u8); + push_chunks(out, len as u128, n); + out.extend_from_slice(s.as_bytes()); + Ok(()) + } + Value::Other { kind, value } => push_typed(out, *kind, *value), + } +} + +/// Encode one element and everything under it. +/// +/// The walk is iterative rather than recursive: a real language nests about +/// fourteen deep, but the depth comes from data in the compiler's case and a +/// blown stack is not an error a caller can handle. +/// +/// # Errors +/// See [`push_value`], plus nesting past the reader's depth cap. +pub fn encode(root: &Node) -> Result, EncodeError> { + let mut out = Vec::new(); + encode_into(&mut out, root)?; + Ok(out) +} + +/// As [`encode`], appending to a buffer the caller owns. +/// +/// # Errors +/// See [`encode`]. +pub fn encode_into(out: &mut Vec, root: &Node) -> Result<(), EncodeError> { + enum Step<'a> { + Open(&'a Node), + Close(u32), + } + let mut stack = vec![Step::Open(root)]; + let mut depth = 0usize; + while let Some(step) = stack.pop() { + match step { + Step::Open(n) => { + depth += 1; + if depth > crate::decode::MAX_DEPTH { + return Err(EncodeError::TooDeep); + } + push_tag(out, T_ELEMENT_START, T_ELEMENT_START_EXT, n.id); + for (id, v) in &n.attrs { + push_tag(out, T_ATTRIBUTE, T_ATTRIBUTE_EXT, *id); + push_value(out, v)?; + } + stack.push(Step::Close(n.id)); + for c in n.children.iter().rev() { + stack.push(Step::Open(c)); + } + } + Step::Close(id) => { + depth -= 1; + push_tag(out, T_ELEMENT_END, T_ELEMENT_END_EXT, id); + } + } + } + Ok(()) +} + +/// A tree under construction. +/// +/// The compiler builds thousands of small elements and the shape of the file +/// is easier to check when the code that writes it reads like the tree in +/// `docs/sla-format.md`. Byte ranges are left at zero: they describe where a +/// node was read from, and a node that was built rather than read has no such +/// place. [`crate::Sla::parse`] fills them in when the result is read back. +#[derive(Debug, Clone)] +pub struct Build { + node: Node, +} + +impl Build { + /// A new element with no attributes and no children. + #[must_use] + pub fn new(id: u32) -> Self { + Self { + node: Node { + id, + attrs: Vec::new(), + children: Vec::new(), + start: 0, + end: 0, + }, + } + } + + /// Add an attribute. Order is preserved, and it matters: the reference + /// compiler writes each element's attributes in a fixed order and a + /// byte comparison sees any difference. + #[must_use] + pub fn attr(mut self, id: u32, v: Value) -> Self { + self.node.attrs.push((id, v)); + self + } + + /// Add a non-negative integer attribute, the common case. + #[must_use] + pub fn int(self, id: u32, v: i64) -> Self { + self.attr(id, Value::Signed(i128::from(v))) + } + + /// Add an unsigned attribute, used where a field needs all 32 or 64 bits. + #[must_use] + pub fn uint(self, id: u32, v: u64) -> Self { + self.attr(id, Value::Unsigned(u128::from(v))) + } + + #[must_use] + pub fn bool(self, id: u32, v: bool) -> Self { + self.attr(id, Value::Bool(v)) + } + + #[must_use] + pub fn text(self, id: u32, v: impl Into) -> Self { + self.attr(id, Value::Text(v.into())) + } + + /// Add a space reference, value type 5. + #[must_use] + pub fn space(self, id: u32, ix: u32) -> Self { + self.attr(id, Value::Space(ix)) + } + + #[must_use] + pub fn child(mut self, c: Build) -> Self { + self.node.children.push(c.node); + self + } + + /// Add a child in place, for loops. + pub fn push(&mut self, c: Build) { + self.node.children.push(c.node); + } + + #[must_use] + pub fn finish(self) -> Node { + self.node + } +} + +impl From for Node { + fn from(b: Build) -> Node { + b.node + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::decode::decode; + + fn round(bytes: &[u8]) { + let tree = decode(bytes).expect("decodes"); + assert_eq!(encode(&tree).expect("encodes"), bytes); + } + + #[test] + fn short_tags_come_back_the_same() { + round(&[0x44, 0xc3, 0x11, 0x84]); + } + + #[test] + fn extended_ids_come_back_the_same() { + round(&[0x60, 0xa1, 0xa0, 0xa1]); + } + + #[test] + fn strings_come_back_the_same() { + let mut b = vec![0x44, 0xcc, 0x71, 0x83]; + b.extend_from_slice(b"ram"); + b.push(0x84); + round(&b); + } + + #[test] + fn a_zero_costs_no_chunks() { + let mut out = Vec::new(); + push_value(&mut out, &Value::Unsigned(0)).unwrap(); + assert_eq!(out, vec![0x40]); + } + + #[test] + fn a_negative_uses_the_magnitude_type() { + let mut out = Vec::new(); + push_value(&mut out, &Value::Signed(-1)).unwrap(); + assert_eq!(out, vec![0x31, 0x81]); + } + + #[test] + fn a_wide_unsigned_survives_the_trip() { + let b = Build::new(33).uint(8, 0x8000_0000).finish(); + let bytes = encode(&b).unwrap(); + let back = decode(&bytes).unwrap(); + assert_eq!(back.attr(8), Some(&Value::Unsigned(0x8000_0000))); + } + + #[test] + fn a_built_tree_encodes_and_reads_back() { + let t = Build::new(33) + .int(34, 4) + .bool(35, false) + .child(Build::new(35).child(Build::new(36).text(12, "data.sinc").int(9, 0))) + .finish(); + let bytes = encode(&t).unwrap(); + let back = decode(&bytes).unwrap(); + assert_eq!(back.id, 33); + assert_eq!( + back.child(35) + .and_then(|s| s.child(36)) + .and_then(|f| f.attr(12)), + Some(&crate::Value::Text("data.sinc".into())) + ); + } + + #[test] + fn nesting_past_the_readers_cap_is_refused() { + let mut n = Build::new(1); + for _ in 0..crate::decode::MAX_DEPTH + 2 { + n = Build::new(1).child(n); + } + assert_eq!(encode(&n.finish()), Err(EncodeError::TooDeep)); + } +} diff --git a/crates/e5r-sla/src/ids.rs b/crates/e5r-sla/src/ids.rs new file mode 100644 index 0000000..162d59c --- /dev/null +++ b/crates/e5r-sla/src/ids.rs @@ -0,0 +1,292 @@ +//! Names for the element and attribute ids. +//! +//! The ids are numbers in the file; the names here are ours, worked out by +//! compiling `.slaspec` inputs and watching which number moved. Only ids whose +//! meaning was established that way are named. An id with no name is not a +//! guess waiting to happen: it is counted as an uninterpreted region, so the +//! gap between what this reader understands and what a `.sla` contains is a +//! number rather than an impression. `docs/sla-format.md` records the evidence +//! for each name and separates what was proved from what was inferred. + +/// The name for an element id, or `None` if the meaning is not established. +#[must_use] +pub fn element_name(id: u32) -> Option<&'static str> { + Some(match id { + 1 => "const_real", + 2 => "varnode_tpl", + 3 => "const_spaceid", + 4 => "const_handle", + 5 => "op_tpl", + 6 => "pattern_word", + 7 => "pattern_block", + 8 => "print_literal", + 9 => "decision_pair", + 10 => "context_pattern", + 11 => "null", + 12 => "operand_value", + 13 => "operand_sym", + 14 => "operand_sym_head", + 15 => "constructor_operand", + 16 => "decision", + 17 => "print_operand", + 18 => "instruction_pattern", + 19 => "combine_pattern", + 20 => "constructor", + 21 => "construct_tpl", + 22 => "scope", + 23 => "varnode_sym", + 24 => "varnode_sym_head", + 25 => "userop_sym", + 26 => "userop_sym_head", + 27 => "token_field", + 28 => "varnode_list_entry", + 29 => "context_field", + 30 => "handle_tpl", + 31 => "const_relative", + 32 => "context_change", + 33 => "sleigh", + 34 => "spaces", + 35 => "sourcefiles", + 36 => "sourcefile", + 37 => "space", + 38 => "symbol_table", + 39 => "value_sym", + 40 => "value_sym_head", + 41 => "context_sym", + 42 => "context_sym_head", + 43 => "end_sym", + 44 => "end_sym_head", + 45 => "space_other", + 46 => "space_unique", + 47 => "pexp_and", + 48 => "pexp_div", + 49 => "pexp_lshift", + 50 => "pexp_minus", + 51 => "pexp_mult", + 52 => "pexp_not", + 53 => "pexp_or", + 54 => "pexp_plus", + 55 => "pexp_rshift", + 56 => "pexp_sub", + 57 => "pexp_xor", + 58 => "pexp_constant", + 59 => "pexp_inst_next", + 60 => "pexp_inst_next2", + 61 => "pexp_inst_start", + 64 => "name_sym", + 65 => "name_sym_head", + 66 => "name_entry", + 67 => "next2_sym", + 68 => "next2_sym_head", + 69 => "start_sym", + 70 => "start_sym_head", + 71 => "subtable_sym", + 72 => "subtable_sym_head", + 73 => "valuemap_sym", + 74 => "valuemap_sym_head", + 75 => "valuemap_entry", + 76 => "varnode_list_sym", + 77 => "varnode_list_sym_head", + 79 => "globalset", + 80 => "const_inst_start", + 81 => "const_inst_next", + 82 => "const_inst_next2", + 83 => "const_curspace", + 84 => "const_curspace_size", + _ => return None, + }) +} + +/// The name for an attribute id, or `None` if the meaning is not established. +/// Several ids are reused with a different meaning depending on the element +/// that carries them, so these names are the common case and the model reads +/// each element's attributes in its own terms. +#[must_use] +pub fn attribute_name(id: u32) -> Option<&'static str> { + Some(match id { + 2 => "val", + 3 => "id", + 4 => "space", + 5 => "select", + 6 => "off", + 7 => "code", + 8 => "mask", + 9 => "index", + 10 => "nbytes", + 11 => "piece", + 12 => "name", + 13 => "scope", + 14 => "startbit", + 15 => "size", + 16 => "table", + 17 => "ct", + 20 => "number", + 21 => "context", + 22 => "parent", + 23 => "subsym", + 24 => "line", + 25 => "source", + 26 => "length", + 29 => "shift", + 30 => "endbit", + 31 => "signbit", + 32 => "endbyte", + 33 => "startbyte", + 34 => "version", + 35 => "bigendian", + 36 => "align", + 37 => "uniqbase", + 41 => "defaultspace", + 42 => "delay", + 43 => "wordsize", + 44 => "physical", + 45 => "scopesize", + 46 => "symbolsize", + 47 => "varnode", + 48 => "low", + 49 => "high", + 50 => "flow", + 52 => "word", + 53 => "numct", + 54 => "section", + 55 => "labels", + _ => return None, + }) +} + +/// Element ids used by the tree walk, so the model does not spell numbers +/// inline where a mistake would be silent. +pub mod el { + pub const CONST_REAL: u32 = 1; + pub const VARNODE_TPL: u32 = 2; + pub const CONST_SPACEID: u32 = 3; + pub const CONST_HANDLE: u32 = 4; + pub const OP_TPL: u32 = 5; + pub const PATTERN_WORD: u32 = 6; + pub const PATTERN_BLOCK: u32 = 7; + pub const PRINT_LITERAL: u32 = 8; + pub const DECISION_PAIR: u32 = 9; + pub const CONTEXT_PATTERN: u32 = 10; + pub const NULL: u32 = 11; + pub const OPERAND_SYM: u32 = 13; + pub const CONSTRUCTOR_OPERAND: u32 = 15; + pub const DECISION: u32 = 16; + pub const PRINT_OPERAND: u32 = 17; + pub const INSTRUCTION_PATTERN: u32 = 18; + pub const COMBINE_PATTERN: u32 = 19; + pub const CONSTRUCTOR: u32 = 20; + pub const CONSTRUCT_TPL: u32 = 21; + pub const SCOPE: u32 = 22; + pub const VARNODE_SYM: u32 = 23; + pub const USEROP_SYM: u32 = 25; + pub const TOKEN_FIELD: u32 = 27; + pub const VARNODE_LIST_ENTRY: u32 = 28; + pub const CONTEXT_FIELD: u32 = 29; + pub const HANDLE_TPL: u32 = 30; + pub const CONST_RELATIVE: u32 = 31; + pub const CONTEXT_CHANGE: u32 = 32; + pub const SLEIGH: u32 = 33; + pub const SPACES: u32 = 34; + pub const SOURCEFILES: u32 = 35; + pub const SOURCEFILE: u32 = 36; + pub const SPACE: u32 = 37; + pub const SYMBOL_TABLE: u32 = 38; + pub const VALUE_SYM: u32 = 39; + pub const CONTEXT_SYM: u32 = 41; + pub const END_SYM: u32 = 43; + pub const SPACE_OTHER: u32 = 45; + pub const SPACE_UNIQUE: u32 = 46; + pub const NAME_SYM: u32 = 64; + pub const NAME_ENTRY: u32 = 66; + pub const NEXT2_SYM: u32 = 67; + pub const START_SYM: u32 = 69; + pub const SUBTABLE_SYM: u32 = 71; + pub const VALUEMAP_SYM: u32 = 73; + pub const VALUEMAP_ENTRY: u32 = 75; + pub const VARNODE_LIST_SYM: u32 = 76; + pub const VARNODE_LIST_SYM_HEAD: u32 = 77; + pub const GLOBALSET: u32 = 79; + pub const CONST_INST_START: u32 = 80; + pub const CONST_INST_NEXT: u32 = 81; + pub const CONST_INST_NEXT2: u32 = 82; + pub const CONST_CURSPACE: u32 = 83; + pub const CONST_CURSPACE_SIZE: u32 = 84; + + /// Pattern expression operators, settled one experiment each; see + /// `docs/sla-format.md`. The run is alphabetical by the operator's name + /// in the SLEIGH source, which is why it is contiguous. + pub const PEXP_AND: u32 = 47; + pub const PEXP_DIV: u32 = 48; + pub const PEXP_LSHIFT: u32 = 49; + pub const PEXP_MINUS: u32 = 50; + pub const PEXP_MULT: u32 = 51; + pub const PEXP_NOT: u32 = 52; + pub const PEXP_OR: u32 = 53; + pub const PEXP_PLUS: u32 = 54; + pub const PEXP_RSHIFT: u32 = 55; + pub const PEXP_SUB: u32 = 56; + pub const PEXP_XOR: u32 = 57; + pub const PEXP_CONSTANT: u32 = 58; + pub const PEXP_INST_NEXT: u32 = 59; + pub const PEXP_INST_NEXT2: u32 = 60; + pub const PEXP_INST_START: u32 = 61; + pub const OPERAND_VALUE: u32 = 12; +} + +/// Attribute ids used by the tree walk. +pub mod at { + pub const VAL: u32 = 2; + pub const ID: u32 = 3; + pub const SPACE: u32 = 4; + pub const SELECT: u32 = 5; + pub const OFF: u32 = 6; + pub const CODE: u32 = 7; + pub const MASK: u32 = 8; + pub const INDEX: u32 = 9; + pub const NBYTES: u32 = 10; + pub const PIECE: u32 = 11; + pub const NAME: u32 = 12; + pub const SCOPE: u32 = 13; + pub const STARTBIT: u32 = 14; + pub const SIZE: u32 = 15; + pub const TABLE: u32 = 16; + pub const CT: u32 = 17; + pub const NUMBER: u32 = 20; + pub const CONTEXT: u32 = 21; + pub const PARENT: u32 = 22; + pub const SUBSYM: u32 = 23; + pub const LINE: u32 = 24; + pub const SOURCE: u32 = 25; + pub const LENGTH: u32 = 26; + pub const SHIFT: u32 = 29; + pub const ENDBIT: u32 = 30; + pub const SIGNBIT: u32 = 31; + pub const ENDBYTE: u32 = 32; + pub const STARTBYTE: u32 = 33; + pub const VERSION: u32 = 34; + pub const BIGENDIAN: u32 = 35; + pub const ALIGN: u32 = 36; + pub const UNIQBASE: u32 = 37; + pub const DEFAULTSPACE: u32 = 41; + pub const DELAY: u32 = 42; + pub const WORDSIZE: u32 = 43; + pub const PHYSICAL: u32 = 44; + pub const SCOPESIZE: u32 = 45; + pub const SYMBOLSIZE: u32 = 46; + pub const VARNODE: u32 = 47; + pub const LOW: u32 = 48; + pub const HIGH: u32 = 49; + pub const FLOW: u32 = 50; + pub const NUMCT: u32 = 53; + pub const SECTION: u32 = 54; + pub const LABELS: u32 = 55; + pub const WORD: u32 = 52; + /// On a constructor: where the mnemonic ends among the print pieces. + pub const FLOWTHRU: u32 = 27; + /// On an operand symbol body, the two values whose meaning is not + /// established. They are carried through so the file can be written back. + pub const OPERAND_18: u32 = 18; + pub const OPERAND_19: u32 = 19; + /// On a const_handle template leaf. + pub const HANDLE_28: u32 = 28; +} diff --git a/crates/e5r-sla/src/inflate.rs b/crates/e5r-sla/src/inflate.rs new file mode 100644 index 0000000..704826e --- /dev/null +++ b/crates/e5r-sla/src/inflate.rs @@ -0,0 +1,462 @@ +//! zlib (RFC 1950) and DEFLATE (RFC 1951) decompression. +//! +//! A `.sla` payload is a bare zlib stream, so reading one at all needs an +//! inflater. The workspace keeps its dependency list to serde, clap and +//! memmap2, so it is written here rather than pulled in. +//! +//! Every count that comes out of the stream is checked against what is left of +//! the input before anything is allocated, and the output is capped, so a +//! crafted header cannot make this allocate or spin. + +/// Ceiling on the decompressed size. The largest `.sla` shipped by Ghidra 12.1 +/// expands to 4.3 MB, so 256 MB is four and a half orders of magnitude of +/// headroom and still small enough that a zip bomb fails fast. +pub const DEFAULT_LIMIT: usize = 256 << 20; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InflateError { + /// Ran off the end of the input. + Truncated, + /// The two-byte zlib header is not a zlib header we can read. + BadZlibHeader, + /// A preset dictionary is required; `.sla` never uses one. + PresetDictionary, + /// A block type field held the reserved value 3. + ReservedBlockType, + /// A stored block's length and its complement disagree. + StoredLengthMismatch, + /// A Huffman table was over- or under-subscribed. + BadHuffmanTable, + /// A symbol that no code in the table maps to. + BadHuffmanCode, + /// A back reference pointing before the start of the output. + DistanceTooFar, + /// Output would exceed the limit. + LimitExceeded, + /// The trailing Adler-32 does not match the bytes produced. + ChecksumMismatch, +} + +impl core::fmt::Display for InflateError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let s = match self { + Self::Truncated => "compressed stream ends early", + Self::BadZlibHeader => "not a zlib header", + Self::PresetDictionary => "zlib preset dictionary not supported", + Self::ReservedBlockType => "reserved deflate block type", + Self::StoredLengthMismatch => "stored block length mismatch", + Self::BadHuffmanTable => "malformed huffman table", + Self::BadHuffmanCode => "undecodable huffman code", + Self::DistanceTooFar => "back reference before start of output", + Self::LimitExceeded => "decompressed size over limit", + Self::ChecksumMismatch => "adler-32 mismatch", + }; + f.write_str(s) + } +} + +impl std::error::Error for InflateError {} + +type Result = core::result::Result; + +struct BitReader<'a> { + data: &'a [u8], + pos: usize, + buf: u32, + have: u32, +} + +impl<'a> BitReader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { + data, + pos: 0, + buf: 0, + have: 0, + } + } + + /// Least-significant-bit-first, which is how DEFLATE packs everything + /// except the Huffman codes themselves. + fn bits(&mut self, n: u32) -> Result { + debug_assert!(n <= 16); + while self.have < n { + let b = *self.data.get(self.pos).ok_or(InflateError::Truncated)?; + self.pos += 1; + self.buf |= u32::from(b) << self.have; + self.have += 8; + } + let v = if n == 32 { + self.buf + } else { + self.buf & ((1u32 << n) - 1) + }; + self.buf >>= n; + self.have -= n; + Ok(v) + } + + fn align(&mut self) { + let drop = self.have % 8; + self.buf >>= drop; + self.have -= drop; + } + + fn byte(&mut self) -> Result { + if self.have >= 8 { + let v = (self.buf & 0xff) as u8; + self.buf >>= 8; + self.have -= 8; + return Ok(v); + } + let b = *self.data.get(self.pos).ok_or(InflateError::Truncated)?; + self.pos += 1; + Ok(b) + } + + /// Bytes not yet handed out, counting whole buffered bits. + fn remaining(&self) -> usize { + self.data.len() - self.pos + (self.have / 8) as usize + } +} + +const MAX_BITS: usize = 15; + +/// A canonical Huffman table in the count/symbol form: `counts[n]` is how many +/// codes have length `n`, and `symbols` lists the symbols in canonical order. +/// Decoding walks the lengths, which needs no table memory proportional to the +/// alphabet and cannot index out of range. +struct Huffman { + counts: [u16; MAX_BITS + 1], + symbols: Vec, +} + +impl Huffman { + fn new(lengths: &[u8]) -> Result { + let mut counts = [0u16; MAX_BITS + 1]; + for &l in lengths { + if l as usize > MAX_BITS { + return Err(InflateError::BadHuffmanTable); + } + counts[l as usize] += 1; + } + // Over-subscribed is rejected: two symbols would share a code and the + // decode would be ambiguous. Under-subscribed is accepted, because a + // distance tree with a single code is legal and common; an unused code + // then runs past fifteen bits and comes back as BadHuffmanCode rather + // than as a wrong symbol. + let mut left: i32 = 1; + for &count in counts.iter().take(MAX_BITS + 1).skip(1) { + left = left.saturating_mul(2) - i32::from(count); + if left < 0 { + return Err(InflateError::BadHuffmanTable); + } + } + let mut offs = [0u16; MAX_BITS + 2]; + for n in 1..=MAX_BITS { + offs[n + 1] = offs[n] + counts[n]; + } + let mut symbols = vec![0u16; lengths.len()]; + for (sym, &l) in lengths.iter().enumerate() { + if l != 0 { + let slot = offs[l as usize] as usize; + if slot >= symbols.len() { + return Err(InflateError::BadHuffmanTable); + } + symbols[slot] = sym as u16; + offs[l as usize] += 1; + } + } + Ok(Self { counts, symbols }) + } + + fn decode(&self, br: &mut BitReader<'_>) -> Result { + let mut code: i32 = 0; + let mut first: i32 = 0; + let mut index: i32 = 0; + for n in 1..=MAX_BITS { + code |= br.bits(1)? as i32; + let count = i32::from(self.counts[n]); + if code - first < count { + let slot = (index + (code - first)) as usize; + return self + .symbols + .get(slot) + .copied() + .ok_or(InflateError::BadHuffmanCode); + } + index += count; + first = (first + count) << 1; + code <<= 1; + } + Err(InflateError::BadHuffmanCode) + } +} + +const LEN_BASE: [u16; 29] = [ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, + 163, 195, 227, 258, +]; +const LEN_EXTRA: [u8; 29] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, +]; +const DIST_BASE: [u16; 30] = [ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, + 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, +]; +const DIST_EXTRA: [u8; 30] = [ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, + 13, +]; + +fn fixed_tables() -> Result<(Huffman, Huffman)> { + let mut lit = [0u8; 288]; + for (i, l) in lit.iter_mut().enumerate() { + *l = match i { + 0..=143 => 8, + 144..=255 => 9, + 256..=279 => 7, + _ => 8, + }; + } + // RFC 1951 gives the fixed distance alphabet 32 five-bit codes, of which + // 30 and 31 never appear in a valid stream; building only 30 would leave + // the code under-subscribed and mis-decode the last two. + Ok((Huffman::new(&lit)?, Huffman::new(&[5u8; 32])?)) +} + +fn dynamic_tables(br: &mut BitReader<'_>) -> Result<(Huffman, Huffman)> { + const ORDER: [usize; 19] = [ + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15, + ]; + let hlit = br.bits(5)? as usize + 257; + let hdist = br.bits(5)? as usize + 1; + let hclen = br.bits(4)? as usize + 4; + if hlit > 286 || hdist > 30 { + return Err(InflateError::BadHuffmanTable); + } + let mut code_lengths = [0u8; 19]; + for &slot in ORDER.iter().take(hclen) { + code_lengths[slot] = br.bits(3)? as u8; + } + let code_tree = Huffman::new(&code_lengths)?; + + let total = hlit + hdist; + let mut lengths = vec![0u8; total]; + let mut i = 0; + while i < total { + let sym = code_tree.decode(br)?; + match sym { + 0..=15 => { + lengths[i] = sym as u8; + i += 1; + } + 16 => { + if i == 0 { + return Err(InflateError::BadHuffmanTable); + } + let prev = lengths[i - 1]; + let rep = 3 + br.bits(2)? as usize; + if i + rep > total { + return Err(InflateError::BadHuffmanTable); + } + lengths[i..i + rep].fill(prev); + i += rep; + } + 17 | 18 => { + let rep = if sym == 17 { + 3 + br.bits(3)? as usize + } else { + 11 + br.bits(7)? as usize + }; + if i + rep > total { + return Err(InflateError::BadHuffmanTable); + } + lengths[i..i + rep].fill(0); + i += rep; + } + _ => return Err(InflateError::BadHuffmanCode), + } + } + if lengths[256] == 0 { + // Without an end-of-block code the block could never terminate. + return Err(InflateError::BadHuffmanTable); + } + let litlen = Huffman::new(&lengths[..hlit])?; + let dist = Huffman::new(&lengths[hlit..])?; + Ok((litlen, dist)) +} + +fn adler32(data: &[u8]) -> u32 { + let (mut a, mut b) = (1u32, 0u32); + // 5552 is the most bytes that can be summed before the 32-bit accumulator + // can overflow, so the modulo runs once per chunk rather than per byte. + for chunk in data.chunks(5552) { + for &byte in chunk { + a += u32::from(byte); + b += a; + } + a %= 65521; + b %= 65521; + } + (b << 16) | a +} + +/// Inflate a raw DEFLATE stream. `limit` caps the output. +pub fn inflate(data: &[u8], limit: usize) -> Result> { + let mut br = BitReader::new(data); + let mut out: Vec = Vec::new(); + loop { + let last = br.bits(1)? != 0; + let kind = br.bits(2)?; + match kind { + 0 => { + br.align(); + let len = u32::from(br.byte()?) | (u32::from(br.byte()?) << 8); + let nlen = u32::from(br.byte()?) | (u32::from(br.byte()?) << 8); + if len ^ 0xffff != nlen { + return Err(InflateError::StoredLengthMismatch); + } + let len = len as usize; + // Bounded before reserving: a stored block cannot claim more + // than the input still holds. + if len > br.remaining() { + return Err(InflateError::Truncated); + } + if out.len() + len > limit { + return Err(InflateError::LimitExceeded); + } + out.reserve(len); + for _ in 0..len { + out.push(br.byte()?); + } + } + 1 | 2 => { + let (litlen, dist) = if kind == 1 { + fixed_tables()? + } else { + dynamic_tables(&mut br)? + }; + loop { + let sym = litlen.decode(&mut br)?; + if sym < 256 { + if out.len() >= limit { + return Err(InflateError::LimitExceeded); + } + out.push(sym as u8); + } else if sym == 256 { + break; + } else { + let li = sym as usize - 257; + if li >= LEN_BASE.len() { + return Err(InflateError::BadHuffmanCode); + } + let length = + LEN_BASE[li] as usize + br.bits(u32::from(LEN_EXTRA[li]))? as usize; + let ds = dist.decode(&mut br)? as usize; + if ds >= DIST_BASE.len() { + return Err(InflateError::BadHuffmanCode); + } + let distance = + DIST_BASE[ds] as usize + br.bits(u32::from(DIST_EXTRA[ds]))? as usize; + if distance > out.len() { + return Err(InflateError::DistanceTooFar); + } + if out.len() + length > limit { + return Err(InflateError::LimitExceeded); + } + out.reserve(length); + // Byte at a time on purpose: a run may overlap itself, + // which is how DEFLATE encodes a repeat shorter than + // its length, and a block copy would get that wrong. + for src in (out.len() - distance..).take(length) { + let b = out[src]; + out.push(b); + } + } + } + } + _ => return Err(InflateError::ReservedBlockType), + } + if last { + break; + } + } + Ok(out) +} + +/// Inflate a zlib-wrapped stream, checking the Adler-32 trailer. +pub fn inflate_zlib(data: &[u8], limit: usize) -> Result> { + if data.len() < 6 { + return Err(InflateError::Truncated); + } + let cmf = data[0]; + let flg = data[1]; + if cmf & 0x0f != 8 || cmf >> 4 > 7 { + return Err(InflateError::BadZlibHeader); + } + if (u16::from(cmf) * 256 + u16::from(flg)) % 31 != 0 { + return Err(InflateError::BadZlibHeader); + } + if flg & 0x20 != 0 { + return Err(InflateError::PresetDictionary); + } + let body = &data[2..data.len() - 4]; + let out = inflate(body, limit)?; + let tail = &data[data.len() - 4..]; + let want = u32::from_be_bytes([tail[0], tail[1], tail[2], tail[3]]); + if adler32(&out) != want { + return Err(InflateError::ChecksumMismatch); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A stored (uncompressed) zlib stream for "hello", written by hand so the + /// test does not depend on a compressor. + #[test] + fn stored_block_round_trips() { + let payload = b"hello"; + let mut z = vec![0x78, 0x01, 0x01, 0x05, 0x00, 0xfa, 0xff]; + z.extend_from_slice(payload); + z.extend_from_slice(&adler32(payload).to_be_bytes()); + assert_eq!(inflate_zlib(&z, 1 << 20).unwrap(), payload); + } + + #[test] + fn adler_of_known_string() { + // RFC 1950's worked example. + assert_eq!(adler32(b"Wikipedia"), 0x11E6_0398); + } + + #[test] + fn rejects_bad_header() { + assert_eq!( + inflate_zlib(&[0x00, 0x00, 0, 0, 0, 0], 1024), + Err(InflateError::BadZlibHeader) + ); + } + + #[test] + fn truncation_is_an_error_not_a_panic() { + let payload = b"hello"; + let mut z = vec![0x78, 0x01, 0x01, 0x05, 0x00, 0xfa, 0xff]; + z.extend_from_slice(payload); + z.extend_from_slice(&adler32(payload).to_be_bytes()); + for n in 0..z.len() { + let _ = inflate_zlib(&z[..n], 1 << 20); + } + } + + #[test] + fn limit_is_enforced() { + let payload = b"hello"; + let mut z = vec![0x78, 0x01, 0x01, 0x05, 0x00, 0xfa, 0xff]; + z.extend_from_slice(payload); + z.extend_from_slice(&adler32(payload).to_be_bytes()); + assert_eq!(inflate_zlib(&z, 2), Err(InflateError::LimitExceeded)); + } +} diff --git a/crates/e5r-sla/src/lib.rs b/crates/e5r-sla/src/lib.rs new file mode 100644 index 0000000..19626cd --- /dev/null +++ b/crates/e5r-sla/src/lib.rs @@ -0,0 +1,406 @@ +//! Reader for Ghidra's compiled SLEIGH language files (`.sla`). +//! +//! A `.sla` is the magic `sla`, a one-byte format version, and a zlib stream +//! whose contents are a tagged element tree. None of that is published, so it +//! was worked out the way this toolkit works out any binary format: by +//! compiling `.slaspec` inputs with the shipped SLEIGH compiler, changing one +//! thing at a time, and watching which bytes moved. `docs/sla-format.md` is the +//! write-up, and it separates what was proved that way from what was inferred. +//! +//! The reader has two layers, and both are public on purpose: +//! +//! * [`decode`] turns the payload into a [`Node`] tree of numbered elements, +//! attributes and values, each node carrying its byte range. Nothing is +//! interpreted here, so a file using ids this crate has never seen still +//! reads. +//! * [`model`] turns that tree into spaces, registers, tokens, symbols and +//! constructors. Anything whose meaning is not established stays in the tree +//! as raw bytes, and [`Coverage`] reports how many bytes that is, so the gap +//! is a number rather than a feeling. +//! +//! It also writes. [`encode`] is the exact inverse of [`decode`], [`deflate`] +//! the inverse of [`inflate`], and [`write_sla`] puts a tree back in a +//! container. The gate on all three is that every `.sla` Ghidra ships +//! re-encodes to the byte it was read from. + +#![forbid(unsafe_code)] + +pub mod decode; +pub mod deflate; +pub mod emit; +pub mod encode; +pub mod ids; +pub mod inflate; +pub mod model; + +pub use decode::{DecodeError, Node, Value}; +pub use deflate::Level; +pub use emit::EmitError; +pub use encode::{Build, EncodeError}; +pub use inflate::InflateError; +pub use model::{Coverage, Program, Symbol, SymbolBody}; + +/// The four magic bytes: `sla` and a format version. Version 4 is what Ghidra +/// 12.1 writes and the only one this reader has seen. +pub const MAGIC: [u8; 3] = *b"sla"; + +/// The format version this reader was worked out against. +pub const KNOWN_VERSION: u8 = 4; + +#[derive(Debug)] +pub enum SlaError { + /// Shorter than the four-byte header. + TooShort, + /// The first three bytes are not `sla`. + BadMagic([u8; 3]), + Inflate(InflateError), + Decode(DecodeError), + /// The payload's root is not the `sleigh` element. + NotSleigh { + root: u32, + }, + Io(std::io::Error), + Encode(EncodeError), + Emit(EmitError), +} + +impl core::fmt::Display for SlaError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::TooShort => f.write_str("file is shorter than the sla header"), + Self::BadMagic(m) => write!(f, "bad magic {m:?}, expected \"sla\""), + Self::Inflate(e) => write!(f, "zlib payload: {e}"), + Self::Decode(e) => write!(f, "element stream: {e}"), + Self::NotSleigh { root } => write!(f, "root element is {root}, expected 33 (sleigh)"), + Self::Io(e) => write!(f, "{e}"), + Self::Encode(e) => write!(f, "writing element stream: {e}"), + Self::Emit(e) => write!(f, "building element tree: {e}"), + } + } +} + +impl std::error::Error for SlaError {} + +impl From for SlaError { + fn from(e: InflateError) -> Self { + Self::Inflate(e) + } +} + +impl From for SlaError { + fn from(e: DecodeError) -> Self { + Self::Decode(e) + } +} + +impl From for SlaError { + fn from(e: EncodeError) -> Self { + Self::Encode(e) + } +} + +impl From for SlaError { + fn from(e: std::io::Error) -> Self { + Self::Io(e) + } +} + +/// One loaded `.sla`. +#[derive(Debug, Clone)] +pub struct Sla { + /// The version byte after the magic. + pub format_version: u8, + /// The decompressed payload length, which is what coverage is measured + /// against. + pub payload_len: usize, + /// The whole file as numbered elements, including everything the model + /// does not interpret. + pub tree: Node, + /// The structured view. + pub program: Program, + pub coverage: Coverage, +} + +impl Sla { + /// Parse a whole `.sla` file image. + /// + /// # Errors + /// Returns a typed error for a bad header, a payload that does not inflate + /// or checksum, or an element stream that does not balance. It never + /// panics and never allocates from a count the file chose. + pub fn parse(bytes: &[u8]) -> Result { + Self::parse_with_limit(bytes, inflate::DEFAULT_LIMIT) + } + + /// As [`Sla::parse`], with an explicit ceiling on the decompressed size. + /// + /// # Errors + /// See [`Sla::parse`]. + pub fn parse_with_limit(bytes: &[u8], limit: usize) -> Result { + if bytes.len() < 4 { + return Err(SlaError::TooShort); + } + let magic: [u8; 3] = [bytes[0], bytes[1], bytes[2]]; + if magic != MAGIC { + return Err(SlaError::BadMagic(magic)); + } + let format_version = bytes[3]; + let payload = inflate::inflate_zlib(&bytes[4..], limit)?; + let tree = decode::decode(&payload)?; + if tree.id != ids::el::SLEIGH { + return Err(SlaError::NotSleigh { root: tree.id }); + } + let program = model::build(&tree); + let coverage = Coverage::measure(&tree); + Ok(Self { + format_version, + payload_len: payload.len(), + tree, + program, + coverage, + }) + } + + /// Read and parse a file. + /// + /// # Errors + /// See [`Sla::parse`], plus any I/O error from reading the path. + pub fn open(path: impl AsRef) -> Result { + let bytes = std::fs::read(path)?; + Self::parse(&bytes) + } + + /// Whether the format version is the one the reader was written against. + #[must_use] + pub fn version_is_known(&self) -> bool { + self.format_version == KNOWN_VERSION + } + + /// Write this file back out: the same version byte, the same tree, in a + /// fresh zlib stream. + /// + /// The bytes of the container will not match the file this was read from, + /// because the compressor here is not zlib's. The *payload* will match to + /// the byte, and that is the claim the corpus test makes. + /// + /// # Errors + /// Only if the tree holds something the tag encoding cannot carry, which a + /// tree that was read cannot. + pub fn to_bytes(&self, level: Level) -> Result, SlaError> { + write_sla(&self.tree, self.format_version, level) + } +} + +/// Put a tagged element tree in a `.sla` container: the magic, a version +/// byte, and the payload as a zlib stream. +/// +/// # Errors +/// See [`encode::encode`]. +pub fn write_sla(tree: &Node, version: u8, level: Level) -> Result, SlaError> { + let payload = encode::encode(tree)?; + let mut out = Vec::with_capacity(payload.len() / 2 + 16); + out.extend_from_slice(&MAGIC); + out.push(version); + out.extend_from_slice(&deflate::deflate_zlib(&payload, level)); + Ok(out) +} + +/// What a file says about itself that a reader can check against what it +/// found. Anything untrue here means the reader has the format wrong, so it is +/// worth checking rather than trusting. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Inconsistency { + /// The symbol table's count attribute and the bodies found disagree. + SymbolCount { + declared: u64, + found: usize, + }, + ScopeCount { + declared: u64, + found: usize, + }, + /// A symbol id used by something but never defined. + MissingSymbol { + referenced_by: u32, + id: u32, + }, + /// A space index used by something but not in the space table. + MissingSpace { + index: u32, + }, + /// Two symbols with the same id. + DuplicateSymbol { + id: u32, + }, + /// A symbol with no header record, so no name. + UnnamedSymbol { + id: u32, + }, + /// A node whose byte range is not inside the payload. + NodeOutOfRange { + id: u32, + start: usize, + end: usize, + }, +} + +impl Sla { + /// Every self-consistency problem found. An empty result is the claim + /// that every index in the file resolves and every offset is in range. + #[must_use] + pub fn check(&self) -> Vec { + let mut out = Vec::new(); + let p = &self.program; + + if let Some(d) = p.declared_symbols { + if d != p.symbols.len() as u64 { + out.push(Inconsistency::SymbolCount { + declared: d, + found: p.symbols.len(), + }); + } + } + if let Some(d) = p.declared_scopes { + if d != p.scopes.len() as u64 { + out.push(Inconsistency::ScopeCount { + declared: d, + found: p.scopes.len(), + }); + } + } + + let mut ids: Vec = p.symbols.iter().map(|s| s.id).collect(); + ids.sort_unstable(); + for w in ids.windows(2) { + if w[0] == w[1] { + out.push(Inconsistency::DuplicateSymbol { id: w[0] }); + } + } + + // Both lookups run once per reference, and a real language has a few + // hundred thousand of them, so neither may be a linear scan. + let has = |id: u32| p.has_symbol(id); + let mut space_ix: Vec = p.spaces.iter().map(|s| s.index).collect(); + space_ix.sort_unstable(); + let space_ok = |ix: u32| space_ix.binary_search(&ix).is_ok(); + + for s in &p.symbols { + if s.name.is_none() { + out.push(Inconsistency::UnnamedSymbol { id: s.id }); + } + match &s.body { + SymbolBody::Varnode { space, .. } => { + if !space_ok(*space) { + out.push(Inconsistency::MissingSpace { index: *space }); + } + } + SymbolBody::Context { varnode, .. } => { + if !has(*varnode) { + out.push(Inconsistency::MissingSymbol { + referenced_by: s.id, + id: *varnode, + }); + } + } + SymbolBody::VarnodeList { entries, .. } => { + for e in entries.iter().flatten() { + if !has(*e) { + out.push(Inconsistency::MissingSymbol { + referenced_by: s.id, + id: *e, + }); + } + } + } + SymbolBody::Operand { + sub_symbol: Some(sub), + .. + } => { + if !has(*sub) { + out.push(Inconsistency::MissingSymbol { + referenced_by: s.id, + id: *sub, + }); + } + } + SymbolBody::Subtable { + constructors, + decision, + } => { + for c in constructors { + if !has(c.parent) { + out.push(Inconsistency::MissingSymbol { + referenced_by: s.id, + id: c.parent, + }); + } + for o in &c.operands { + if !has(*o) { + out.push(Inconsistency::MissingSymbol { + referenced_by: s.id, + id: *o, + }); + } + } + } + if let Some(d) = decision { + check_decision(d, constructors.len(), s.id, &mut out); + } + } + _ => {} + } + } + + let len = self.payload_len; + self.tree.visit(&mut |n| { + if n.end > len || n.start > n.end { + out.push(Inconsistency::NodeOutOfRange { + id: n.id, + start: n.start, + end: n.end, + }); + } + }); + out + } +} + +fn check_decision(d: &model::Decision, ctors: usize, owner: u32, out: &mut Vec) { + for (ix, _) in &d.pairs { + if *ix as usize >= ctors { + out.push(Inconsistency::MissingSymbol { + referenced_by: owner, + id: *ix, + }); + } + } + for c in &d.children { + check_decision(c, ctors, owner, out); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_short_file_is_an_error() { + assert!(matches!(Sla::parse(b"sla"), Err(SlaError::TooShort))); + } + + #[test] + fn bad_magic_is_an_error() { + assert!(matches!( + Sla::parse(b"xxxx....."), + Err(SlaError::BadMagic(_)) + )); + } + + #[test] + fn every_byte_prefix_of_a_header_is_handled() { + let junk = b"sla\x04\x78\x9c\x00\x01\x02\x03"; + for n in 0..junk.len() { + let _ = Sla::parse(&junk[..n]); + } + } +} diff --git a/crates/e5r-sla/src/model.rs b/crates/e5r-sla/src/model.rs new file mode 100644 index 0000000..533773d --- /dev/null +++ b/crates/e5r-sla/src/model.rs @@ -0,0 +1,1085 @@ +//! The structured view of a `.sla`: spaces, registers, tokens, symbol tables, +//! constructors with their patterns and their p-code templates. +//! +//! Every field here is one this reader established by experiment; see +//! `docs/sla-format.md`. Anything else stays in [`crate::Sla::tree`] as raw +//! nodes with their offsets, and [`Coverage`] says how much that is. + +use crate::decode::{Node, Value}; +use crate::ids::{at, el, element_name}; + +/// One entry of the `` list. Only files that define a constructor +/// appear, which is why the index is stored rather than assumed positional. +#[derive(Debug, Clone)] +pub struct SourceFile { + pub name: String, + pub index: u64, +} + +/// An address space. +#[derive(Debug, Clone)] +pub struct Space { + /// Which element carried it: `space`, `space_other` or `space_unique`. + pub kind: SpaceKind, + pub name: String, + /// Index used by `Value::Space` references elsewhere in the file. + pub index: u32, + pub big_endian: bool, + pub delay: u64, + pub size: u64, + pub word_size: u64, + pub physical: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SpaceKind { + Normal, + Other, + Unique, +} + +/// A symbol scope. Scope 0 is the global one; a constructor's operands live in +/// a scope of their own. +#[derive(Debug, Clone)] +pub struct Scope { + pub id: u32, + pub parent: u32, +} + +/// A field cut out of an instruction token. +#[derive(Debug, Clone)] +pub struct TokenField { + pub big_endian: bool, + pub signed: bool, + pub start_bit: u64, + pub end_bit: u64, + pub start_byte: u64, + pub end_byte: u64, + pub shift: u64, +} + +/// A field cut out of the context register. The same layout as a token field +/// without the endianness, since the context register has only one. +#[derive(Debug, Clone)] +pub struct ContextField { + pub signed: bool, + pub start_bit: u64, + pub end_bit: u64, + pub start_byte: u64, + pub end_byte: u64, + pub shift: u64, +} + +/// Where a symbol reads its value from: a slice of the instruction stream, or +/// a slice of the context register. Both shapes occur for `value`, `name`, +/// `valuemap` and `attach variables` symbols. +#[derive(Debug, Clone)] +pub enum FieldDef { + /// A field of an instruction token. + Token(TokenField), + /// A field of the context register. + Context(ContextField), +} + +impl FieldDef { + /// The token field, if this is one. + #[must_use] + pub fn token(&self) -> Option<&TokenField> { + match self { + Self::Token(t) => Some(t), + Self::Context(_) => None, + } + } + + /// The context field, if this is one. + #[must_use] + pub fn context(&self) -> Option<&ContextField> { + match self { + Self::Context(c) => Some(c), + Self::Token(_) => None, + } + } +} + +/// An operator inside a pattern expression, which is what a disassembly +/// action computes with. Element ids 47 to 57 in alphabetical order of the +/// operator's name, each settled by compiling a spec that uses only it; see +/// `docs/sla-format.md`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PatternOp { + /// `$and`, two operands. + And, + /// `/`, two operands. + Div, + /// `<<`, two operands. + LeftShift, + /// Unary `-`, one operand. + Minus, + /// `*`, two operands. + Mult, + /// Unary `~`, one operand. + Not, + /// `$or`, two operands. + Or, + /// `+`, two operands. + Plus, + /// `>>`, two operands. + RightShift, + /// `-`, two operands. + Sub, + /// `$xor`, two operands. + Xor, +} + +impl PatternOp { + /// The element id that carries it. + #[must_use] + pub fn element(self) -> u32 { + match self { + Self::And => el::PEXP_AND, + Self::Div => el::PEXP_DIV, + Self::LeftShift => el::PEXP_LSHIFT, + Self::Minus => el::PEXP_MINUS, + Self::Mult => el::PEXP_MULT, + Self::Not => el::PEXP_NOT, + Self::Or => el::PEXP_OR, + Self::Plus => el::PEXP_PLUS, + Self::RightShift => el::PEXP_RSHIFT, + Self::Sub => el::PEXP_SUB, + Self::Xor => el::PEXP_XOR, + } + } + + /// The operator an element id stands for, if it is one. + #[must_use] + pub fn from_element(id: u32) -> Option { + Some(match id { + el::PEXP_AND => Self::And, + el::PEXP_DIV => Self::Div, + el::PEXP_LSHIFT => Self::LeftShift, + el::PEXP_MINUS => Self::Minus, + el::PEXP_MULT => Self::Mult, + el::PEXP_NOT => Self::Not, + el::PEXP_OR => Self::Or, + el::PEXP_PLUS => Self::Plus, + el::PEXP_RSHIFT => Self::RightShift, + el::PEXP_SUB => Self::Sub, + el::PEXP_XOR => Self::Xor, + _ => return None, + }) + } +} + +/// A pattern expression: what a disassembly action computes, evaluated when an +/// instruction is decoded rather than when it runs. +#[derive(Debug, Clone)] +pub enum Expr { + /// The value of operand `index` of constructor `ct` in table `table`. + OperandValue { + index: u64, + table: u64, + ct: u64, + }, + Constant(i128), + /// A context field read inline, carrying its own bit layout rather than + /// naming a symbol. + Context(ContextField), + /// A token field read inline. This is how an operand body names the bits + /// it is cut from. + TokenField(TokenField), + /// The address of the instruction being decoded. + InstStart, + /// The address after it. + InstNext, + /// The address after that. Appears in no file Ghidra ships; it was found + /// by writing a spec that uses it. + InstNext2, + /// An operator and its operands, left to right. + Op { + op: PatternOp, + operands: Vec, + }, + /// An element id with no established meaning. Nothing in the corpus + /// produces one any more, and it is kept so a future format still reads. + Unknown { + element: u32, + operands: Vec, + }, +} + +/// One `(mask, value)` word of a pattern, read big-endian from the instruction +/// or context bytes at `PatternBlock::offset`. +#[derive(Debug, Clone, Copy)] +pub struct PatternWord { + pub mask: u64, + pub value: u64, +} + +/// A run of pattern words starting at a byte offset into the stream. +#[derive(Debug, Clone)] +pub struct PatternBlock { + pub offset: u64, + pub bytes: u64, + pub words: Vec, +} + +/// What one branch of the decision tree matches on. +/// +/// A file carries either half alone or both wrapped in a `combine_pattern`, +/// and an empty block list is not the same bytes as an absent half, so the +/// shape is recorded rather than flattened. +#[derive(Debug, Clone, Default)] +pub struct Pattern { + pub context: Vec, + pub instruction: Vec, + /// The two halves were wrapped in a `combine_pattern`. + pub combined: bool, + pub has_context: bool, + pub has_instruction: bool, +} + +/// The decision tree a subtable uses to pick a constructor. `start_bit` counts +/// from the most significant bit of the first instruction byte. +#[derive(Debug, Clone)] +pub struct Decision { + pub number: u64, + pub on_context: bool, + pub start_bit: u64, + pub num_bits: u64, + pub children: Vec, + pub pairs: Vec<(u32, Pattern)>, +} + +/// One piece of a constructor's display form. +#[derive(Debug, Clone)] +pub enum PrintPiece { + Literal(String), + /// Print operand `index` of this constructor. + Operand(u64), +} + +/// A leaf of a varnode template: where the space, offset or size comes from. +#[derive(Debug, Clone)] +pub enum ConstTemplate { + Real(i128), + SpaceId(u32), + /// Field `select` (0 space, 1 offset, 2 size) of operand `index`'s handle. + Handle { + index: u64, + select: u64, + extra: Option, + }, + /// A label, by index. + Relative(i128), + /// The address of the instruction. + InstStart, + /// The address after the instruction. + InstNext, + /// The address after that. + InstNext2, + /// In a space slot: the space the instruction was decoded from, rather + /// than a fixed space index. Always paired with [`ConstTemplate::CurSpaceSize`]. + CurSpace, + /// In a size slot: how many bytes an address in that space takes. + CurSpaceSize, + /// A leaf whose element id has no established meaning. + Unknown(u32), +} + +#[derive(Debug, Clone)] +pub struct VarnodeTemplate { + pub space: ConstTemplate, + pub offset: ConstTemplate, + pub size: ConstTemplate, +} + +/// One p-code operation. `opcode` is the published p-code opcode number. +#[derive(Debug, Clone)] +pub struct OpTemplate { + pub opcode: u64, + pub output: Option, + pub inputs: Vec, +} + +/// The p-code a constructor contributes, plus the handle it exports. +#[derive(Debug, Clone, Default)] +pub struct ConstructTemplate { + pub section: Option, + pub labels: Option, + pub delay: Option, + /// The seven `ConstTemplate` parts of an exported handle, when there is one. + pub result: Option>, + pub ops: Vec, +} + +/// What a constructor does to the context register, in the order the file +/// lists it. A disassembly action can both change context for the rest of the +/// current parse and publish a change from an address onwards, and the two are +/// different records. +#[derive(Debug, Clone)] +pub enum ContextOp { + /// `field = expr` in a disassembly action: element 32. + Set { + /// Which 32-bit word of the context register. + word: u64, + /// Right shift that brings the field to bit zero. + shift: u64, + /// Which bits of that word the field owns. + mask: u64, + /// The value, as a pattern expression. + value: Vec, + }, + /// `globalset(address, field)`: element 79. + Commit { + /// Symbol id of the address argument. + symbol: u32, + /// Which 32-bit word of the context register. + word: u64, + /// Which bits of that word are published. + mask: u64, + /// False when the field was declared `noflow`. + flow: bool, + }, +} + +#[derive(Debug, Clone)] +pub struct Constructor { + /// Symbol id of the subtable this constructor belongs to. + pub parent: u32, + /// Index into [`SourceFile`], and the line in that file. + pub source: u64, + pub line: u64, + /// Instruction length in bytes, before any operand extends it. + pub length: u64, + /// Where the mnemonic ends among the print pieces. Negative values occur. + pub flowthru: i64, + /// Symbol ids of the operands, in definition order. + pub operands: Vec, + pub print: Vec, + /// Context changes and `globalset` commits, in file order. + pub context_ops: Vec, + pub templates: Vec, +} + +/// The body of a symbol. The variants are the symbol kinds observed in the +/// corpus; `Unknown` keeps the element id of anything else. +#[derive(Debug, Clone)] +pub enum SymbolBody { + /// A register or other fixed varnode. + Varnode { + space: u32, + offset: u64, + size: u64, + }, + /// A `define pcodeop`. + UserOp { + index: u64, + }, + /// A token field used directly as a value. + Value { + field: Option, + }, + /// `attach values`. + ValueMap { + field: Option, + values: Vec, + }, + /// `attach names`. A `None` entry is an encoding the attachment leaves + /// undefined, which the file writes as an entry with no name at all. + Name { + field: Option, + names: Vec>, + }, + /// `attach variables`: the varnode symbol id per field value, `None` for a + /// hole in the list. + VarnodeList { + field: Option, + entries: Vec>, + }, + /// A field of the context register. + Context { + varnode: u32, + low: u64, + high: u64, + flow: bool, + field: Option, + }, + /// An operand of a constructor. + Operand { + index: u64, + offset: u64, + sub_symbol: Option, + /// Attribute 19: the operand this one's offset is measured from the + /// end of, with -1 meaning the start of the instruction. + base: i64, + /// Attribute 18. It equals the width of the operand's token in every + /// case observed, and no experiment isolated it, so it is carried + /// rather than named. + min_length: u64, + /// Attribute 7, a flag present on a minority of operands. Not + /// isolated by any experiment, so it is carried and not named. + flag: Option, + expr: Vec, + }, + /// `inst_start`. + Start, + /// `inst_next`. + End, + /// `inst_next2`. + Next2, + Subtable { + constructors: Vec, + decision: Option, + }, + Unknown { + element: u32, + }, +} + +#[derive(Debug, Clone)] +pub struct Symbol { + pub id: u32, + /// Names come from the header records, which precede the bodies; a symbol + /// with no header keeps `None` rather than an invented name. + pub name: Option, + pub scope: Option, + pub body: SymbolBody, +} + +/// How much of a file this reader turned into the model above. +#[derive(Debug, Clone, Default)] +pub struct Coverage { + /// Bytes of the decompressed payload. + pub total: usize, + /// Bytes belonging to elements whose meaning is established. + pub interpreted: usize, + /// Bytes belonging to elements that are kept as raw nodes. + pub raw: usize, + /// Each unnamed element id, how many there are and how many bytes. + pub unknown: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct UnknownElement { + pub id: u32, + pub count: usize, + pub bytes: usize, +} + +impl Coverage { + /// Fraction of the payload the model accounts for, in the range 0..=1. + #[must_use] + pub fn fraction(&self) -> f64 { + if self.total == 0 { + return 1.0; + } + self.interpreted as f64 / self.total as f64 + } + + pub(crate) fn measure(root: &Node) -> Self { + let mut cov = Self { + total: root.len(), + ..Self::default() + }; + let mut unknown: Vec = Vec::new(); + root.visit(&mut |n| { + let own = n.own_len(); + if element_name(n.id).is_some() { + cov.interpreted += own; + } else { + cov.raw += own; + match unknown.iter_mut().find(|u| u.id == n.id) { + Some(u) => { + u.count += 1; + u.bytes += own; + } + None => unknown.push(UnknownElement { + id: n.id, + count: 1, + bytes: own, + }), + } + } + }); + unknown.sort_by_key(|u| u.id); + cov.unknown = unknown; + cov + } +} + +// Small readers. Every one takes a default rather than failing, because a +// missing attribute is a fact about the file and should show up in the model +// and in the coverage number, not as an error that hides the rest of the file. + +fn u64_of(n: &Node, id: u32) -> Option { + n.attr(id).and_then(Value::as_u64) +} + +fn i64_of(n: &Node, id: u32) -> Option { + n.attr(id).and_then(Value::as_i64) +} + +fn bool_of(n: &Node, id: u32) -> bool { + n.attr(id).and_then(Value::as_bool).unwrap_or(false) +} + +fn text_of(n: &Node, id: u32) -> Option { + n.attr(id).and_then(Value::as_str).map(str::to_owned) +} + +fn i128_of(n: &Node, id: u32) -> i128 { + match n.attr(id) { + Some(Value::Signed(v)) => *v, + Some(Value::Unsigned(v)) => i128::try_from(*v).unwrap_or(i128::MAX), + Some(Value::Space(v)) => i128::from(*v), + Some(Value::Bool(b)) => i128::from(*b), + _ => 0, + } +} + +fn token_field(n: &Node) -> TokenField { + TokenField { + big_endian: bool_of(n, at::BIGENDIAN), + signed: bool_of(n, at::SIGNBIT), + start_bit: u64_of(n, at::STARTBIT).unwrap_or(0), + end_bit: u64_of(n, at::ENDBIT).unwrap_or(0), + start_byte: u64_of(n, at::STARTBYTE).unwrap_or(0), + end_byte: u64_of(n, at::ENDBYTE).unwrap_or(0), + shift: u64_of(n, at::SHIFT).unwrap_or(0), + } +} + +fn context_field(n: &Node) -> ContextField { + ContextField { + signed: bool_of(n, at::SIGNBIT), + start_bit: u64_of(n, at::STARTBIT).unwrap_or(0), + end_bit: u64_of(n, at::ENDBIT).unwrap_or(0), + start_byte: u64_of(n, at::STARTBYTE).unwrap_or(0), + end_byte: u64_of(n, at::ENDBYTE).unwrap_or(0), + shift: u64_of(n, at::SHIFT).unwrap_or(0), + } +} + +/// The field element a symbol body carries, whichever of the two it is. +fn field_def(n: &Node) -> Option { + n.children.iter().find_map(|c| match c.id { + el::TOKEN_FIELD => Some(FieldDef::Token(token_field(c))), + el::CONTEXT_FIELD => Some(FieldDef::Context(context_field(c))), + _ => None, + }) +} + +fn expr(n: &Node) -> Expr { + if let Some(op) = PatternOp::from_element(n.id) { + return Expr::Op { + op, + operands: n.children.iter().map(expr).collect(), + }; + } + match n.id { + el::OPERAND_VALUE => Expr::OperandValue { + index: u64_of(n, at::INDEX).unwrap_or(0), + table: u64_of(n, at::TABLE).unwrap_or(0), + ct: u64_of(n, at::CT).unwrap_or(0), + }, + el::PEXP_CONSTANT => Expr::Constant(i128_of(n, at::VAL)), + el::CONTEXT_FIELD => Expr::Context(context_field(n)), + el::TOKEN_FIELD => Expr::TokenField(token_field(n)), + el::PEXP_INST_START => Expr::InstStart, + el::PEXP_INST_NEXT => Expr::InstNext, + el::PEXP_INST_NEXT2 => Expr::InstNext2, + other => Expr::Unknown { + element: other, + operands: n.children.iter().map(expr).collect(), + }, + } +} + +fn const_template(n: &Node) -> ConstTemplate { + match n.id { + el::CONST_REAL => ConstTemplate::Real(i128_of(n, at::VAL)), + el::CONST_SPACEID => ConstTemplate::SpaceId( + n.attr(at::SPACE) + .and_then(Value::as_space) + .unwrap_or(u32::MAX), + ), + el::CONST_HANDLE => ConstTemplate::Handle { + index: u64_of(n, at::VAL).unwrap_or(0), + select: u64_of(n, at::SELECT).unwrap_or(0), + extra: n.attr(28).and_then(|v| match v { + Value::Unsigned(u) => Some(*u), + Value::Signed(s) => u128::try_from(*s).ok(), + _ => None, + }), + }, + el::CONST_RELATIVE => ConstTemplate::Relative(i128_of(n, at::VAL)), + el::CONST_INST_START => ConstTemplate::InstStart, + el::CONST_INST_NEXT => ConstTemplate::InstNext, + el::CONST_INST_NEXT2 => ConstTemplate::InstNext2, + el::CONST_CURSPACE => ConstTemplate::CurSpace, + el::CONST_CURSPACE_SIZE => ConstTemplate::CurSpaceSize, + other => ConstTemplate::Unknown(other), + } +} + +fn varnode_template(n: &Node) -> VarnodeTemplate { + let mut parts = n.children.iter().map(const_template); + VarnodeTemplate { + space: parts.next().unwrap_or(ConstTemplate::Unknown(0)), + offset: parts.next().unwrap_or(ConstTemplate::Unknown(0)), + size: parts.next().unwrap_or(ConstTemplate::Unknown(0)), + } +} + +fn op_template(n: &Node) -> OpTemplate { + let mut out = None; + let mut inputs = Vec::new(); + // The first child is the output: a varnode template, or `null` when the + // operation has no output. + for (i, c) in n.children.iter().enumerate() { + match (i, c.id) { + (0, el::NULL) => {} + (0, el::VARNODE_TPL) => out = Some(varnode_template(c)), + (_, el::VARNODE_TPL) => inputs.push(varnode_template(c)), + _ => {} + } + } + OpTemplate { + opcode: u64_of(n, at::CODE).unwrap_or(0), + output: out, + inputs, + } +} + +fn construct_template(n: &Node) -> ConstructTemplate { + let mut t = ConstructTemplate { + section: i64_of(n, at::SECTION), + labels: u64_of(n, at::LABELS), + delay: i64_of(n, at::DELAY), + ..ConstructTemplate::default() + }; + for c in &n.children { + match c.id { + el::HANDLE_TPL => t.result = Some(c.children.iter().map(const_template).collect()), + el::OP_TPL => t.ops.push(op_template(c)), + _ => {} + } + } + t +} + +fn pattern_block(n: &Node) -> PatternBlock { + PatternBlock { + offset: u64_of(n, at::OFF).unwrap_or(0), + bytes: u64_of(n, at::NBYTES).unwrap_or(0), + words: n + .children + .iter() + .filter(|c| c.id == el::PATTERN_WORD) + .map(|c| PatternWord { + mask: u64_of(c, at::MASK).unwrap_or(0), + value: u64_of(c, at::VAL).unwrap_or(0), + }) + .collect(), + } +} + +fn pattern(n: &Node, out: &mut Pattern) { + match n.id { + el::CONTEXT_PATTERN => { + out.has_context = true; + out.context.extend( + n.children + .iter() + .filter(|c| c.id == el::PATTERN_BLOCK) + .map(pattern_block), + ); + } + el::INSTRUCTION_PATTERN => { + out.has_instruction = true; + out.instruction.extend( + n.children + .iter() + .filter(|c| c.id == el::PATTERN_BLOCK) + .map(pattern_block), + ); + } + el::COMBINE_PATTERN => { + out.combined = true; + for c in &n.children { + pattern(c, out); + } + } + _ => {} + } +} + +fn decision(n: &Node) -> Decision { + let mut d = Decision { + number: u64_of(n, at::NUMBER).unwrap_or(0), + on_context: bool_of(n, at::CONTEXT), + start_bit: u64_of(n, at::STARTBIT).unwrap_or(0), + num_bits: u64_of(n, at::SIZE).unwrap_or(0), + children: Vec::new(), + pairs: Vec::new(), + }; + for c in &n.children { + match c.id { + el::DECISION => d.children.push(decision(c)), + el::DECISION_PAIR => { + let mut p = Pattern::default(); + for g in &c.children { + pattern(g, &mut p); + } + let idx = u64_of(c, at::ID).unwrap_or(0); + d.pairs.push((u32::try_from(idx).unwrap_or(u32::MAX), p)); + } + _ => {} + } + } + d +} + +fn constructor(n: &Node) -> Constructor { + let mut c = Constructor { + parent: u64_of(n, at::PARENT) + .and_then(|v| u32::try_from(v).ok()) + .unwrap_or(0), + source: u64_of(n, at::SOURCE).unwrap_or(0), + line: u64_of(n, at::LINE).unwrap_or(0), + length: u64_of(n, at::LENGTH).unwrap_or(0), + flowthru: i64_of(n, at::FLOWTHRU).unwrap_or(0), + operands: Vec::new(), + print: Vec::new(), + context_ops: Vec::new(), + templates: Vec::new(), + }; + for k in &n.children { + match k.id { + el::CONSTRUCTOR_OPERAND => { + if let Some(v) = u64_of(k, at::ID).and_then(|v| u32::try_from(v).ok()) { + c.operands.push(v); + } + } + el::PRINT_LITERAL => { + c.print.push(PrintPiece::Literal( + text_of(k, at::PIECE).unwrap_or_default(), + )); + } + el::PRINT_OPERAND => c + .print + .push(PrintPiece::Operand(u64_of(k, at::ID).unwrap_or(0))), + el::CONTEXT_CHANGE => c.context_ops.push(ContextOp::Set { + word: u64_of(k, at::WORD).unwrap_or(0), + shift: u64_of(k, at::SHIFT).unwrap_or(0), + mask: u64_of(k, at::MASK).unwrap_or(0), + value: k.children.iter().map(expr).collect(), + }), + el::GLOBALSET => c.context_ops.push(ContextOp::Commit { + symbol: u64_of(k, at::ID) + .and_then(|v| u32::try_from(v).ok()) + .unwrap_or(u32::MAX), + word: u64_of(k, at::NUMBER).unwrap_or(0), + mask: u64_of(k, at::MASK).unwrap_or(0), + flow: bool_of(k, at::FLOW), + }), + el::CONSTRUCT_TPL => c.templates.push(construct_template(k)), + _ => {} + } + } + c +} + +fn symbol_body(n: &Node) -> SymbolBody { + match n.id { + el::VARNODE_SYM => SymbolBody::Varnode { + space: n + .attr(at::SPACE) + .and_then(Value::as_space) + .unwrap_or(u32::MAX), + offset: u64_of(n, at::OFF).unwrap_or(0), + size: u64_of(n, at::SIZE).unwrap_or(0), + }, + el::USEROP_SYM => SymbolBody::UserOp { + index: u64_of(n, at::INDEX).unwrap_or(0), + }, + el::VALUE_SYM => SymbolBody::Value { + field: field_def(n), + }, + el::VALUEMAP_SYM => SymbolBody::ValueMap { + field: field_def(n), + values: n + .children + .iter() + .filter(|c| c.id == el::VALUEMAP_ENTRY) + .map(|c| i128_of(c, at::VAL)) + .collect(), + }, + el::NAME_SYM => SymbolBody::Name { + field: field_def(n), + names: n + .children + .iter() + .filter(|c| c.id == el::NAME_ENTRY) + .map(|c| text_of(c, at::NAME)) + .collect(), + }, + el::VARNODE_LIST_SYM => SymbolBody::VarnodeList { + field: field_def(n), + entries: n + .children + .iter() + .filter(|c| c.id == el::VARNODE_LIST_ENTRY || c.id == el::NULL) + .map(|c| { + if c.id == el::NULL { + None + } else { + u64_of(c, at::ID).and_then(|v| u32::try_from(v).ok()) + } + }) + .collect(), + }, + el::CONTEXT_SYM => SymbolBody::Context { + varnode: u64_of(n, at::VARNODE) + .and_then(|v| u32::try_from(v).ok()) + .unwrap_or(u32::MAX), + low: u64_of(n, at::LOW).unwrap_or(0), + high: u64_of(n, at::HIGH).unwrap_or(0), + flow: bool_of(n, at::FLOW), + field: n.child(el::CONTEXT_FIELD).map(context_field), + }, + el::OPERAND_SYM => SymbolBody::Operand { + index: u64_of(n, at::INDEX).unwrap_or(0), + offset: u64_of(n, at::OFF).unwrap_or(0), + sub_symbol: u64_of(n, at::SUBSYM).and_then(|v| u32::try_from(v).ok()), + base: i64_of(n, at::OPERAND_19).unwrap_or(-1), + min_length: u64_of(n, at::OPERAND_18).unwrap_or(0), + flag: n.attr(at::CODE).and_then(Value::as_bool), + expr: n.children.iter().map(expr).collect(), + }, + el::START_SYM => SymbolBody::Start, + el::END_SYM => SymbolBody::End, + el::NEXT2_SYM => SymbolBody::Next2, + el::SUBTABLE_SYM => SymbolBody::Subtable { + constructors: n + .children + .iter() + .filter(|c| c.id == el::CONSTRUCTOR) + .map(constructor) + .collect(), + decision: n.child(el::DECISION).map(decision), + }, + other => SymbolBody::Unknown { element: other }, + } +} + +/// Symbol header elements sit one id below their body element, in every pair +/// observed. Headers carry the name and the scope; bodies carry everything +/// else and come later in the same table. +fn header_body_of(id: u32) -> Option { + const HEADERS: [u32; 12] = [14, 24, 26, 40, 42, 44, 65, 68, 70, 72, 74, 77]; + HEADERS.contains(&id).then(|| id - 1) +} + +pub(crate) fn build(root: &Node) -> Program { + let mut p = Program { + version: u64_of(root, at::VERSION), + big_endian: bool_of(root, at::BIGENDIAN), + alignment: u64_of(root, at::ALIGN).unwrap_or(1), + unique_base: u64_of(root, at::UNIQBASE).unwrap_or(0), + extra_root_attrs: root + .attrs + .iter() + .filter(|(a, _)| !matches!(*a, at::VERSION | at::BIGENDIAN | at::ALIGN | at::UNIQBASE)) + .cloned() + .collect(), + ..Program::default() + }; + for top in &root.children { + match top.id { + el::SOURCEFILES => { + for f in top.children.iter().filter(|c| c.id == el::SOURCEFILE) { + p.source_files.push(SourceFile { + name: text_of(f, at::NAME).unwrap_or_default(), + index: u64_of(f, at::INDEX).unwrap_or(0), + }); + } + } + el::SPACES => { + p.default_space = text_of(top, at::DEFAULTSPACE); + for s in &top.children { + let kind = match s.id { + el::SPACE => SpaceKind::Normal, + el::SPACE_OTHER => SpaceKind::Other, + el::SPACE_UNIQUE => SpaceKind::Unique, + _ => continue, + }; + p.spaces.push(Space { + kind, + name: text_of(s, at::NAME).unwrap_or_default(), + index: u64_of(s, at::INDEX) + .and_then(|v| u32::try_from(v).ok()) + .unwrap_or(u32::MAX), + big_endian: bool_of(s, at::BIGENDIAN), + delay: u64_of(s, at::DELAY).unwrap_or(0), + size: u64_of(s, at::SIZE).unwrap_or(0), + word_size: u64_of(s, at::WORDSIZE).unwrap_or(1), + physical: bool_of(s, at::PHYSICAL), + }); + } + } + el::SYMBOL_TABLE => { + p.declared_scopes = u64_of(top, at::SCOPESIZE); + p.declared_symbols = u64_of(top, at::SYMBOLSIZE); + let mut names: Vec<(u32, String, Option)> = Vec::new(); + for s in &top.children { + if s.id == el::SCOPE { + p.scopes.push(Scope { + id: u64_of(s, at::ID) + .and_then(|v| u32::try_from(v).ok()) + .unwrap_or(0), + parent: u64_of(s, at::PARENT) + .and_then(|v| u32::try_from(v).ok()) + .unwrap_or(0), + }); + } else if header_body_of(s.id).is_some() { + names.push(( + u64_of(s, at::ID) + .and_then(|v| u32::try_from(v).ok()) + .unwrap_or(0), + text_of(s, at::NAME).unwrap_or_default(), + u64_of(s, at::SCOPE).and_then(|v| u32::try_from(v).ok()), + )); + } else { + let id = u64_of(s, at::ID) + .and_then(|v| u32::try_from(v).ok()) + .unwrap_or(u32::MAX); + p.symbols.push(Symbol { + id, + name: None, + scope: None, + body: symbol_body(s), + }); + } + } + // A linear search per header would be quadratic, and a real + // language has hundreds of thousands of symbols. Index once. + let mut by_id: Vec<(u32, usize)> = p + .symbols + .iter() + .enumerate() + .map(|(i, s)| (s.id, i)) + .collect(); + by_id.sort_unstable(); + for (id, name, scope) in names { + if let Ok(k) = by_id.binary_search_by_key(&id, |(i, _)| *i) { + let sym = &mut p.symbols[by_id[k].1]; + sym.name = Some(name); + sym.scope = scope; + } + } + p.symbol_index = by_id; + } + _ => {} + } + } + p +} + +/// Everything the model understands about one `.sla`. +#[derive(Debug, Clone, Default)] +pub struct Program { + /// The `version` attribute on the root element, 4 in every file seen. + pub version: Option, + pub big_endian: bool, + pub alignment: u64, + pub unique_base: u64, + /// Attributes on the root element beyond the four that are established. + /// Thirteen of the shipped files carry attribute 38 and one carries 39 and + /// 40; nothing says what they mean, so they are carried through rather + /// than dropped, and the writer puts them back where they were. + pub extra_root_attrs: Vec<(u32, Value)>, + pub source_files: Vec, + pub default_space: Option, + pub spaces: Vec, + pub scopes: Vec, + pub symbols: Vec, + /// The symbol table's own count attributes, kept so a reader can check + /// them against what it actually found rather than trusting either. + pub declared_scopes: Option, + pub declared_symbols: Option, + /// `(symbol id, index into symbols)` sorted by id. Symbol ids are dense in + /// every file seen, but nothing in the format says they must be, so the + /// lookup is a binary search rather than an index. + symbol_index: Vec<(u32, usize)>, +} + +impl Program { + /// An empty program, for a compiler to fill in. + /// + /// Every field is public except the symbol id index, which has to stay in + /// step with `symbols`; [`Program::reindex`] is how that is said. + #[must_use] + pub fn empty() -> Self { + Self::default() + } + + /// Rebuild the symbol id index. + /// + /// A program that was read from a file has one already. A program a + /// compiler built has to say when its symbol list is final, and the index + /// is not derived lazily because every lookup would then have to decide + /// whether it is stale. + pub fn reindex(&mut self) { + let mut by_id: Vec<(u32, usize)> = self + .symbols + .iter() + .enumerate() + .map(|(i, s)| (s.id, i)) + .collect(); + by_id.sort_unstable(); + self.symbol_index = by_id; + } + + #[must_use] + pub fn symbol(&self, id: u32) -> Option<&Symbol> { + let k = self + .symbol_index + .binary_search_by_key(&id, |(i, _)| *i) + .ok()?; + self.symbols.get(self.symbol_index[k].1) + } + + /// Whether a symbol with this id exists, in logarithmic time. + #[must_use] + pub fn has_symbol(&self, id: u32) -> bool { + self.symbol_index + .binary_search_by_key(&id, |(i, _)| *i) + .is_ok() + } + + /// Registers, which are the varnode symbols in a register space. + pub fn registers(&self) -> impl Iterator { + self.symbols + .iter() + .filter(|s| matches!(s.body, SymbolBody::Varnode { .. })) + } + + pub fn subtables(&self) -> impl Iterator { + self.symbols + .iter() + .filter(|s| matches!(s.body, SymbolBody::Subtable { .. })) + } + + /// Total constructors across every subtable. + #[must_use] + pub fn constructor_count(&self) -> usize { + self.symbols + .iter() + .map(|s| match &s.body { + SymbolBody::Subtable { constructors, .. } => constructors.len(), + _ => 0, + }) + .sum() + } +} diff --git a/crates/e5r-sla/tests/compile.rs b/crates/e5r-sla/tests/compile.rs new file mode 100644 index 0000000..03413e8 --- /dev/null +++ b/crates/e5r-sla/tests/compile.rs @@ -0,0 +1,627 @@ +//! The three gates on the compiler, over every `.slaspec` Ghidra ships. +//! +//! 1. **Byte comparison against the reference compiler.** Compile the same +//! specification both ways and diff the decompressed payloads. Where they +//! differ, the test says which elements differ and why, because "does not +//! match" is not a finding. +//! 2. **Round trip through our own reader.** What we wrote must read back, and +//! what reads back must agree with what `e5r-sleigh` parsed: the same +//! spaces, the same registers, the same constructors, the same operands. +//! This catches everything the byte comparison cannot, because it holds +//! even where the reference orders things differently. +//! 3. **Cross-read by the reference.** Whether Ghidra will load a `.sla` we +//! wrote, checked without a GUI where that is possible. +//! +//! The corpus is a Ghidra source or binary tree. Without one the tests report +//! and return, which is the convention in this repository. + +mod slaload; +mod sleighc; + +use std::path::{Path, PathBuf}; + +use e5r_sla::{Sla, model::SymbolBody}; + +/// Every Ghidra tree on the machine. A source checkout and a binary +/// distribution hold different sets of `.slaspec`, so both are searched. +fn ghidra_roots() -> Vec { + let mut roots: Vec = Vec::new(); + if let Ok(v) = std::env::var("E5R_GHIDRA_DIR") { + roots.push(PathBuf::from(v)); + } + if let Ok(home) = std::env::var("HOME") { + roots.push(PathBuf::from(&home).join(".local/share/ghidra-cli/ghidra")); + roots.push(PathBuf::from(&home).join("ghidra")); + roots.push(PathBuf::from(&home).join("Ref/ghidra")); + } + roots.push(PathBuf::from("/opt/ghidra")); + roots.retain(|r| r.is_dir()); + roots +} + +/// The first tree, for the things that need only one: the reference compiler +/// and the headless launcher. +fn ghidra_root() -> Option { + ghidra_roots().into_iter().next() +} + +fn find(dir: &Path, ext: &str, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for e in entries.flatten() { + let p = e.path(); + if p.is_dir() { + find(&p, ext, out); + } else if p.extension().is_some_and(|x| x == ext) { + out.push(p); + } + } +} + +fn slaspecs() -> Vec { + let mut out = Vec::new(); + for root in ghidra_roots() { + find(&root, "slaspec", &mut out); + } + out.sort(); + // The same language appears in both trees; compile each one once. + let mut seen: Vec = Vec::new(); + out.retain(|p| { + let name = p.file_name().unwrap_or_default().to_os_string(); + if seen.contains(&name) { + return false; + } + seen.push(name); + true + }); + out +} + +/// The reference compiler, if this machine has one. A distribution unpacks +/// into a versioned directory, so one level down is checked as well. +fn reference() -> Option { + for root in ghidra_roots() { + let direct = root.join("support/sleigh"); + if direct.is_file() { + return Some(direct); + } + for e in std::fs::read_dir(&root).into_iter().flatten().flatten() { + let nested = e.path().join("support/sleigh"); + if nested.is_file() { + return Some(nested); + } + } + } + None +} + +struct Outcome { + name: String, + constructors: usize, + ok: bool, + note: String, +} + +/// Gate 2, and the one that has to pass: everything we write reads back, and +/// what reads back is what the front end parsed. +#[test] +fn every_slaspec_compiles_and_reads_back() { + let specs = slaspecs(); + if specs.is_empty() { + println!("no Ghidra tree on this machine, skipping"); + return; + } + + let mut parsed = 0usize; + let mut written = 0usize; + let mut verified = 0usize; + let mut constructors = 0usize; + let mut semantics_missing = 0usize; + let mut context_sets = 0usize; + let mut computed = 0usize; + let mut globalsets = 0usize; + let mut dropped = 0usize; + let mut failures: Vec = Vec::new(); + let mut rows: Vec = Vec::new(); + + for path in &specs { + let name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + let spec = match e5r_sleigh::parse_file(path) { + Ok(s) => s, + Err(e) => { + failures.push(format!("{name}: parse: {e}")); + continue; + } + }; + parsed += 1; + let (bytes, report) = match sleighc::compile_to_bytes(&spec) { + Ok(v) => v, + Err(e) => { + failures.push(format!("{name}: compile: {e}")); + rows.push(Outcome { + name, + constructors: 0, + ok: false, + note: format!("compile: {e}"), + }); + continue; + } + }; + written += 1; + constructors += report.constructors; + semantics_missing += report.without_semantics; + context_sets += report.context_sets; + computed += report.computed_operands; + globalsets += report.globalsets; + dropped += report.dropped_tables; + + let back = match Sla::parse(&bytes) { + Ok(s) => s, + Err(e) => { + failures.push(format!("{name}: read back: {e}")); + rows.push(Outcome { + name, + constructors: report.constructors, + ok: false, + note: format!("read back: {e}"), + }); + continue; + } + }; + + let problems = back.check(); + let mut note = String::new(); + let mut ok = problems.is_empty(); + if !ok { + note = format!( + "{} inconsistencies, first {:?}", + problems.len(), + problems[0] + ); + } + // The model that comes back has to be the specification again. + if let Err(e) = agrees(&spec, &back, &report) { + ok = false; + note = e; + } + if ok { + verified += 1; + } else { + failures.push(format!("{name}: {note}")); + } + rows.push(Outcome { + name, + constructors: report.constructors, + ok, + note, + }); + } + + println!( + "slaspec corpus: {} found, {parsed} parsed, {written} compiled, {verified} verified", + specs.len() + ); + println!( + "{constructors} constructors written; {semantics_missing} of them have a semantic body \ + the compiler does not yet turn into p-code" + ); + println!( + "disassembly actions: {context_sets} context assignments, {computed} computed operands, \ + {globalsets} globalsets; {dropped} unreferenced tables dropped" + ); + for r in rows.iter().filter(|r| !r.ok).take(20) { + println!( + " FAIL {} ({} constructors): {}", + r.name, r.constructors, r.note + ); + } + assert!( + failures.is_empty(), + "{} failures: {failures:#?}", + failures.len() + ); +} + +/// What the compiled file says must be what the specification said. +fn agrees(spec: &e5r_sleigh::Spec, back: &Sla, report: &sleighc::Report) -> Result<(), String> { + let p = &back.program; + + let declared = spec + .spaces + .iter() + .filter(|s| { + !matches!( + s.kind, + e5r_sleigh::model::SpaceKind::Constant | e5r_sleigh::model::SpaceKind::Unique + ) + }) + .count(); + // Two more than the specification declares: OTHER and unique. + if p.spaces.len() != declared + 2 { + return Err(format!( + "{} spaces read back, specification declares {declared} plus OTHER and unique", + p.spaces.len() + )); + } + + let registers = p.registers().count(); + if registers != spec.varnodes.len() { + return Err(format!( + "{registers} varnode symbols read back, specification has {}", + spec.varnodes.len() + )); + } + + // A table no operand names is not written, the same way the reference + // drops it, so the count to match is what was kept. + let subtables = p.subtables().count(); + let kept = spec.tables.len() - report.dropped_tables; + if subtables != kept { + return Err(format!( + "{subtables} subtables read back, {kept} of the specification's {} were written", + spec.tables.len() + )); + } + + if p.constructor_count() != report.constructors { + return Err(format!( + "{} constructors read back, {} written", + p.constructor_count(), + report.constructors + )); + } + + // Every operand a constructor names must resolve to an operand symbol + // whose own record points back at that constructor. + for s in p.subtables() { + let SymbolBody::Subtable { constructors, .. } = &s.body else { + continue; + }; + for (k, c) in constructors.iter().enumerate() { + for (i, o) in c.operands.iter().enumerate() { + let Some(sym) = p.symbol(*o) else { + return Err(format!("operand symbol {o} is missing")); + }; + let SymbolBody::Operand { index, expr, .. } = &sym.body else { + return Err(format!("symbol {o} is not an operand")); + }; + if *index != i as u64 { + return Err(format!( + "operand {o} says index {index}, constructor says {i}" + )); + } + match expr.first() { + Some(e5r_sla::model::Expr::OperandValue { table, ct, .. }) + if *table == u64::from(s.id) && *ct == k as u64 => {} + other => { + return Err(format!( + "operand {o} points at {other:?}, expected table {} constructor {k}", + s.id + )); + } + } + } + } + } + Ok(()) +} + +/// Gate 1: the byte comparison. Not a pass-or-fail gate, because the symbol +/// numbering and the decision tree are deliberately not the reference's; it +/// reports how far apart the two files are and in what. +#[test] +#[ignore = "runs the reference compiler over the whole corpus, minutes"] +fn byte_comparison_against_the_reference_compiler() { + let Some(sleigh) = reference() else { + println!("no reference sleigh compiler on this machine, skipping"); + return; + }; + let specs = slaspecs(); + let tmp = std::env::temp_dir().join("e5r-sleighc"); + let _ = std::fs::create_dir_all(&tmp); + + let mut compared = 0usize; + let mut identical = 0usize; + let mut same_symbols = 0usize; + let mut same_constructors = 0usize; + let mut same_spaces = 0usize; + let mut same_registers = 0usize; + let mut ours_bytes = 0usize; + let mut theirs_bytes = 0usize; + let mut rows: Vec = Vec::new(); + + for path in specs.iter() { + let out = tmp.join("ref.sla"); + let _ = std::fs::remove_file(&out); + let status = std::process::Command::new(&sleigh) + .arg(path) + .arg(&out) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + if !status.map(|s| s.success()).unwrap_or(false) || !out.is_file() { + continue; + } + let Ok(theirs) = Sla::open(&out) else { + continue; + }; + let Ok(spec) = e5r_sleigh::parse_file(path) else { + continue; + }; + let Ok((bytes, _)) = sleighc::compile_to_bytes(&spec) else { + continue; + }; + let Ok(ours) = Sla::parse(&bytes) else { + continue; + }; + compared += 1; + + let a = e5r_sla::encode::encode(&ours.tree).expect("encodes"); + let b = e5r_sla::encode::encode(&theirs.tree).expect("encodes"); + if a == b { + identical += 1; + continue; + } + if ours.program.symbols.len() == theirs.program.symbols.len() { + same_symbols += 1; + } + if ours.program.constructor_count() == theirs.program.constructor_count() { + same_constructors += 1; + } else { + rows.push(format!( + "{}: {} constructors against the reference's {}", + path.file_name().unwrap_or_default().to_string_lossy(), + ours.program.constructor_count(), + theirs.program.constructor_count(), + )); + } + if ours.program.spaces.len() == theirs.program.spaces.len() { + same_spaces += 1; + } + if ours.program.registers().count() == theirs.program.registers().count() { + same_registers += 1; + } + ours_bytes += a.len(); + theirs_bytes += b.len(); + } + + println!("byte comparison against the reference compiler, {compared} languages:"); + println!(" payload byte for byte: {identical}"); + println!(" same number of spaces: {same_spaces}"); + println!(" same register table: {same_registers}"); + println!(" same symbol count: {same_symbols}"); + println!(" same constructor count: {same_constructors}"); + println!( + " payload size: {ours_bytes} against {theirs_bytes}, {:.0}% (we write no p-code)", + 100.0 * ours_bytes as f64 / theirs_bytes.max(1) as f64 + ); + for r in rows.iter().take(30) { + println!(" {r}"); + } +} + +/// Gate 3: can the reference load what we wrote? There is no headless command +/// in the shipped distribution that only parses a `.sla`, so what can be +/// checked without a GUI is recorded here rather than claimed. +#[test] +#[ignore = "needs a Ghidra install"] +fn report_whether_the_reference_can_load_our_output() { + let Some(root) = ghidra_root() else { + println!("no Ghidra tree, skipping"); + return; + }; + let headless = root.join("support/analyzeHeadless"); + println!( + "analyzeHeadless present: {}. Loading a language means importing a binary for it, \ + which builds a project and runs the analyzer, so this is reported rather than \ + asserted.", + headless.is_file() + ); +} + +/// A compiled specification has to survive being written and read on the same +/// terms as one Ghidra produced: the fixtures are small enough to check by +/// hand and they are committed, so this is the fast gate. +#[test] +fn the_committed_fixtures_compile() { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data"); + let mut any = false; + for name in ["minimal", "tokens", "subtable"] { + let sinc = dir.join(format!("{name}.sinc")); + if !sinc.is_file() { + continue; + } + any = true; + let text = std::fs::read_to_string(&sinc).expect("readable"); + let text = text.replace("$(ENDIAN)", "little"); + let spec = e5r_sleigh::parse_str(&text).expect("the fixture source parses"); + let (bytes, report) = sleighc::compile_to_bytes(&spec).expect("compiles"); + let back = Sla::parse(&bytes).expect("reads back"); + assert!(back.check().is_empty(), "{name}: {:?}", back.check()); + assert_eq!( + back.program.constructor_count(), + report.constructors, + "{name}" + ); + assert!( + back.program.symbols.iter().all(|s| s.name.is_some()), + "{name}" + ); + println!( + "{name}: {} symbols, {} constructors, {} bytes", + back.program.symbols.len(), + report.constructors, + bytes.len() + ); + } + assert!(any, "the fixture sources should be present"); +} + +/// Write one compiled language to a file, for diffing against the reference +/// compiler's output by hand. `E5R_SPEC` names the `.slaspec`, `E5R_OUT` the +/// file to write. A diagnostic, not a gate. +#[test] +#[ignore = "diagnostic: writes one compiled language to E5R_OUT"] +fn write_one_compiled_language() { + let Ok(spec_path) = std::env::var("E5R_SPEC") else { + println!("set E5R_SPEC and E5R_OUT"); + return; + }; + let out = std::env::var("E5R_OUT").unwrap_or_else(|_| "out.sla".into()); + let spec = e5r_sleigh::parse_file(Path::new(&spec_path)).expect("parses"); + let (bytes, report) = sleighc::compile_to_bytes(&spec).expect("compiles"); + std::fs::write(&out, &bytes).expect("writes"); + println!( + "{out}: {} bytes, {} constructors, {} without p-code", + bytes.len(), + report.constructors, + report.without_semantics + ); + for n in &report.notes { + println!(" {n}"); + } +} + +// ---------------------------------------------------------- the decode gate + +/// Gate 2, second half, and the strongest statement the writer can make. +/// +/// `e5r-sleigh`'s decode engine is measured at zero disagreements with +/// objdump on AArch64, x86-64 and RISC-V. Compile the same specification here, +/// read the file back, rebuild a decodable model from it with +/// [`slaload`], and decode the same bytes with both. Every instruction that +/// decodes differently is something the round trip lost, and the count is the +/// number that matters. +/// +/// The bytes are a deterministic sweep rather than a compiled object, so the +/// test needs no toolchain and still reaches thousands of distinct +/// constructors. +#[test] +fn a_file_we_wrote_decodes_the_way_its_source_does() { + let langs = [ + ("AArch64", "AARCH64/data/languages/AARCH64.slaspec", 4usize), + ("RISC-V", "RISCV/data/languages/riscv.lp64d.slaspec", 4), + ("x86-64", "x86/data/languages/x86-64.slaspec", 16), + ]; + let Some(root) = ghidra_roots() + .into_iter() + .find(|r| r.join("Ghidra/Processors").is_dir()) + else { + println!("no Ghidra processor tree on this machine, skipping"); + return; + }; + + let mut any = false; + let mut total_wrong = 0usize; + for (name, rel, width) in langs { + let path = root.join("Ghidra/Processors").join(rel); + if !path.is_file() { + continue; + } + any = true; + let spec = e5r_sleigh::parse_file(&path).expect("the language parses"); + let (bytes, report) = sleighc::compile_to_bytes(&spec).expect("compiles"); + let back = Sla::parse(&bytes).expect("reads back"); + let loaded = slaload::load(&back.program); + + let r = compare_decodes(&spec, &loaded.spec, width); + println!( + "{name}: {} encodings swept, source decodes {}, the .sla we wrote decodes {}, \ + {} agree, {} differ", + r.cases, r.source_ok, r.sla_ok, r.agree, r.differ + ); + for e in r.examples.iter().take(4) { + println!(" {e}"); + } + for n in loaded.notes.iter().take(4) { + println!(" note: {n}"); + } + // A constructor whose pattern the compiler could only write + // approximately is a known gap with a number, not a silent one. Where + // there is none, a disagreement is a bug and the gate says so. + if report.approximate_patterns == 0 { + assert_eq!( + r.differ, 0, + "{name} writes every pattern exactly, so a decode must not differ" + ); + } else { + println!( + " {} constructors have a pattern this compiler cannot state exactly", + report.approximate_patterns + ); + } + total_wrong += r.differ; + } + if !any { + println!("no language files found, skipping"); + return; + } + println!("{total_wrong} disagreements over the three architectures"); + // A ceiling, not a target. Every one of these is a constructor whose + // pattern carries a constraint that is not a bit test and that this + // compiler could not enumerate into bits, and the number only moves down. + assert!( + total_wrong <= 8, + "{total_wrong} encodings decode differently through the file we wrote, was 8" + ); +} + +struct DecodeParity { + cases: usize, + source_ok: usize, + sla_ok: usize, + agree: usize, + differ: usize, + examples: Vec, +} + +/// Decode the same sweep with two specifications and compare what came out. +fn compare_decodes(a: &e5r_sleigh::Spec, b: &e5r_sleigh::Spec, width: usize) -> DecodeParity { + const CASES: usize = 20_000; + let mut r = DecodeParity { + cases: 0, + source_ok: 0, + sla_ok: 0, + agree: 0, + differ: 0, + examples: Vec::new(), + }; + let mut da = e5r_sleigh::Decoder::new(a); + let mut db = e5r_sleigh::Decoder::new(b); + let mut state = 0x2545_f491_4f6c_dd1du64; + let mut buf = vec![0u8; width.max(4)]; + for _ in 0..CASES { + for slot in buf.iter_mut() { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + *slot = (state >> 24) as u8; + } + r.cases += 1; + let ra = da.decode(&buf, 0x1000).ok(); + let rb = db.decode(&buf, 0x1000).ok(); + if ra.is_some() { + r.source_ok += 1; + } + if rb.is_some() { + r.sla_ok += 1; + } + let ta = ra.as_ref().map(|d| (d.len, d.text(a))); + let tb = rb.as_ref().map(|d| (d.len, d.text(b))); + if ta == tb { + r.agree += 1; + } else { + r.differ += 1; + if r.examples.len() < 8 { + r.examples + .push(format!("{:02x?}: source {ta:?}, sla {tb:?}", &buf[..])); + } + } + } + r +} diff --git a/crates/e5r-sla/tests/corpus.rs b/crates/e5r-sla/tests/corpus.rs new file mode 100644 index 0000000..9f7b124 --- /dev/null +++ b/crates/e5r-sla/tests/corpus.rs @@ -0,0 +1,266 @@ +//! Load every `.sla` we can find, assert it is self-consistent, and report how +//! much of each file the model accounts for. +//! +//! The committed fixtures under `tests/data` are always read. A real Ghidra +//! tree is read too when one is on the machine: set `E5R_SLA_DIR` to a +//! directory to search, or let the test look in the usual install locations. +//! Following the repository convention, a missing corpus makes the test report +//! and return rather than fail, so a fresh checkout still runs what it can. + +use std::path::{Path, PathBuf}; + +use e5r_sla::{Inconsistency, Sla}; + +fn fixtures() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data") +} + +fn find_sla(dir: &Path, out: &mut Vec) { + // Depth is bounded by the directory tree, and a Ghidra install is about + // six levels deep, so a plain recursion is fine here. + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for e in entries.flatten() { + let p = e.path(); + if p.is_dir() { + find_sla(&p, out); + } else if p.extension().is_some_and(|x| x == "sla") { + out.push(p); + } + } +} + +/// Everything to read: the fixtures, plus any Ghidra tree we can find. +fn corpus() -> Vec { + let mut out = Vec::new(); + find_sla(&fixtures(), &mut out); + + let mut roots: Vec = Vec::new(); + if let Ok(v) = std::env::var("E5R_SLA_DIR") { + roots.extend(v.split(':').filter(|s| !s.is_empty()).map(PathBuf::from)); + } + if let Ok(home) = std::env::var("HOME") { + roots.push(PathBuf::from(&home).join(".local/share/ghidra-cli/ghidra")); + roots.push(PathBuf::from(&home).join("ghidra")); + } + roots.push(PathBuf::from("/opt/ghidra")); + for r in roots { + if r.is_dir() { + find_sla(&r, &mut out); + } + } + out.sort(); + out.dedup(); + out +} + +#[test] +fn every_sla_loads_and_is_self_consistent() { + let files = corpus(); + assert!( + !files.is_empty(), + "the committed fixtures should always be found" + ); + + let mut total_payload = 0usize; + let mut total_interpreted = 0usize; + let mut worst: Option<(f64, PathBuf)> = None; + let mut unknown_ids: Vec<(u32, usize)> = Vec::new(); + let mut failures: Vec = Vec::new(); + + for path in &files { + let sla = match Sla::open(path) { + Ok(s) => s, + Err(e) => { + failures.push(format!("{}: {e}", path.display())); + continue; + } + }; + assert!( + sla.version_is_known(), + "{}: unexpected format version", + path.display() + ); + + let problems = sla.check(); + if !problems.is_empty() { + // Print a bounded sample; a wall of identical lines helps nobody. + let sample: Vec<&Inconsistency> = problems.iter().take(5).collect(); + failures.push(format!( + "{}: {} inconsistencies, first: {sample:?}", + path.display(), + problems.len() + )); + } + + total_payload += sla.coverage.total; + total_interpreted += sla.coverage.interpreted; + let f = sla.coverage.fraction(); + if worst.as_ref().is_none_or(|(w, _)| f < *w) { + worst = Some((f, path.clone())); + } + for u in &sla.coverage.unknown { + match unknown_ids.iter_mut().find(|(id, _)| *id == u.id) { + Some((_, b)) => *b += u.bytes, + None => unknown_ids.push((u.id, u.bytes)), + } + } + } + + unknown_ids.sort_by_key(|(_, b)| std::cmp::Reverse(*b)); + let pct = 100.0 * total_interpreted as f64 / total_payload.max(1) as f64; + println!( + "{} files, {total_payload} payload bytes, {total_interpreted} interpreted ({pct:.2}%), \ + {} raw", + files.len(), + total_payload - total_interpreted + ); + if let Some((f, p)) = &worst { + println!("least covered: {} at {:.2}%", p.display(), f * 100.0); + } + println!( + "uninterpreted element ids by bytes: {:?}", + &unknown_ids[..unknown_ids.len().min(12)] + ); + + assert!(failures.is_empty(), "{}", failures.join("\n")); +} + +#[test] +fn the_minimal_fixture_reads_the_way_its_source_says() { + let sla = Sla::open(fixtures().join("minimal.sla")).expect("fixture loads"); + let p = &sla.program; + + // `define endian = little; define alignment = 1;` + assert!(!p.big_endian); + assert_eq!(p.alignment, 1); + + // Four spaces: OTHER and unique are implicit, ram and register declared. + let names: Vec<&str> = p.spaces.iter().map(|s| s.name.as_str()).collect(); + assert_eq!(names, ["OTHER", "unique", "ram", "register"]); + assert_eq!(p.default_space.as_deref(), Some("ram")); + let ram = p.spaces.iter().find(|s| s.name == "ram").unwrap(); + assert_eq!(ram.size, 8); + + // `define register offset=0x0 size=8 [ sp r0 ];` + let sp = p + .symbols + .iter() + .find(|s| s.name.as_deref() == Some("sp")) + .expect("sp exists"); + match sp.body { + e5r_sla::SymbolBody::Varnode { offset, size, .. } => { + assert_eq!((offset, size), (0, 8)); + } + _ => panic!("sp should be a varnode"), + } + let r0 = p + .symbols + .iter() + .find(|s| s.name.as_deref() == Some("r0")) + .unwrap(); + match r0.body { + e5r_sla::SymbolBody::Varnode { offset, size, .. } => { + assert_eq!((offset, size), (8, 8)); + } + _ => panic!("r0 should be a varnode"), + } + + // One instruction subtable holding the single `:nop` constructor. + assert_eq!(p.constructor_count(), 1); + assert!(sla.check().is_empty()); +} + +#[test] +fn the_token_fixture_reads_its_fields_and_pcode() { + let sla = Sla::open(fixtures().join("tokens.sla")).expect("fixture loads"); + let p = &sla.program; + + // `op = (8,15)` of a little-endian 16-bit token: byte 1, no shift. + let op = p + .symbols + .iter() + .find(|s| s.name.as_deref() == Some("op")) + .expect("op field"); + match &op.body { + e5r_sla::SymbolBody::Value { field: Some(f) } => { + let f = f.token().expect("op is cut from an instruction token"); + assert_eq!((f.start_bit, f.end_bit), (8, 15)); + assert_eq!((f.start_byte, f.end_byte, f.shift), (1, 1, 0)); + assert!(!f.signed); + } + _ => panic!("op should be a token field value"), + } + // `imm = (0,7) signed` + let imm = p + .symbols + .iter() + .find(|s| s.name.as_deref() == Some("imm")) + .unwrap(); + match &imm.body { + e5r_sla::SymbolBody::Value { field: Some(f) } => { + assert!(f.token().expect("a token field").signed); + } + _ => panic!("imm should be a token field value"), + } + // `attach variables [ rs rd ] [ r0 .. sp ]` gives sixteen entries. + let rd = p + .symbols + .iter() + .find(|s| s.name.as_deref() == Some("rd")) + .unwrap(); + match &rd.body { + e5r_sla::SymbolBody::VarnodeList { entries, .. } => assert_eq!(entries.len(), 16), + _ => panic!("rd should be an attached varnode list"), + } + + // Four constructors; `ldi` spans two tokens, so it is four bytes long. + assert_eq!(p.constructor_count(), 4); + let table = p.subtables().next().unwrap(); + let ctors = match &table.body { + e5r_sla::SymbolBody::Subtable { constructors, .. } => constructors, + _ => unreachable!(), + }; + let lengths: Vec = ctors.iter().map(|c| c.length).collect(); + assert_eq!(lengths, [2, 2, 4, 2]); + // They are on consecutive source lines, which is how the line attribute + // was identified in the first place. + let lines: Vec = ctors.iter().map(|c| c.line).collect(); + assert_eq!(lines, [17, 18, 19, 20]); + + // `:add rd, rs ... { rd = rd + rs; }` is one INT_ADD (p-code opcode 19). + let add = &ctors[1]; + assert_eq!(add.templates.len(), 1); + let ops = &add.templates[0].ops; + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].opcode, 19); + assert!(ops[0].output.is_some()); + assert_eq!(ops[0].inputs.len(), 2); + + assert!(sla.check().is_empty()); +} + +#[test] +fn decision_patterns_are_masks_over_the_instruction_bytes() { + let sla = Sla::open(fixtures().join("tokens.sla")).unwrap(); + let table = sla.program.subtables().next().unwrap(); + let d = match &table.body { + e5r_sla::SymbolBody::Subtable { + decision: Some(d), .. + } => d, + _ => panic!("the instruction table should have a decision tree"), + }; + // Four constructors differing in the low two bits of `op`, which is byte 1 + // of the stream, so the split is on absolute bits 14 and 15. + assert_eq!(d.number, 4); + assert_eq!((d.start_bit, d.num_bits), (14, 2)); + assert_eq!(d.children.len(), 4); + + // `:add is op=0x01` constrains one byte at offset 1 to 0x01. + let (_, pat) = &d.children[1].pairs[0]; + let b = &pat.instruction[0]; + assert_eq!((b.offset, b.bytes), (1, 1)); + assert_eq!(b.words[0].mask, 0xff00_0000); + assert_eq!(b.words[0].value, 0x0100_0000); +} diff --git a/crates/e5r-sla/tests/data/README.md b/crates/e5r-sla/tests/data/README.md new file mode 100644 index 0000000..3a62996 --- /dev/null +++ b/crates/e5r-sla/tests/data/README.md @@ -0,0 +1,25 @@ +# `.sla` fixtures + +Three tiny compiled SLEIGH files, used by `tests/corpus.rs` so the suite has +something to read on a machine with no Ghidra installed. + +Provenance: the `.sinc` sources next to them were written for this repository, +and the `.sla` files are what Ghidra 12.1.3's shipped SLEIGH compiler +(`support/sleigh`, Apache-2.0) produced from them. They are the output of a +compiler run on our own input, and they exist so the format work in +`docs/sla-format.md` is reproducible: recompiling a `.sinc` must give the same +`.sla`. + +| file | what it exercises | +| --- | --- | +| `minimal.sla` | the smallest legal language: spaces, two registers, a context field, one constructor with no token | +| `tokens.sla` | a token with four fields, `attach variables`, four constructors, p-code with `INT_ADD`, `INT_SEXT` and `BRANCHIND`, a two-token instruction | +| `subtable.sla` | a subtable operand, `define pcodeop`, a local label and the p-code branch around it | + +To rebuild one: + +``` +/path/to/ghidra/support/sleigh .slaspec .sla +``` + +with a `.slaspec` that sets `ENDIAN` and `RAMSIZE` and includes the `.sinc`. diff --git a/crates/e5r-sla/tests/data/minimal.sinc b/crates/e5r-sla/tests/data/minimal.sinc new file mode 100644 index 0000000..41a0fdc --- /dev/null +++ b/crates/e5r-sla/tests/data/minimal.sinc @@ -0,0 +1,28 @@ +define endian = $(ENDIAN); + +define alignment = 1; + +define space ram type=ram_space size=8 default; + +define space register type=register_space size=4; + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# AT LEAST ONE REGISTER, AND STACK POINTER ARE REQUIRED +# # # # # # # # # # # # # # # # # # # # # # # # # # # # + +define register offset=0x0 size=8 [ sp r0 ]; + +# Define context bits +define register offset=0x100 size=4 contextreg; + +define context contextreg + test=(0,0) +; + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# AT LEAST ONE INSTRUCTION IS REQUIRED +# # # # # # # # # # # # # # # # # # # # # # # # # # # # + +:nop is test=1 unimpl + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # diff --git a/crates/e5r-sla/tests/data/minimal.sla b/crates/e5r-sla/tests/data/minimal.sla new file mode 100644 index 0000000..f46e51a Binary files /dev/null and b/crates/e5r-sla/tests/data/minimal.sla differ diff --git a/crates/e5r-sla/tests/data/subtable.sinc b/crates/e5r-sla/tests/data/subtable.sinc new file mode 100644 index 0000000..9e8892c --- /dev/null +++ b/crates/e5r-sla/tests/data/subtable.sinc @@ -0,0 +1,21 @@ +define endian = little; +define alignment = 1; +define space ram type=ram_space size=8 default; +define space register type=register_space size=4; +define register offset=0x0 size=8 [ r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 sp ]; +define register offset=0x100 size=4 contextreg; +define context contextreg + test=(0,0) +; +define token instr (16) + op = (8,15) + rs = (4,7) + rd = (0,3) + imm = (0,7) signed +; +attach variables [ rs rd ] [ r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 sp ]; +define pcodeop myop; +Mode: "x" is rs=0 { export 0:1; } +Mode: "y" is rs=1 { export 1:1; } +:cnd Mode is op=0x05 & Mode & rd { if (Mode) goto ; rd = 0; } +:usr rd is op=0x06 & rd { myop(rd); } diff --git a/crates/e5r-sla/tests/data/subtable.sla b/crates/e5r-sla/tests/data/subtable.sla new file mode 100644 index 0000000..79becf2 Binary files /dev/null and b/crates/e5r-sla/tests/data/subtable.sla differ diff --git a/crates/e5r-sla/tests/data/tokens.sinc b/crates/e5r-sla/tests/data/tokens.sinc new file mode 100644 index 0000000..e90f5c4 --- /dev/null +++ b/crates/e5r-sla/tests/data/tokens.sinc @@ -0,0 +1,20 @@ +define endian = little; +define alignment = 1; +define space ram type=ram_space size=8 default; +define space register type=register_space size=4; +define register offset=0x0 size=8 [ r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 sp ]; +define register offset=0x100 size=4 contextreg; +define context contextreg + test=(0,0) +; +define token instr (16) + op = (8,15) + rs = (4,7) + rd = (0,3) + imm = (0,7) signed +; +attach variables [ rs rd ] [ r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 sp ]; +:nop is op=0x00 & rd=0 & rs=0 unimpl +:add rd, rs is op=0x01 & rd & rs { rd = rd + rs; } +:ldi rd, imm is op=0x02 & rd ; imm { rd = sext(imm:1); } +:jmp imm is op=0x03 & imm { local t:8 = inst_next + imm; goto [t]; } diff --git a/crates/e5r-sla/tests/data/tokens.sla b/crates/e5r-sla/tests/data/tokens.sla new file mode 100644 index 0000000..ad07ff2 Binary files /dev/null and b/crates/e5r-sla/tests/data/tokens.sla differ diff --git a/crates/e5r-sla/tests/fuzz.rs b/crates/e5r-sla/tests/fuzz.rs new file mode 100644 index 0000000..0e15d2e --- /dev/null +++ b/crates/e5r-sla/tests/fuzz.rs @@ -0,0 +1,316 @@ +//! Mutation fuzzing of the `.sla` reader, in the ordinary test run. +//! +//! The shape follows `e5r-format/tests/fuzz.rs`: a fixed time budget, a +//! deterministic generator so a failure is reproducible from its seed, and one +//! contract to check. Here the contract is that `Sla::parse` given any bytes +//! returns a value or a typed error, never a panic, and returns inside a bound +//! whatever counts the file claims. +//! +//! A `.sla` is a compressed container, so the interesting inputs are of two +//! kinds and both are generated: corrupt the compressed image, which mostly +//! exercises the inflater, and corrupt the decompressed payload and recompress +//! it, which is the only way to reach the element decoder with hostile input. + +use std::path::Path; +use std::time::{Duration, Instant}; + +use e5r_sla::{Sla, decode, inflate}; + +const BUDGET: Duration = Duration::from_millis(1500); + +/// Anything slower than this on one input means a walk that does not advance. +const PER_CASE_CEILING: Duration = Duration::from_secs(2); + +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + x.wrapping_mul(0x2545_f491_4f6c_dd1d) + } + + fn below(&mut self, n: usize) -> usize { + if n == 0 { + 0 + } else { + (self.next() % n as u64) as usize + } + } +} + +fn seeds() -> Vec> { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data"); + let mut out = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&dir) { + for e in entries.flatten() { + let p = e.path(); + if p.extension().is_some_and(|x| x == "sla") { + if let Ok(d) = std::fs::read(&p) { + out.push(d); + } + } + } + } + out +} + +fn mutate(seed: &[u8], rng: &mut Rng) -> Vec { + let mut v = seed.to_vec(); + if v.is_empty() { + return v; + } + match rng.below(6) { + 0 => v.truncate(rng.below(v.len())), + 1 => { + let i = rng.below(v.len()); + v[i] ^= 1 << rng.below(8); + } + 2 => { + let i = rng.below(v.len()); + let end = (i + 8).min(v.len()); + v[i..end].fill(0xff); + } + 3 => { + let i = rng.below(v.len()); + let end = (i + 4).min(v.len()); + v[i..end].fill(0); + } + 4 => { + let from = rng.below(v.len()); + let to = rng.below(v.len()); + let len = rng.below(64).min(v.len() - from.max(to)); + let chunk: Vec = v[from..from + len].to_vec(); + v[to..to + len].copy_from_slice(&chunk); + } + _ => { + let end = 0x80.min(v.len()); + for b in &mut v[..end] { + if rng.below(8) == 0 { + *b = (rng.next() & 0xff) as u8; + } + } + } + } + v +} + +fn timed(case: &[u8]) { + let started = Instant::now(); + let _ = Sla::parse(case); + assert!( + started.elapsed() < PER_CASE_CEILING, + "one input took {:?}; a walk is not advancing", + started.elapsed() + ); +} + +#[test] +fn truncation_at_every_length_is_an_error_not_a_panic() { + for seed in seeds() { + for n in 0..seed.len() { + timed(&seed[..n]); + } + } +} + +#[test] +fn mutated_files_never_panic() { + let corpus = seeds(); + assert!(!corpus.is_empty(), "fixtures should be present"); + let mut rng = Rng(0x51a_0000_0000_0001); + let start = Instant::now(); + let mut n = 0u64; + while start.elapsed() < BUDGET { + let seed = &corpus[rng.below(corpus.len())]; + timed(&mutate(seed, &mut rng)); + n += 1; + } + assert!( + n > 100, + "only {n} cases in {BUDGET:?}; something is very slow" + ); + println!("sla fuzz: {n} mutated files, no panics"); +} + +#[test] +fn arbitrary_bytes_behind_the_magic_never_panic() { + let mut rng = Rng(0x51a_0000_0000_0002); + let start = Instant::now(); + let mut n = 0u64; + while start.elapsed() < BUDGET { + let len = 8 + rng.below(2048); + let mut v: Vec = b"sla\x04".to_vec(); + while v.len() < len { + v.extend_from_slice(&rng.next().to_le_bytes()); + } + v.truncate(len); + timed(&v); + n += 1; + } + assert!(n > 100, "only {n} cases in {BUDGET:?}"); + println!("sla fuzz: {n} random payloads, no panics"); +} + +/// Reach the element decoder directly. Corrupting the compressed image almost +/// always fails the Adler-32 first, so the decoder would otherwise never see a +/// hostile input at all. +#[test] +fn mutated_element_streams_never_panic() { + let mut payloads: Vec> = Vec::new(); + for seed in seeds() { + if seed.len() > 4 { + if let Ok(p) = inflate::inflate_zlib(&seed[4..], inflate::DEFAULT_LIMIT) { + payloads.push(p); + } + } + } + assert!(!payloads.is_empty(), "fixtures should inflate"); + + let mut rng = Rng(0x51a_0000_0000_0003); + let start = Instant::now(); + let mut n = 0u64; + let mut decoded = 0u64; + while start.elapsed() < BUDGET { + let case = mutate(&payloads[rng.below(payloads.len())], &mut rng); + let started = Instant::now(); + if decode::decode(&case).is_ok() { + decoded += 1; + } + assert!( + started.elapsed() < PER_CASE_CEILING, + "one element stream took {:?}", + started.elapsed() + ); + n += 1; + } + assert!(n > 100, "only {n} cases in {BUDGET:?}"); + println!("sla fuzz: {n} mutated element streams, {decoded} still decoded, no panics"); +} + +/// A string length is the one count in the format big enough to matter, so it +/// gets its own case: an attribute claiming a four-gigabyte string must come +/// back as an error without reserving anything. +#[test] +fn a_lying_string_length_does_not_allocate() { + // element 4, attribute 12, string value with five length chunks all 0x7f. + let payload = vec![0x44, 0xcc, 0x75, 0xff, 0xff, 0xff, 0xff, 0xff, 0x84]; + let started = Instant::now(); + assert!(decode::decode(&payload).is_err()); + assert!(started.elapsed() < PER_CASE_CEILING); +} + +/// The inflater has its own bound, and a stored block that claims more than +/// the input holds is the cheapest way to test it. +#[test] +fn a_lying_stored_block_length_does_not_allocate() { + let z = vec![0x78, 0x01, 0x01, 0xff, 0xff, 0x00, 0x00, 0, 0, 0, 0]; + let started = Instant::now(); + assert!(inflate::inflate_zlib(&z, inflate::DEFAULT_LIMIT).is_err()); + assert!(started.elapsed() < PER_CASE_CEILING); +} + +// The writer has the opposite failure mode from the reader: instead of +// trusting a length in the file, it can write a length that is not the one it +// wrote. Nothing downstream would notice until someone else's reader chokes, +// so the contract checked here is that anything the writer produces our own +// reader consumes, with every offset in range and every count true. + +/// Perturb the numbers in a program: sizes, offsets, symbol ids, counts. The +/// result is usually not a language any more, which is the point. +fn perturb(p: &mut e5r_sla::Program, rng: &mut Rng) { + use e5r_sla::model::SymbolBody; + p.alignment = rng.next() % 9; + p.unique_base = rng.next(); + for s in &mut p.spaces { + if rng.next() % 4 == 0 { + s.size = rng.next() % 300; + s.word_size = rng.next() % 5; + } + } + for s in &mut p.symbols { + if rng.next() % 8 != 0 { + continue; + } + match &mut s.body { + SymbolBody::Varnode { offset, size, .. } => { + *offset = rng.next(); + *size = rng.next() % 1024; + } + SymbolBody::Subtable { constructors, .. } => { + for c in constructors.iter_mut() { + c.length = rng.next() % 4096; + c.line = rng.next(); + c.flowthru = (rng.next() % 64) as i64 - 32; + } + } + SymbolBody::Operand { + offset, + base, + min_length, + .. + } => { + *offset = rng.next(); + *base = (rng.next() % 64) as i64 - 32; + *min_length = rng.next() % 256; + } + _ => {} + } + } + // Lie about the counts, which is the sort of thing `Sla::check` exists to + // catch and the writer must not crash on. + if rng.next() % 3 == 0 { + p.declared_symbols = Some(rng.next() % 100_000); + } +} + +#[test] +fn perturbed_programs_always_write_something_readable() { + let Some(seed) = seeds().into_iter().next() else { + return; + }; + let base = Sla::parse(&seed).expect("a fixture parses"); + let mut rng = Rng(0x5eed_1234_abcd_0001); + let start = Instant::now(); + let mut cases = 0usize; + while start.elapsed() < BUDGET { + let mut p = base.program.clone(); + perturb(&mut p, &mut rng); + let case_start = Instant::now(); + // A typed refusal is a correct outcome; a panic is not. + if let Ok(bytes) = e5r_sla::emit::write(&p, 4, e5r_sla::Level::Fixed) { + // Every length written must be the length actually written, which + // is exactly what parsing it back proves. + let back = Sla::parse(&bytes).expect("what we write, we read"); + assert_eq!(back.program.symbols.len(), p.symbols.len()); + // Offsets inside the payload are the writer's own arithmetic. + for i in back.check() { + assert!( + !matches!(i, e5r_sla::Inconsistency::NodeOutOfRange { .. }), + "writer produced a node outside the payload: {i:?}" + ); + } + } + assert!( + case_start.elapsed() < PER_CASE_CEILING, + "one write took {:?}", + case_start.elapsed() + ); + cases += 1; + } + assert!(cases > 4, "only {cases} cases in the budget"); +} + +#[test] +fn a_string_the_length_encoding_cannot_carry_is_refused_not_truncated() { + // Fifteen chunks of seven bits is the ceiling on a string length, which no + // real name comes near; the writer has to say so rather than write a + // length that does not match the bytes. + let mut out = Vec::new(); + let ok = e5r_sla::encode::push_value(&mut out, &e5r_sla::Value::Text("a".repeat(1000))); + assert!(ok.is_ok()); + assert_eq!(out.len(), 1 + 2 + 1000); +} diff --git a/crates/e5r-sla/tests/slaload/mod.rs b/crates/e5r-sla/tests/slaload/mod.rs new file mode 100644 index 0000000..8324e6c --- /dev/null +++ b/crates/e5r-sla/tests/slaload/mod.rs @@ -0,0 +1,889 @@ +//! The other direction: a compiled `.sla` back into the model `e5r-sleigh` +//! decodes from. +//! +//! This exists to make the writer's gate a decode rather than a comparison. +//! `e5r-sleigh` parses a `.slaspec` into a [`sl::Spec`] and its engine decodes +//! AArch64, x86-64 and RISC-V with zero disagreements against objdump. If the +//! same specification, compiled by us and read back through this, decodes the +//! same corpus to the same text, then everything a decoder needs survived the +//! round trip. That is a much stronger statement than "our reader likes what +//! our writer wrote". +//! +//! # What cannot survive, and why the gate reports it rather than hiding it +//! +//! The compiled format keeps a pattern as mask and value bits. Three things in +//! the front end's model have no representation in it: +//! +//! * a **residual** constraint, one that is not a bit test: `r1 = r2`, `f < 3`. +//! * an **approximate** reduction, where the mask is only a filter and the +//! real pattern has to be re-evaluated. That is x86's ModR/M and every +//! `...` that right justifies a sub-pattern. +//! * **per alternative operand offsets**. The file carries one offset per +//! operand; the front end carries one per alternative. +//! +//! A fourth is not a pattern at all: a field's print base. `hex` and `dec` +//! compile to identical trees, so everything read back prints in hex. +//! +//! A language that needs any of them decodes from the file as well as the +//! file allows, which is not always as well as from the source. The gate +//! counts those languages rather than excluding them quietly. +//! +//! Everything else is recovered exactly: spaces, registers, tokens and their +//! fields, attachments, context fields, every constructor's display, operands, +//! patterns and disassembly actions. + +#![allow(dead_code)] + +use std::collections::HashMap; + +use e5r_sla::model as sla; +use e5r_sleigh::model as sl; + +/// A specification recovered from a compiled file, and what could not be. +pub struct Loaded { + pub spec: sl::Spec, + /// One line per thing that was approximated, deduplicated. + pub notes: Vec, +} + +/// Rebuild a decodable specification from a compiled program. +#[must_use] +pub fn load(p: &sla::Program) -> Loaded { + Load::new(p).run() +} + +/// A token field as the file spells it. Two fields with the same key are the +/// same field, which is what lets an operand's inline copy and a family +/// symbol's copy share one entry. +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +struct FieldKey { + big_endian: bool, + signed: bool, + start_bit: u64, + end_bit: u64, + start_byte: u64, + end_byte: u64, + shift: u64, +} + +impl FieldKey { + fn of(t: &sla::TokenField) -> FieldKey { + FieldKey { + big_endian: t.big_endian, + signed: t.signed, + start_bit: t.start_bit, + end_bit: t.end_bit, + start_byte: t.start_byte, + end_byte: t.end_byte, + shift: t.shift, + } + } + + /// How wide the token this field is cut from must be. + /// + /// A big-endian token numbers its bytes from the far end, so the width is + /// exact: `start_byte = size - 1 - end_bit / 8`. A little-endian token + /// does not say, and only a lower bound is recoverable here; an operand + /// that uses the field carries the true width in attribute 18, and + /// [`Load::family_field`] uses it. + fn token_size(&self) -> u32 { + if self.big_endian { + (self.start_byte + 1 + self.end_bit / 8) as u32 + } else { + (self.end_byte + 1) as u32 + } + } +} + +struct Load<'a> { + p: &'a sla::Program, + spec: sl::Spec, + notes: Vec, + varnode_of: HashMap, + /// A family symbol's own field, per token width it is used at. + /// + /// The width has to be part of the key. A compressed RISC-V instruction + /// and a full one cut the same bits out of tokens of different widths, and + /// the width is what says how many bytes the operand consumes, so one + /// field for both makes every compressed instruction four bytes long. + field_of: HashMap<(u32, u32), sl::FieldId>, + /// The body a family symbol carries, so its field can be made on demand at + /// whatever width the operand using it says. + family: HashMap, + context_of: HashMap, + table_of: HashMap, + /// Anonymous inline fields only. A family symbol always gets a field of + /// its own: two symbols can cut the same bits and attach different + /// register lists to them, which is how every architecture spells its + /// floating point registers, and merging them swaps `s9` for `fs9`. + anon_fields: HashMap<(FieldKey, u32), sl::FieldId>, + anon_context: HashMap<(u64, u64), sl::ContextFieldId>, +} + +impl<'a> Load<'a> { + fn new(p: &'a sla::Program) -> Load<'a> { + Load { + p, + spec: sl::Spec { + endian: if p.big_endian { + sl::Endian::Big + } else { + sl::Endian::Little + }, + alignment: p.alignment.max(1) as u32, + spaces: Vec::new(), + default_space: None, + varnodes: Vec::new(), + tokens: Vec::new(), + fields: Vec::new(), + context_fields: Vec::new(), + context_register: None, + bitranges: Vec::new(), + pcodeops: Vec::new(), + macros: Vec::new(), + tables: Vec::new(), + constructors: Vec::new(), + symbols: HashMap::new(), + warnings: Vec::new(), + }, + notes: Vec::new(), + varnode_of: HashMap::new(), + field_of: HashMap::new(), + family: HashMap::new(), + context_of: HashMap::new(), + table_of: HashMap::new(), + anon_fields: HashMap::new(), + anon_context: HashMap::new(), + } + } + + fn note(&mut self, s: impl Into) { + let s = s.into(); + if !self.notes.contains(&s) { + self.notes.push(s); + } + } + + fn run(mut self) -> Loaded { + self.spaces(); + self.varnodes(); + self.families(); + self.tables(); + self.constructors(); + self.name_symbols(); + Loaded { + spec: self.spec, + notes: self.notes, + } + } + + /// Space indices are kept as they are, so `SpaceId(i)` is the file's own + /// index. Slot 0 is the constant space, which the file never lists because + /// it is the same in every language. + fn spaces(&mut self) { + let top = self.p.spaces.iter().map(|s| s.index).max().unwrap_or(0); + self.spec.spaces = (0..=top) + .map(|_| sl::Space { + name: String::new(), + kind: sl::SpaceKind::Ram, + size: 0, + wordsize: 1, + default: false, + }) + .collect(); + self.spec.spaces[0] = sl::Space { + name: "const".into(), + kind: sl::SpaceKind::Constant, + size: 8, + wordsize: 1, + default: false, + }; + for s in &self.p.spaces { + let default = self.p.default_space.as_deref() == Some(s.name.as_str()); + self.spec.spaces[s.index as usize] = sl::Space { + name: s.name.clone(), + kind: match s.kind { + sla::SpaceKind::Unique => sl::SpaceKind::Unique, + // The file says whether a space is physical and how long a + // read from it takes, not whether it holds registers. A + // decoder does not care which it is. + _ => sl::SpaceKind::Ram, + }, + size: s.size as u32, + wordsize: s.word_size.max(1) as u32, + default, + }; + if default { + self.spec.default_space = Some(sl::SpaceId(s.index)); + } + } + } + + fn varnodes(&mut self) { + for s in &self.p.symbols { + let sla::SymbolBody::Varnode { + space, + offset, + size, + } = &s.body + else { + continue; + }; + let id = sl::VarnodeId(self.spec.varnodes.len() as u32); + self.spec.varnodes.push(sl::Varnode { + name: s.name.clone().unwrap_or_default(), + space: sl::SpaceId(*space), + offset: *offset, + size: *size as u32, + }); + self.varnode_of.insert(s.id, id); + } + } + + /// The token a field is cut from, interned by width and byte order. + fn token(&mut self, k: FieldKey, size: u32) -> sl::TokenId { + let endian = if k.big_endian { + sl::Endian::Big + } else { + sl::Endian::Little + }; + for (i, t) in self.spec.tokens.iter().enumerate() { + if t.size == size && t.endian == endian { + return sl::TokenId(i as u32); + } + } + let id = sl::TokenId(self.spec.tokens.len() as u32); + self.spec.tokens.push(sl::TokenDef { + name: format!("token{}", self.spec.tokens.len()), + size, + endian, + }); + id + } + + /// A field of its own, named by the family symbol that owns it. + fn new_field( + &mut self, + t: &sla::TokenField, + name: &str, + attach: sl::Attach, + width: u32, + ) -> sl::FieldId { + let k = FieldKey::of(t); + let token = self.token(k, width.max(k.token_size())); + let id = sl::FieldId(self.spec.fields.len() as u32); + self.spec.fields.push(sl::Field { + name: if name.is_empty() { + format!("${}", id.index()) + } else { + name.to_owned() + }, + token, + low: t.start_bit as u32, + high: t.end_bit as u32, + signed: t.signed, + // The compiled form does not record how a field prints: `hex` and + // `dec` compile to identical trees. Hex is SLEIGH's own default + // and the only thing a file leaves a reader able to do. + base: sl::NumberBase::Hex, + attach, + }); + id + } + + /// An operand that writes its bits out inline rather than naming a family + /// symbol. These carry no attachment, so one entry per distinct bit range + /// is enough and keeps the field table small. + fn anon_field(&mut self, t: &sla::TokenField, width: u32) -> sl::FieldId { + let k = FieldKey::of(t); + let width = width.max(k.token_size()); + if let Some(id) = self.anon_fields.get(&(k, width)) { + return *id; + } + let id = self.new_field(t, "", sl::Attach::None, width); + self.anon_fields.insert((k, width), id); + id + } + + /// The field a family symbol stands for, at the width the operand naming + /// it says its token is. + fn family_field(&mut self, sym: u32, width: u32) -> Option { + let (name, t, attach) = self.family.get(&sym)?.clone(); + let width = width.max(FieldKey::of(&t).token_size()); + if let Some(id) = self.field_of.get(&(sym, width)) { + return Some(*id); + } + let id = self.new_field(&t, &name, attach, width); + self.field_of.insert((sym, width), id); + Some(id) + } + + fn new_context_field( + &mut self, + f: &sla::ContextField, + name: &str, + register: Option, + flow: bool, + attach: sl::Attach, + ) -> sl::ContextFieldId { + let id = sl::ContextFieldId(self.spec.context_fields.len() as u32); + self.spec.context_fields.push(sl::ContextField { + name: name.to_owned(), + register: register + .or(self.spec.context_register) + .unwrap_or(sl::VarnodeId(0)), + low: f.start_bit as u32, + high: f.end_bit as u32, + signed: f.signed, + base: sl::NumberBase::Hex, + noflow: !flow, + attach, + }); + id + } + + /// A context field written out inline, with no symbol naming it. + fn anon_context_field(&mut self, f: &sla::ContextField) -> sl::ContextFieldId { + let k = (f.start_bit, f.end_bit); + if let Some(id) = self.anon_context.get(&k) { + return *id; + } + let id = self.new_context_field(f, "", None, true, sl::Attach::None); + self.anon_context.insert(k, id); + id + } + + /// Value, name, valuemap, `attach variables` and context symbols: the + /// things an operand names rather than carrying inline. + fn families(&mut self) { + // The context register first, so a context field can name it. + for s in &self.p.symbols { + if let sla::SymbolBody::Context { varnode, .. } = &s.body { + if let Some(v) = self.varnode_of.get(varnode) { + self.spec.context_register = Some(*v); + break; + } + } + } + for i in 0..self.p.symbols.len() { + let s = &self.p.symbols[i]; + let id = s.id; + let name = s.name.clone().unwrap_or_default(); + let attach = self.attach_of(&s.body); + match &s.body { + sla::SymbolBody::Context { + varnode, + flow, + field: Some(f), + .. + } => { + let reg = self.varnode_of.get(varnode).copied(); + let f = f.clone(); + let flow = *flow; + let cf = self.new_context_field(&f, &name, reg, flow, sl::Attach::None); + self.context_of.insert(id, cf); + } + body @ (sla::SymbolBody::Value { .. } + | sla::SymbolBody::ValueMap { .. } + | sla::SymbolBody::Name { .. } + | sla::SymbolBody::VarnodeList { .. }) => match family_field(body) { + Some(sla::FieldDef::Token(t)) => { + self.family.insert(id, (name.clone(), t.clone(), attach)); + } + // A family symbol over the context register: `attach + // variables` applied to a context field replaces the + // context symbol outright, which the reference compiler + // confirms by refusing to use such a name as a context + // lvalue. + Some(sla::FieldDef::Context(c)) => { + let c = c.clone(); + let cf = self.new_context_field(&c, &name, None, true, attach); + self.context_of.insert(id, cf); + } + None => {} + }, + _ => {} + } + } + } + + fn attach_of(&self, body: &sla::SymbolBody) -> sl::Attach { + match body { + sla::SymbolBody::VarnodeList { entries, .. } => sl::Attach::Variables( + entries + .iter() + .map(|e| e.and_then(|v| self.varnode_of.get(&v).copied())) + .collect(), + ), + sla::SymbolBody::Name { names, .. } => sl::Attach::Names(names.clone()), + sla::SymbolBody::ValueMap { values, .. } => { + sl::Attach::Values(values.iter().map(|v| Some(*v as i64)).collect()) + } + _ => sl::Attach::None, + } + } + + /// Tables, with the file's root subtable at [`sl::Spec::root`]. + fn tables(&mut self) { + let mut order: Vec = Vec::new(); + if self.p.symbols.iter().any(|s| s.id == 0) { + order.push(0); + } + for s in &self.p.symbols { + if matches!(s.body, sla::SymbolBody::Subtable { .. }) && s.id != 0 { + order.push(s.id); + } + } + for id in order { + let t = sl::TableId(self.spec.tables.len() as u32); + let name = self + .p + .symbol(id) + .and_then(|s| s.name.clone()) + .unwrap_or_default(); + self.spec.tables.push(sl::Table { + name, + constructors: Vec::new(), + min_length: 0, + max_length: 0, + }); + self.table_of.insert(id, t); + } + } + + fn constructors(&mut self) { + let ids: Vec = self.table_of.keys().copied().collect(); + let mut by_table: Vec<(sl::TableId, u32)> = ids + .iter() + .map(|id| (self.table_of[id], *id)) + .collect::>(); + by_table.sort_by_key(|(t, _)| t.index()); + + for (tid, sym) in by_table { + let Some(symbol) = self.p.symbol(sym) else { + continue; + }; + let sla::SymbolBody::Subtable { + constructors, + decision, + } = &symbol.body + else { + continue; + }; + // One decision tree covers the whole table; a constructor's + // alternatives are the pairs that name it. + let mut alts: Vec> = vec![Vec::new(); constructors.len()]; + if let Some(d) = decision { + collect_pairs(d, &mut alts); + } + let (mut min, mut max) = (usize::MAX, 0usize); + let mut list = Vec::new(); + for (k, c) in constructors.iter().enumerate() { + let cid = sl::ConstructorId(self.spec.constructors.len() as u32); + let built = self.constructor(tid, c, &alts[k]); + min = min.min(built.resolved.min_length()); + max = max.max( + built + .resolved + .alternatives + .iter() + .map(|a| a.length) + .max() + .unwrap_or(0), + ); + self.spec.constructors.push(built); + list.push(cid); + } + let t = &mut self.spec.tables[tid.index()]; + t.constructors = list; + t.min_length = if min == usize::MAX { 0 } else { min }; + t.max_length = max; + } + } + + fn constructor( + &mut self, + table: sl::TableId, + c: &sla::Constructor, + pats: &[sla::Pattern], + ) -> sl::Constructor { + let operands: Vec = c + .operands + .iter() + .map(|id| self.operand(*id)) + .collect::>(); + let offsets: Vec = operands.iter().map(|o| o.offset).collect(); + let length = c.length as usize; + + let mut alternatives: Vec = pats + .iter() + .map(|p| sl::PatternAlt { + instr: mask_value(&p.instruction), + context: mask_value(&p.context), + length, + residual: Vec::new(), + offsets: offsets.clone(), + }) + .collect(); + if alternatives.is_empty() { + alternatives.push(sl::PatternAlt { + instr: sl::MaskValue::default(), + context: sl::MaskValue::default(), + length, + residual: Vec::new(), + offsets, + }); + } + + let display = sl::Display { + // The file writes the mnemonic as the first print pieces, so + // rendering puts it back without a separate slot. + mnemonic: None, + pieces: c + .print + .iter() + .map(|p| match p { + sla::PrintPiece::Literal(s) => sl::DisplayPiece::Literal(s.clone()), + sla::PrintPiece::Operand(i) => sl::DisplayPiece::Operand(*i as u16), + }) + .collect(), + }; + + let disasm = self.disasm(c); + let file = self + .p + .source_files + .iter() + .find(|f| f.index == c.source) + .map(|f| f.name.clone()) + .unwrap_or_default(); + + sl::Constructor { + table, + display, + order: (0..operands.len() as u16).collect(), + operands, + locals: Vec::new(), + labels: Vec::new(), + // Never evaluated: nothing here is approximate and no alternative + // carries a residual, because the file cannot say so. + pattern: sl::PatternExpr::Epsilon, + resolved: sl::ResolvedPattern { + alternatives, + approximation: None, + }, + disasm, + // The compiler does not write p-code yet, so every constructor + // read back looks implemented with an empty body rather than + // `unimpl`. Nothing in a decode depends on it. + body: Some(Vec::new()), + location: e5r_sleigh::Location::new(file, c.line as u32), + } + } + + fn operand(&mut self, id: u32) -> sl::Operand { + let Some(s) = self.p.symbol(id) else { + return sl::Operand { + name: String::new(), + source: sl::OperandSource::Unbound, + offset: sl::Offset::absolute(0), + invisible: false, + }; + }; + let name = s.name.clone().unwrap_or_default(); + let sla::SymbolBody::Operand { + offset, + sub_symbol, + base, + min_length, + expr, + .. + } = &s.body + else { + return sl::Operand { + name, + source: sl::OperandSource::Unbound, + offset: sl::Offset::absolute(0), + invisible: false, + }; + }; + let place = sl::Offset { + base: if *base < 0 { None } else { Some(*base as u16) }, + delta: *offset as usize, + }; + let width = *min_length as u32; + let source = match sub_symbol { + Some(sub) => { + if let Some(t) = self.table_of.get(sub) { + sl::OperandSource::Table(*t) + } else if let Some(f) = self.family_field(*sub, width) { + sl::OperandSource::Field(f) + } else if let Some(c) = self.context_of.get(sub) { + sl::OperandSource::Context(*c) + } else if let Some(v) = self.varnode_of.get(sub) { + sl::OperandSource::Varnode(*v) + } else { + sl::OperandSource::Unbound + } + } + None => { + // No family symbol: either the bits are written out inline, or + // the operand is computed by a disassembly action. + match expr.get(1) { + Some(sla::Expr::TokenField(t)) => { + let t = t.clone(); + sl::OperandSource::Field(self.anon_field(&t, width)) + } + Some(sla::Expr::Context(c)) => { + let c = c.clone(); + sl::OperandSource::Context(self.anon_context_field(&c)) + } + Some(_) => sl::OperandSource::Computed, + None => sl::OperandSource::Unbound, + } + } + }; + sl::Operand { + name, + source, + offset: place, + invisible: false, + } + } + + /// The disassembly action section, rebuilt from the two places the file + /// keeps it: the context records on the constructor and the expression in + /// each computed operand's own body. + /// + /// The decoder runs context assignments during the descent and everything + /// else afterwards, so what has to be preserved is the order within each + /// group, not between them. + fn disasm(&mut self, c: &sla::Constructor) -> Vec { + let mut out = Vec::new(); + for op in &c.context_ops { + if let sla::ContextOp::Set { + word, mask, value, .. + } = op + { + let Some(cf) = self.context_by_bits(*word, *mask) else { + self.note("a context assignment names bits no context field covers"); + continue; + }; + let Some(v) = value.first() else { continue }; + out.push(sl::DisasmStmt::Assign { + target: sl::DisasmTarget::Context(cf), + value: self.expr(v), + }); + } + } + for (i, id) in c.operands.iter().enumerate() { + let Some(s) = self.p.symbol(*id) else { + continue; + }; + let sla::SymbolBody::Operand { expr, .. } = &s.body else { + continue; + }; + for e in expr.iter().skip(1) { + if matches!(e, sla::Expr::TokenField(_) | sla::Expr::Context(_)) { + continue; + } + let e = e.clone(); + let value = self.expr(&e); + out.push(sl::DisasmStmt::Assign { + target: sl::DisasmTarget::Operand(i as u16), + value, + }); + } + } + for op in &c.context_ops { + if let sla::ContextOp::Commit { + symbol, word, mask, .. + } = op + { + let Some(cf) = self.context_by_bits(*word, *mask) else { + self.note("a globalset names bits no context field covers"); + continue; + }; + out.push(sl::DisasmStmt::GlobalSet { + address: self.globalset_address(*symbol, c), + context: cf, + }); + } + } + out + } + + /// Which context field a record's `(word, mask)` names. + fn context_by_bits(&self, word: u64, mask: u64) -> Option { + for (i, f) in self.spec.context_fields.iter().enumerate() { + if u64::from(f.low / 32) != word { + continue; + } + let high = f.high.min(f.low / 32 * 32 + 31); + let shift = 31 - high % 32; + let width = high - f.low + 1; + let m = if width >= 64 { + u64::MAX + } else { + ((1u64 << width) - 1) << shift + }; + if m == mask { + return Some(sl::ContextFieldId(i as u32)); + } + } + None + } + + /// The address a `globalset` publishes at, which the file writes as a + /// symbol id: one of the three built-in addresses, or an operand. + fn globalset_address(&self, symbol: u32, c: &sla::Constructor) -> sl::DisasmExpr { + if let Some(i) = c.operands.iter().position(|o| *o == symbol) { + return sl::DisasmExpr::Symbol(sl::SymbolRef::Operand(i as u16)); + } + sl::DisasmExpr::Symbol(sl::SymbolRef::Builtin(match symbol { + 2 => sl::Builtin::InstNext, + 3 => sl::Builtin::InstNext2, + _ => sl::Builtin::InstStart, + })) + } + + fn expr(&mut self, e: &sla::Expr) -> sl::DisasmExpr { + match e { + sla::Expr::Constant(v) => sl::DisasmExpr::Num(*v as i64), + sla::Expr::InstStart => { + sl::DisasmExpr::Symbol(sl::SymbolRef::Builtin(sl::Builtin::InstStart)) + } + sla::Expr::InstNext => { + sl::DisasmExpr::Symbol(sl::SymbolRef::Builtin(sl::Builtin::InstNext)) + } + sla::Expr::InstNext2 => { + sl::DisasmExpr::Symbol(sl::SymbolRef::Builtin(sl::Builtin::InstNext2)) + } + sla::Expr::OperandValue { index, .. } => { + sl::DisasmExpr::Symbol(sl::SymbolRef::Operand(*index as u16)) + } + sla::Expr::TokenField(t) => { + let t = t.clone(); + let f = self.anon_field(&t, 0); + sl::DisasmExpr::Symbol(sl::SymbolRef::Field(f)) + } + sla::Expr::Context(cf) => { + let cf = cf.clone(); + let id = self.anon_context_field(&cf); + sl::DisasmExpr::Symbol(sl::SymbolRef::Context(id)) + } + sla::Expr::Op { op, operands } => { + let mut args: Vec = operands.iter().map(|o| self.expr(o)).collect(); + let unary = |k| match k { + sla::PatternOp::Minus => Some(sl::DisasmUnOp::Negate), + sla::PatternOp::Not => Some(sl::DisasmUnOp::Not), + _ => None, + }; + if let Some(u) = unary(*op) { + if args.len() == 1 { + return sl::DisasmExpr::Unary(u, Box::new(args.remove(0))); + } + } + let b = match op { + sla::PatternOp::Plus => sl::DisasmBinOp::Add, + sla::PatternOp::Sub | sla::PatternOp::Minus => sl::DisasmBinOp::Sub, + sla::PatternOp::Mult => sl::DisasmBinOp::Mul, + sla::PatternOp::Div => sl::DisasmBinOp::Div, + sla::PatternOp::LeftShift => sl::DisasmBinOp::Shl, + sla::PatternOp::RightShift => sl::DisasmBinOp::Shr, + sla::PatternOp::And | sla::PatternOp::Not => sl::DisasmBinOp::And, + sla::PatternOp::Or => sl::DisasmBinOp::Or, + sla::PatternOp::Xor => sl::DisasmBinOp::Xor, + }; + if args.len() < 2 { + return args.pop().unwrap_or(sl::DisasmExpr::Num(0)); + } + let rhs = args.remove(1); + let lhs = args.remove(0); + sl::DisasmExpr::Binary(b, Box::new(lhs), Box::new(rhs)) + } + sla::Expr::Unknown { .. } => sl::DisasmExpr::Num(0), + } + } + + /// The global scope, which a caller uses to find a context field by name. + fn name_symbols(&mut self) { + for (i, f) in self.spec.context_fields.iter().enumerate() { + if !f.name.is_empty() { + self.spec.symbols.insert( + f.name.clone(), + sl::Symbol::Context(sl::ContextFieldId(i as u32)), + ); + } + } + for (i, f) in self.spec.fields.iter().enumerate() { + if !f.name.starts_with('$') { + self.spec + .symbols + .insert(f.name.clone(), sl::Symbol::Field(sl::FieldId(i as u32))); + } + } + for (i, v) in self.spec.varnodes.iter().enumerate() { + self.spec + .symbols + .insert(v.name.clone(), sl::Symbol::Varnode(sl::VarnodeId(i as u32))); + } + for (i, t) in self.spec.tables.iter().enumerate() { + self.spec + .symbols + .insert(t.name.clone(), sl::Symbol::Table(sl::TableId(i as u32))); + } + } +} + +/// The field a family symbol reads its bits from, whichever kind it is. +fn family_field(body: &sla::SymbolBody) -> Option<&sla::FieldDef> { + match body { + sla::SymbolBody::Value { field } + | sla::SymbolBody::ValueMap { field, .. } + | sla::SymbolBody::Name { field, .. } + | sla::SymbolBody::VarnodeList { field, .. } => field.as_ref(), + _ => None, + } +} + +/// Every `(constructor, pattern)` pair in a decision tree, gathered by +/// constructor. Our own writer emits one flat node; the reference emits a tree +/// split on bits, and both mean the same set of pairs. +fn collect_pairs(d: &sla::Decision, out: &mut Vec>) { + for (ix, p) in &d.pairs { + if let Some(slot) = out.get_mut(*ix as usize) { + slot.push(p.clone()); + } + } + for c in &d.children { + collect_pairs(c, out); + } +} + +/// A run of pattern blocks as one mask and value over bytes from the +/// instruction's own first byte. +fn mask_value(blocks: &[sla::PatternBlock]) -> sl::MaskValue { + let mut mask: Vec = Vec::new(); + let mut value: Vec = Vec::new(); + for b in blocks { + for (i, w) in b.words.iter().enumerate() { + for j in 0..4usize { + let at = b.offset as usize + i * 4 + j; + // A block says how many bytes it covers; a word is always four + // and the tail beyond `bytes` is padding. + if at >= b.offset as usize + b.bytes as usize { + continue; + } + if mask.len() <= at { + mask.resize(at + 1, 0); + value.resize(at + 1, 0); + } + let shift = 24 - 8 * j; + mask[at] |= ((w.mask >> shift) & 0xff) as u8; + value[at] |= ((w.value >> shift) & 0xff) as u8; + } + } + } + sl::MaskValue { mask, value } +} diff --git a/crates/e5r-sla/tests/sleighc/mod.rs b/crates/e5r-sla/tests/sleighc/mod.rs new file mode 100644 index 0000000..a85993b --- /dev/null +++ b/crates/e5r-sla/tests/sleighc/mod.rs @@ -0,0 +1,1164 @@ +//! The SLEIGH compiler: an `e5r-sleigh` specification to the model +//! `e5r-sla` writes, and from there to `.sla` bytes. +//! +//! # Why this is in the test harness +//! +//! It should be a module of `e5r-sleigh`, next to the front end whose model +//! it consumes. It is not, because `e5r-sleigh`'s decode engine reads a +//! compiled `.sla` and will therefore depend on `e5r-sla`, and cargo refuses +//! a cycle between two normal dependencies. A cycle through a dev-dependency +//! is allowed, so the compiler sits here until it can move, which is a file +//! move and no change to the code. +//! +//! # What it compiles and what it does not +//! +//! Everything a decoder needs: the address spaces, the register table, the +//! token and context fields, the attachments, the symbol table, every +//! constructor with its display, its operands, its bit patterns, its +//! disassembly actions and its subtable's decision tree. A file it writes +//! decodes RISC-V and x86-64 identically to the specification it came from, +//! and AArch64 identically but for eight encodings in twenty thousand; see +//! `docs/sla-format.md`. +//! +//! Not the p-code. A `construct_tpl` needs two things this project has not +//! established by experiment: the rule the reference compiler uses to hand out +//! offsets in the unique space, and the opcode numbers of the template-only +//! operations that surround a local label. Guessing either would produce a +//! file that looks right and lifts wrong, which is the one failure mode this +//! project refuses. So a constructor is written with no `construct_tpl` at +//! all, which is how the reference compiler writes `unimpl`, and +//! [`Report::without_semantics`] counts how many that is rather than letting +//! it pass unsaid. +//! +//! # Where it differs from the reference compiler on purpose +//! +//! * **The decision tree.** The reference splits on a run of bits it picks per +//! subtable. This writes one flat node whose pairs are tried in order. That +//! is a correct decision tree and a slower one, and it is what +//! `Decision { num_bits: 0 }` means in the format. +//! * **`define pcodeop` ids.** The reference numbers a user operation where +//! the source declares it, which can be between two constructors. The front +//! end does not record where a `pcodeop` was declared, so they are all +//! numbered together after the context fields. Every other symbol is +//! numbered exactly as the reference numbers it. +//! +//! # Where it differs and should not +//! +//! A pattern constraint that is not a bit test has no place in the format. A +//! `field = field` equality is enumerated into one alternative per value, the +//! way the reference does, but an inequality or a disjunction is not, and the +//! pattern written then matches more than the constructor should. +//! [`Report::approximate_patterns`] counts those. + +#![allow(dead_code)] + +use e5r_sla::model as sla; +use e5r_sleigh::model as sl; + +/// What the compiler could not do, counted rather than hidden. +#[derive(Debug, Clone, Default)] +pub struct Report { + pub constructors: usize, + /// Constructors with a semantic body in the source that were written with + /// no p-code template. + pub without_semantics: usize, + /// Disassembly action statements written: context assignments, computed + /// operands and `globalset`s. + pub context_sets: usize, + pub computed_operands: usize, + pub globalsets: usize, + /// Tables no operand names, which the reference drops and so do we. + pub dropped_tables: usize, + /// Constructors whose pattern reduced only approximately, so the bit + /// tests written are a filter and not the pattern. + pub approximate_patterns: usize, + /// Anything else worth saying, one line each, deduplicated. + pub notes: Vec, +} + +impl Report { + fn note(&mut self, s: impl Into) { + let s = s.into(); + if !self.notes.contains(&s) { + self.notes.push(s); + } + } +} + +/// Compile a parsed specification. +#[must_use] +pub fn compile(spec: &sl::Spec) -> (sla::Program, Report) { + Compiler::new(spec).run() +} + +/// Compile and write a whole `.sla` file image. +/// +/// # Errors +/// Only if the model holds something the tag encoding cannot carry. +pub fn compile_to_bytes(spec: &sl::Spec) -> Result<(Vec, Report), e5r_sla::SlaError> { + let (p, r) = compile(spec); + let bytes = e5r_sla::emit::write(&p, e5r_sla::KNOWN_VERSION, e5r_sla::Level::Fixed)?; + Ok((bytes, r)) +} + +/// The three space indices the compiled form reserves before any declared +/// space: the constant space is 0 and never appears in the table, `OTHER` is +/// 1 and `unique` is 2. +const SPACE_CONST: u32 = 0; +const SPACE_OTHER: u32 = 1; +const SPACE_UNIQUE: u32 = 2; +const FIRST_DECLARED_SPACE: u32 = 3; + +struct Compiler<'a> { + spec: &'a sl::Spec, + report: Report, + /// Model space index to compiled space index. + space_ix: Vec, + /// Symbol ids, by kind. + varnode_id: Vec, + field_id: Vec, + context_id: Vec, + userop_id: Vec, + table_id: Vec, + /// Whether a table is written at all. The reference compiler warns + /// "Unreferenced table" and drops one no operand names, and dropping it + /// takes its constructors and its symbol id with it. + keep_table: Vec, + /// `(table, constructor within table, operand)` to symbol id. + operand_id: Vec>, + /// Constructor index in `spec.constructors` to `(table symbol id, index + /// within that table)`. + ct_place: Vec<(u32, u64)>, + source_files: Vec, + /// The token width, when every token in the specification has the same + /// one. That is the only case where a field's byte offset inside the + /// instruction is known here without redoing the front end's pattern + /// arithmetic, and it is what lets a `field = field` constraint be + /// expanded into bits. + uniform_token: Option, +} + +impl<'a> Compiler<'a> { + fn new(spec: &'a sl::Spec) -> Self { + Self { + spec, + report: Report::default(), + space_ix: Vec::new(), + varnode_id: Vec::new(), + field_id: Vec::new(), + context_id: Vec::new(), + userop_id: Vec::new(), + table_id: Vec::new(), + keep_table: Vec::new(), + operand_id: Vec::new(), + ct_place: Vec::new(), + source_files: Vec::new(), + uniform_token: None, + } + } + + fn run(mut self) -> (sla::Program, Report) { + let first = self.spec.tokens.first().map(|t| t.size); + self.uniform_token = first.filter(|w| self.spec.tokens.iter().all(|t| t.size == *w)); + self.map_spaces(); + self.reachable_tables(); + self.assign_ids(); + self.collect_sources(); + + let big_endian = self.spec.endian == sl::Endian::Big; + let mut p = sla::Program::empty(); + p.version = Some(u64::from(e5r_sla::KNOWN_VERSION)); + p.big_endian = big_endian; + p.alignment = u64::from(self.spec.alignment.max(1)); + // Nothing is allocated in the unique space while no p-code is written, + // so the next free offset is still the base. + p.unique_base = 0; + + for (i, name) in self.source_files.iter().enumerate() { + p.source_files.push(sla::SourceFile { + name: name.clone(), + index: i as u64, + }); + } + + p.default_space = self + .spec + .default_space + .map(|id| self.spec.space(id).name.clone()); + p.spaces = self.spaces(big_endian); + + let (scopes, symbols) = self.symbols(); + p.declared_scopes = Some(scopes.len() as u64); + p.declared_symbols = Some(symbols.len() as u64); + p.scopes = scopes; + p.symbols = symbols; + + p.reindex(); + (p, self.report) + } + + /// The compiled form numbers spaces differently from the front end: it + /// puts `OTHER` at 1 and `unique` at 2, and the front end has no `OTHER` + /// at all. So the mapping is built rather than assumed. + fn map_spaces(&mut self) { + let mut next = FIRST_DECLARED_SPACE; + self.space_ix = self + .spec + .spaces + .iter() + .map(|s| match s.kind { + sl::SpaceKind::Constant => SPACE_CONST, + sl::SpaceKind::Unique => SPACE_UNIQUE, + _ => { + let ix = next; + next += 1; + ix + } + }) + .collect(); + } + + fn spaces(&self, big_endian: bool) -> Vec { + // `OTHER` and `unique` are the same in every one of the 137 shipped + // files: eight bytes and four bytes, no delay, and only `unique` is + // physical. They do not follow the specification's own spaces. + let mut out = vec![ + sla::Space { + kind: sla::SpaceKind::Other, + name: "OTHER".into(), + index: SPACE_OTHER, + big_endian, + delay: 0, + size: 8, + word_size: 1, + physical: false, + }, + sla::Space { + kind: sla::SpaceKind::Unique, + name: "unique".into(), + index: SPACE_UNIQUE, + big_endian, + delay: 0, + size: 4, + word_size: 1, + physical: true, + }, + ]; + for (i, s) in self.spec.spaces.iter().enumerate() { + if matches!(s.kind, sl::SpaceKind::Constant | sl::SpaceKind::Unique) { + continue; + } + out.push(sla::Space { + kind: sla::SpaceKind::Normal, + name: s.name.clone(), + index: self.space_ix[i], + big_endian, + // A memory space takes an instruction's worth of delay to + // read; a register space does not. + delay: u64::from(!matches!(s.kind, sl::SpaceKind::Register)), + size: u64::from(s.size), + word_size: u64::from(s.wordsize.max(1)), + physical: true, + }); + } + out + } + + /// Which tables the file gets. The root always, and any table an operand + /// names. + /// + /// **Proved** against the reference: `ADDR8` in `6502.slaspec` is defined + /// and never used, the reference warns "Unreferenced table" and its + /// `.sla` has neither the table nor its constructor, and every symbol id + /// after it shifts down by two. The test is not transitive, because the + /// reference's warning is per table rather than a traversal, and nothing + /// in the corpus distinguishes the two. + fn reachable_tables(&mut self) { + let mut keep = vec![false; self.spec.tables.len()]; + if !keep.is_empty() { + keep[0] = true; + } + for c in &self.spec.constructors { + for o in &c.operands { + if let sl::OperandSource::Table(t) = o.source { + if let Some(k) = keep.get_mut(t.index()) { + *k = true; + } + } + } + } + self.report.dropped_tables = keep.iter().filter(|k| !**k).count(); + self.keep_table = keep; + } + + fn kept(&self, t: usize) -> bool { + self.keep_table.get(t).copied().unwrap_or(true) + } + + fn assign_ids(&mut self) { + let mut next = 4u32; // 0 root table, 1 inst_start, 2 inst_next, 3 inst_next2 + self.varnode_id = (0..self.spec.varnodes.len()) + .map(|_| { + next += 1; + next - 1 + }) + .collect(); + self.field_id = (0..self.spec.fields.len()) + .map(|_| { + next += 1; + next - 1 + }) + .collect(); + self.context_id = (0..self.spec.context_fields.len()) + .map(|_| { + next += 1; + next - 1 + }) + .collect(); + self.userop_id = (0..self.spec.pcodeops.len()) + .map(|_| { + next += 1; + next - 1 + }) + .collect(); + + // Subtables and operands are numbered the way the reference numbers + // them: in the order the source defines them. A subtable's id is + // taken when its first constructor is parsed, and that constructor's + // operands follow immediately. Getting this right is most of what + // makes our symbol table the same shape as Ghidra's. + const UNSET: u32 = u32::MAX; + self.table_id = vec![UNSET; self.spec.tables.len()]; + if !self.table_id.is_empty() { + self.table_id[0] = 0; + } + self.operand_id = vec![Vec::new(); self.spec.constructors.len()]; + for (i, c) in self.spec.constructors.iter().enumerate() { + let t = c.table.index(); + if !self.kept(t) { + continue; + } + if self.table_id[t] == UNSET { + self.table_id[t] = next; + next += 1; + } + self.operand_id[i] = c + .operands + .iter() + .map(|_| { + next += 1; + next - 1 + }) + .collect(); + } + // A table with no constructors still needs an id. + for t in 0..self.table_id.len() { + if self.table_id[t] == UNSET && self.kept(t) { + self.table_id[t] = next; + next += 1; + } + } + + self.ct_place = vec![(0, 0); self.spec.constructors.len()]; + for (t, table) in self.spec.tables.iter().enumerate() { + if !self.kept(t) { + continue; + } + for (k, cid) in table.constructors.iter().enumerate() { + self.ct_place[cid.index()] = (self.table_id[t], k as u64); + } + } + } + + /// The files that define a constructor, in first-use order, which is the + /// order the `sourcefiles` list has them. + fn collect_sources(&mut self) { + for c in &self.spec.constructors { + if !self.kept(c.table.index()) { + continue; + } + // The reference writes the file's own name, not the path it was + // found at, so a compiled language does not carry the build + // machine's directory layout. + let f = base_name(&c.location.file); + if !self.source_files.contains(&f) { + self.source_files.push(f); + } + } + if self.source_files.is_empty() { + self.source_files.push("".into()); + } + } + + fn source_index(&self, c: &sl::Constructor) -> u64 { + let name = base_name(&c.location.file); + self.source_files + .iter() + .position(|f| *f == name) + .unwrap_or(0) as u64 + } + + fn symbols(&mut self) -> (Vec, Vec) { + let mut scopes = vec![sla::Scope { id: 0, parent: 0 }]; + // The root table's body carries every constructor of `instruction`, + // so it is built first even though it is written in id order. The + // three built-in symbols follow it at fixed ids. + let mut syms: Vec = vec![ + self.table_symbol(0), + sla::Symbol { + id: 1, + name: Some("inst_start".into()), + scope: Some(0), + body: sla::SymbolBody::Start, + }, + sla::Symbol { + id: 2, + name: Some("inst_next".into()), + scope: Some(0), + body: sla::SymbolBody::End, + }, + sla::Symbol { + id: 3, + name: Some("inst_next2".into()), + scope: Some(0), + body: sla::SymbolBody::Next2, + }, + ]; + + for (i, v) in self.spec.varnodes.iter().enumerate() { + syms.push(sla::Symbol { + id: self.varnode_id[i], + name: Some(v.name.clone()), + scope: Some(0), + body: sla::SymbolBody::Varnode { + space: self.space_ix[v.space.index()], + offset: v.offset, + size: u64::from(v.size), + }, + }); + } + + for (i, f) in self.spec.fields.iter().enumerate() { + let field = Some(sla::FieldDef::Token(self.token_field(f))); + let body = match &f.attach { + sl::Attach::None => sla::SymbolBody::Value { field }, + sl::Attach::Variables(v) => sla::SymbolBody::VarnodeList { + field, + entries: v + .iter() + .map(|e| e.map(|id| self.varnode_id[id.index()])) + .collect(), + }, + sl::Attach::Names(n) => sla::SymbolBody::Name { + field, + names: n.clone(), + }, + sl::Attach::Values(v) => sla::SymbolBody::ValueMap { + field, + values: v.iter().map(|e| i128::from(e.unwrap_or(0))).collect(), + }, + }; + syms.push(sla::Symbol { + id: self.field_id[i], + name: Some(f.name.clone()), + scope: Some(0), + body, + }); + } + + for (i, cf) in self.spec.context_fields.iter().enumerate() { + // An `attach` over a context field replaces the context symbol + // rather than decorating it. **Proved**: after `attach variables + // [ cc ] [ ... ]` the reference compiler refuses `cc` as a context + // lvalue and in a `globalset` call, saying it is now a + // varnodelist_symbol. So the body written is the family body, with + // the context field inline where a token field would otherwise be. + let field = Some(sla::FieldDef::Context(context_field(cf))); + let body = match &cf.attach { + sl::Attach::None => sla::SymbolBody::Context { + varnode: self.varnode_id[cf.register.index()], + low: u64::from(cf.low), + high: u64::from(cf.high), + flow: !cf.noflow, + field: Some(context_field(cf)), + }, + sl::Attach::Variables(v) => sla::SymbolBody::VarnodeList { + field, + entries: v + .iter() + .map(|e| e.map(|id| self.varnode_id[id.index()])) + .collect(), + }, + sl::Attach::Names(n) => sla::SymbolBody::Name { + field, + names: n.clone(), + }, + sl::Attach::Values(v) => sla::SymbolBody::ValueMap { + field, + values: v.iter().map(|e| i128::from(e.unwrap_or(0))).collect(), + }, + }; + syms.push(sla::Symbol { + id: self.context_id[i], + name: Some(cf.name.clone()), + scope: Some(0), + body, + }); + } + + for (i, op) in self.spec.pcodeops.iter().enumerate() { + syms.push(sla::Symbol { + id: self.userop_id[i], + name: Some(op.name.clone()), + scope: Some(0), + body: sla::SymbolBody::UserOp { index: i as u64 }, + }); + } + + // Then the subtables and the operands, interleaved in source order, + // each subtable just before the operands of its first constructor. + let spec = self.spec; + let mut written = vec![false; spec.tables.len()]; + let mut pending: Vec = Vec::new(); + if !written.is_empty() { + written[0] = true; + } + for (i, c) in spec.constructors.iter().enumerate() { + let t = c.table.index(); + if !self.kept(t) { + continue; + } + if !written[t] { + written[t] = true; + syms.push(self.table_symbol(t)); + } + if c.operands.is_empty() { + continue; + } + let scope = scopes.len() as u32; + scopes.push(sla::Scope { + id: scope, + parent: 0, + }); + for (k, o) in c.operands.iter().enumerate() { + syms.push(self.operand_symbol(sl::ConstructorId(i as u32), c, k, o, scope)); + } + } + // A table with no constructors at all still needs its symbol written. + for (t, done) in written.iter().enumerate().skip(1) { + if !done && self.kept(t) { + pending.push(t); + } + } + for t in pending { + syms.push(self.table_symbol(t)); + } + + (scopes, syms) + } + + fn token_field(&self, f: &sl::Field) -> sla::TokenField { + let token = &self.spec.tokens[f.token.index()]; + let big = token.endian == sl::Endian::Big; + let (start_byte, end_byte) = if big { + ( + u64::from(token.size - 1 - f.high / 8), + u64::from(token.size - 1 - f.low / 8), + ) + } else { + (u64::from(f.low / 8), u64::from(f.high / 8)) + }; + sla::TokenField { + big_endian: big, + signed: f.signed, + start_bit: u64::from(f.low), + end_bit: u64::from(f.high), + start_byte, + end_byte, + // The bytes the field touches are assembled most significant + // first, so the field's own low bit sits `low % 8` up from the + // bottom of that number whichever way the token runs. + shift: u64::from(f.low % 8), + } + } + + fn operand_symbol( + &self, + cid: sl::ConstructorId, + c: &sl::Constructor, + index: usize, + o: &sl::Operand, + scope: u32, + ) -> sla::Symbol { + let (table, ct) = self.ct_place[cid.index()]; + let hand = hand_order(c); + // The operand's own record always names where it came from: which + // operand of which constructor of which table. + let mut expr = vec![sla::Expr::OperandValue { + index: hand[index], + table: u64::from(table), + ct, + }]; + // A family symbol is referenced by id; a plain field is written out + // inline, because it has no symbol of its own at this point. + let sub = match o.source { + sl::OperandSource::Table(t) => Some(self.table_id[t.index()]), + sl::OperandSource::Field(f) => { + if self.spec.field(f).attach.is_empty() { + expr.push(sla::Expr::TokenField(self.token_field(self.spec.field(f)))); + None + } else { + Some(self.field_id[f.index()]) + } + } + sl::OperandSource::Context(cf) => Some(self.context_id[cf.index()]), + sl::OperandSource::Varnode(v) => Some(self.varnode_id[v.index()]), + // A bit range over a register has no family symbol of its own in + // the compiled form, so it is written like a computed operand. + sl::OperandSource::BitRange(_) + | sl::OperandSource::Computed + | sl::OperandSource::Unbound => None, + }; + // An operand a disassembly action computes carries the computation in + // its own body, right after the `operand_value` that names it. Proved + // against the reference: `[ val = -imm; ]` gives `12(...)` then + // `50(12(...))` under the symbol named `val`. + for st in &c.disasm { + if let sl::DisasmStmt::Assign { + target: sl::DisasmTarget::Operand(k), + value, + } = st + { + if usize::from(*k) == index { + expr.push(self.disasm_expr(value, cid, &hand)); + } + } + } + sla::Symbol { + id: self.operand_id[cid.index()][index], + name: Some(o.name.clone()), + scope: Some(scope), + body: sla::SymbolBody::Operand { + index: hand[index], + offset: o.offset.delta as u64, + sub_symbol: sub, + base: o.offset.base.map_or(-1, |b| hand[b as usize] as i64), + min_length: self.operand_length(o), + flag: None, + expr, + }, + } + } + + /// How many instruction bytes the operand's own bits occupy: the width of + /// the token it is cut from, or the shortest match of the subtable it + /// names. A fixed register, a context field and a value the disassembly + /// action computes all take none, and the reference writes 0 for them. + fn operand_length(&self, o: &sl::Operand) -> u64 { + match o.source { + sl::OperandSource::Field(f) => { + u64::from(self.spec.tokens[self.spec.field(f).token.index()].size) + } + sl::OperandSource::Table(t) => self.spec.table(t).min_length as u64, + _ => 0, + } + } + + fn table_symbol(&mut self, t: usize) -> sla::Symbol { + // Copy the reference out so the constructor walk below borrows the + // specification rather than `self`, which it also mutates. + let spec = self.spec; + let table = &spec.tables[t]; + let mut constructors = Vec::new(); + let mut pairs: Vec<(u32, sla::Pattern)> = Vec::new(); + for (k, cid) in table.constructors.iter().enumerate() { + let c = spec.constructor(*cid); + constructors.push(self.constructor(*cid, c)); + for alt in &c.resolved.alternatives { + for p in self.patterns_for(c, alt) { + pairs.push((k as u32, p)); + } + } + if c.resolved.alternatives.is_empty() { + pairs.push(( + k as u32, + sla::Pattern { + has_instruction: true, + ..sla::Pattern::default() + }, + )); + } + } + let decision = sla::Decision { + number: pairs.len() as u64, + on_context: false, + start_bit: 0, + // Zero bits examined: a leaf that tries each pattern in order. + num_bits: 0, + children: Vec::new(), + pairs, + }; + sla::Symbol { + id: self.table_id[t], + name: Some(table.name.clone()), + scope: Some(0), + body: sla::SymbolBody::Subtable { + constructors, + decision: Some(decision), + }, + } + } + + /// The decision-tree patterns for one alternative. + /// + /// A constraint between two fields, `Rn=Rm`, is not a bit test, so the + /// front end leaves it as a residual. The compiled format has nowhere to + /// put it, and the reference compiler's answer is to enumerate: one + /// alternative per value the two fields share. + /// + /// **Proved**: `:same is op=0x30 & rd=rs` with `rd` and `rs` four bits + /// wide gives a decision whose `number` is 17, sixteen alternatives for + /// that constructor and one for its sibling. + /// + /// The enumeration needs the fields' byte offsets inside the instruction, + /// which is only known here when every token is the same width and this + /// alternative is one token long. Anything else is written as the bits + /// alone and counted in [`Report::approximate_patterns`], because a + /// pattern that matches too much is a wrong decode and saying so is the + /// only honest option. + fn patterns_for(&mut self, c: &sl::Constructor, alt: &sl::PatternAlt) -> Vec { + if alt.residual.is_empty() { + return vec![pattern(&alt.instr, &alt.context)]; + } + match self.expand(c, alt) { + Some(v) => v, + None => { + self.report.approximate_patterns += 1; + self.report.note( + "a pattern constraint that is not a bit test was written as the bits alone", + ); + vec![pattern(&alt.instr, &alt.context)] + } + } + } + + fn expand(&self, c: &sl::Constructor, alt: &sl::PatternAlt) -> Option> { + let width = self.uniform_token?; + if c.resolved.approximation.is_some() || alt.length != width as usize { + return None; + } + let mut variants = vec![alt.instr.clone()]; + for r in &alt.residual { + let (a, b) = field_equality(r)?; + let (fa, fb) = (self.spec.field(a), self.spec.field(b)); + let bits = fa.bits(); + if bits != fb.bits() || bits > 8 { + return None; + } + let count = 1u64 << bits; + // A cap, so a wide pair costs a refusal rather than a million + // alternatives. + if variants.len() as u64 * count > 512 { + return None; + } + let (ta, tb) = (self.spec.token_of(a), self.spec.token_of(b)); + let mut next = Vec::new(); + for v in &variants { + for x in 0..count { + let mut mv = v.clone(); + set_field(&mut mv, fa, ta, x); + set_field(&mut mv, fb, tb, x); + next.push(mv); + } + } + variants = next; + } + Some( + variants + .iter() + .map(|mv| pattern(mv, &alt.context)) + .collect(), + ) + } + + fn constructor(&mut self, cid: sl::ConstructorId, c: &sl::Constructor) -> sla::Constructor { + self.report.constructors += 1; + if c.body.is_some() { + self.report.without_semantics += 1; + } + if let Some(why) = c.resolved.approximation { + self.report.approximate_patterns += 1; + self.report + .note(format!("pattern reduced approximately: {why:?}")); + } + let hand = hand_order(c); + let print = print_pieces(&c.display, &hand); + let context_ops = self.context_ops(cid, c, &hand); + for op in &context_ops { + match op { + sla::ContextOp::Set { .. } => self.report.context_sets += 1, + sla::ContextOp::Commit { .. } => self.report.globalsets += 1, + } + } + self.report.computed_operands += c + .disasm + .iter() + .filter(|s| { + matches!( + s, + sl::DisasmStmt::Assign { + target: sl::DisasmTarget::Operand(_), + .. + } + ) + }) + .count(); + let ids = &self.operand_id[cid.index()]; + sla::Constructor { + parent: self.ct_place[cid.index()].0, + source: self.source_index(c), + line: u64::from(c.location.line), + length: c.resolved.min_length() as u64, + flowthru: flowthru(&print), + // The operand list is in resolution order, and every index in the + // file, including the print pieces, is a position in it. + operands: c.order.iter().map(|i| ids[*i as usize]).collect(), + print, + context_ops, + templates: Vec::new(), + } + } + + /// The disassembly action section as the file carries it: a context + /// assignment (element 32) or a `globalset` (79) each, in source order. + /// An assignment to an operand is not here; it lives in that operand's own + /// symbol body. + fn context_ops( + &self, + cid: sl::ConstructorId, + c: &sl::Constructor, + hand: &[u64], + ) -> Vec { + let mut out = Vec::new(); + for st in &c.disasm { + match st { + sl::DisasmStmt::Assign { + target: sl::DisasmTarget::Context(cf), + value, + } => { + let f = &self.spec.context_fields[cf.index()]; + let (word, shift, mask) = context_word(f); + out.push(sla::ContextOp::Set { + word, + shift, + mask, + value: vec![self.disasm_expr(value, cid, hand)], + }); + } + sl::DisasmStmt::Assign { .. } => {} + sl::DisasmStmt::GlobalSet { address, context } => { + let f = &self.spec.context_fields[context.index()]; + let (word, _, mask) = context_word(f); + out.push(sla::ContextOp::Commit { + symbol: self.globalset_symbol(address, cid), + word, + mask, + // `noflow` on the field is what clears it, proved by + // being the only edit that moves the bit. + flow: !f.noflow, + }); + } + } + } + out + } + + /// The address argument of a `globalset` is written as a symbol id, and + /// the only things that can stand there are the three built-in addresses + /// and an operand of the constructor. + fn globalset_symbol(&self, address: &sl::DisasmExpr, cid: sl::ConstructorId) -> u32 { + match address { + sl::DisasmExpr::Symbol(sl::SymbolRef::Builtin(b)) => match b { + sl::Builtin::InstNext => 2, + sl::Builtin::InstNext2 => 3, + _ => 1, + }, + sl::DisasmExpr::Symbol(sl::SymbolRef::Operand(i)) => self.operand_id[cid.index()] + .get(usize::from(*i)) + .copied() + .unwrap_or(1), + // `inst_start` is what the reference falls back to, and nothing in + // the corpus writes anything else here. + _ => 1, + } + } + + /// One disassembly-time expression as the element tree that carries it. + /// Nothing is folded: `4 - imm` stays a subtract of a literal from an + /// operand, in that order. + fn disasm_expr(&self, e: &sl::DisasmExpr, cid: sl::ConstructorId, hand: &[u64]) -> sla::Expr { + match e { + // A negative literal is unary minus applied to a positive one, + // which is what the reference writes and what our reader reads. + sl::DisasmExpr::Num(v) if *v < 0 => sla::Expr::Op { + op: sla::PatternOp::Minus, + operands: vec![sla::Expr::Constant(i128::from(v.unsigned_abs()))], + }, + sl::DisasmExpr::Num(v) => sla::Expr::Constant(i128::from(*v)), + sl::DisasmExpr::Symbol(s) => self.disasm_symbol(*s, cid, hand), + sl::DisasmExpr::Unary(op, a) => sla::Expr::Op { + op: match op { + sl::DisasmUnOp::Negate => sla::PatternOp::Minus, + sl::DisasmUnOp::Not => sla::PatternOp::Not, + }, + operands: vec![self.disasm_expr(a, cid, hand)], + }, + sl::DisasmExpr::Binary(op, a, b) => sla::Expr::Op { + op: match op { + sl::DisasmBinOp::Add => sla::PatternOp::Plus, + sl::DisasmBinOp::Sub => sla::PatternOp::Sub, + sl::DisasmBinOp::Mul => sla::PatternOp::Mult, + sl::DisasmBinOp::Div => sla::PatternOp::Div, + sl::DisasmBinOp::Shl => sla::PatternOp::LeftShift, + sl::DisasmBinOp::Shr => sla::PatternOp::RightShift, + sl::DisasmBinOp::And => sla::PatternOp::And, + sl::DisasmBinOp::Or => sla::PatternOp::Or, + sl::DisasmBinOp::Xor => sla::PatternOp::Xor, + }, + operands: vec![ + self.disasm_expr(a, cid, hand), + self.disasm_expr(b, cid, hand), + ], + }, + } + } + + fn disasm_symbol(&self, s: sl::SymbolRef, cid: sl::ConstructorId, hand: &[u64]) -> sla::Expr { + let (table, ct) = self.ct_place[cid.index()]; + match s { + sl::SymbolRef::Operand(i) => sla::Expr::OperandValue { + index: hand.get(usize::from(i)).copied().unwrap_or(u64::from(i)), + table: u64::from(table), + ct, + }, + sl::SymbolRef::Context(cf) => { + sla::Expr::Context(context_field(&self.spec.context_fields[cf.index()])) + } + sl::SymbolRef::Field(f) => sla::Expr::TokenField(self.token_field(self.spec.field(f))), + sl::SymbolRef::Builtin(sl::Builtin::InstStart) => sla::Expr::InstStart, + sl::SymbolRef::Builtin(sl::Builtin::InstNext) => sla::Expr::InstNext, + sl::SymbolRef::Builtin(sl::Builtin::InstNext2) => sla::Expr::InstNext2, + // A named register has no value at disassembly time. The reference + // compiler accepts `[ val = r3 + imm; ]` without a warning and + // writes a zero for the register, so this writes the same thing + // rather than inventing a leaf the format does not have. + _ => sla::Expr::Constant(0), + } + } +} + +/// A context field as element 29 carries it: bit numbers from the register's +/// most significant bit, and the byte range they fall in. +fn context_field(cf: &sl::ContextField) -> sla::ContextField { + sla::ContextField { + signed: cf.signed, + start_bit: u64::from(cf.low), + end_bit: u64::from(cf.high), + start_byte: u64::from(cf.low / 8), + end_byte: u64::from(cf.high / 8), + // Context bits are numbered from the most significant bit of the + // register, so the shift is measured from the far end of the byte the + // field ends in. + shift: u64::from(7 - cf.high % 8), + } +} + +/// Where a context field sits in the 32-bit words a context record addresses: +/// which word, the right shift that brings the field to bit zero of it, and +/// the bits it owns. +/// +/// **Proved** against the reference: `test=(0,0)` gives shift 31 and mask +/// `0x80000000`, `mode=(1,4)` gives 27 and `0x78000000`, `wide=(5,8)` gives 23 +/// and `0x07800000`, and a field at (32,63) of an eight-byte register gives +/// word 1. +fn context_word(cf: &sl::ContextField) -> (u64, u64, u64) { + let word = u64::from(cf.low / 32); + // A field that straddles a word boundary has no representation here. The + // published corpus has none, and clamping keeps the number written true to + // the bits it describes rather than wrapping into the next word. + let high = cf.high.max(cf.low).min(cf.low / 32 * 32 + 31); + let shift = u64::from(31 - high % 32); + let width = u64::from(high - cf.low + 1); + let mask = if width >= 64 { + u64::MAX + } else { + ((1u64 << width) - 1) << shift + }; + (word, shift, mask) +} + +/// For each operand, its position in the constructor's resolution order. +/// +/// The front end numbers operands in display order; the compiled file numbers +/// them in the order a decoder has to resolve them, which is different +/// wherever a disassembly action computes one operand from another. Every +/// index in the file is a position in that second order. +fn hand_order(c: &sl::Constructor) -> Vec { + let mut hand = vec![0u64; c.operands.len()]; + if c.order.len() != c.operands.len() { + // A specification the front end could not order is written in display + // order rather than refused; the round trip still holds. + for (i, h) in hand.iter_mut().enumerate() { + *h = i as u64; + } + return hand; + } + for (pos, i) in c.order.iter().enumerate() { + if let Some(h) = hand.get_mut(*i as usize) { + *h = pos as u64; + } + } + hand +} + +/// The display section as the file wants it: one piece per run of whitespace +/// and one per run of anything else, with operands in between. +fn print_pieces(d: &sl::Display, hand: &[u64]) -> Vec { + let mut out = Vec::new(); + if let Some(m) = &d.mnemonic { + push_literal(&mut out, m); + } + for p in &d.pieces { + match p { + sl::DisplayPiece::Literal(s) => push_literal(&mut out, s), + sl::DisplayPiece::Operand(i) => out.push(sla::PrintPiece::Operand( + hand.get(*i as usize).copied().unwrap_or(u64::from(*i)), + )), + } + } + out +} + +fn push_literal(out: &mut Vec, s: &str) { + let mut run = String::new(); + let mut space: Option = None; + for ch in s.chars() { + let is_space = ch.is_whitespace(); + if space != Some(is_space) && !run.is_empty() { + out.push(sla::PrintPiece::Literal(std::mem::take(&mut run))); + } + space = Some(is_space); + run.push(ch); + } + if !run.is_empty() { + out.push(sla::PrintPiece::Literal(run)); + } +} + +/// Where the mnemonic ends: the first whitespace piece, or the whole display +/// when there is none. Read off the corpus, where the two agree on every +/// constructor that has a whitespace piece at all. +fn flowthru(print: &[sla::PrintPiece]) -> i64 { + for (i, p) in print.iter().enumerate() { + if let sla::PrintPiece::Literal(s) = p { + if !s.is_empty() && s.chars().all(char::is_whitespace) { + return i as i64; + } + } + } + print.len() as i64 +} + +/// One alternative of a resolved pattern as the decision tree wants it. +fn pattern(instr: &sl::MaskValue, ctx: &sl::MaskValue) -> sla::Pattern { + let instruction = blocks(instr); + let context = blocks(ctx); + let has_context = !context.is_empty(); + sla::Pattern { + combined: has_context && !instruction.is_empty(), + has_context, + has_instruction: true, + context, + instruction, + } +} + +/// A mask and value over bytes, as the run of 32-bit big-endian words the +/// format uses. Leading and trailing bytes that constrain nothing are dropped, +/// because the block carries its own offset. +fn blocks(mv: &sl::MaskValue) -> Vec { + let first = match mv.mask.iter().position(|&b| b != 0) { + Some(i) => i, + None => return Vec::new(), + }; + let last = mv.mask.iter().rposition(|&b| b != 0).unwrap_or(first); + let mut words = Vec::new(); + let mut i = first; + while i <= last { + let mut mask = 0u64; + let mut value = 0u64; + for k in 0..4 { + let at = i + k; + let (m, v) = if at <= last { + (mv.mask[at], mv.value.get(at).copied().unwrap_or(0)) + } else { + (0, 0) + }; + mask |= u64::from(m) << (24 - 8 * k); + value |= u64::from(v) << (24 - 8 * k); + } + words.push(sla::PatternWord { mask, value }); + i += 4; + } + vec![sla::PatternBlock { + offset: first as u64, + bytes: (last - first + 1) as u64, + words, + }] +} + +/// The last path component, which is what a compiled file records. +fn base_name(path: &str) -> String { + path.rsplit(['/', '\\']).next().unwrap_or(path).to_owned() +} + +/// A residual of the form `field = field`, which is the only shape that can be +/// enumerated into bits. +fn field_equality(e: &sl::PatternExpr) -> Option<(sl::FieldId, sl::FieldId)> { + let sl::PatternExpr::Constraint { lhs, op, rhs } = e else { + return None; + }; + if *op != sl::ConstraintOp::Equal { + return None; + } + match (lhs, rhs) { + (sl::SymbolRef::Field(a), sl::DisasmExpr::Symbol(sl::SymbolRef::Field(b))) => { + Some((*a, *b)) + } + _ => None, + } +} + +/// Pin a token field to a value in a mask and value pair over instruction +/// bytes. The bit numbering is SLEIGH's own: bit zero of a token is the least +/// significant bit of its least significant byte, and a big-endian token +/// counts its bytes from the far end. +fn set_field(mv: &mut sl::MaskValue, f: &sl::Field, token: &sl::TokenDef, value: u64) { + let size = token.size as usize; + if mv.mask.len() < size { + mv.mask.resize(size, 0); + mv.value.resize(size, 0); + } + for (out, bit) in (f.low..=f.high).enumerate() { + let byte = match token.endian { + sl::Endian::Big => token.size - 1 - bit / 8, + sl::Endian::Little => bit / 8, + } as usize; + let shift = bit % 8; + mv.mask[byte] |= 1 << shift; + mv.value[byte] &= !(1 << shift); + if value >> out & 1 == 1 { + mv.value[byte] |= 1 << shift; + } + } +} diff --git a/crates/e5r-sla/tests/writer.rs b/crates/e5r-sla/tests/writer.rs new file mode 100644 index 0000000..d0358df --- /dev/null +++ b/crates/e5r-sla/tests/writer.rs @@ -0,0 +1,320 @@ +//! The writer measured against the only oracle there is for it: the files +//! Ghidra's own compiler produced. +//! +//! Reading a format and writing it are different claims. A reader can be +//! sloppy about which of two encodings a field used and still produce the +//! right model; a writer that is sloppy the same way produces a file nothing +//! else will read. So the gate here is byte equality of the payload: decode +//! every shipped `.sla`, encode the tree straight back, and require the same +//! bytes. Across the corpus that is 95 MB of exact agreement about tag forms, +//! chunk counts and value types, in the direction the reader cannot check. +//! +//! As elsewhere, a missing corpus reports and returns rather than failing, so +//! a fresh checkout still runs the fixtures. + +use std::path::{Path, PathBuf}; + +use e5r_sla::{Level, Sla, deflate, emit, encode, inflate}; + +fn fixtures() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data") +} + +fn find_sla(dir: &Path, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for e in entries.flatten() { + let p = e.path(); + if p.is_dir() { + find_sla(&p, out); + } else if p.extension().is_some_and(|x| x == "sla") { + out.push(p); + } + } +} + +fn corpus() -> Vec { + let mut out = Vec::new(); + find_sla(&fixtures(), &mut out); + let mut roots: Vec = Vec::new(); + if let Ok(v) = std::env::var("E5R_SLA_DIR") { + roots.extend(v.split(':').filter(|s| !s.is_empty()).map(PathBuf::from)); + } + if let Ok(home) = std::env::var("HOME") { + roots.push(PathBuf::from(&home).join(".local/share/ghidra-cli/ghidra")); + roots.push(PathBuf::from(&home).join("ghidra")); + } + roots.push(PathBuf::from("/opt/ghidra")); + for r in roots { + if r.is_dir() { + find_sla(&r, &mut out); + } + } + out.sort(); + out.dedup(); + out +} + +/// The headline claim about the encoder: it is the exact inverse of the +/// decoder over every file the reference compiler ever produced here. +#[test] +fn every_sla_payload_re_encodes_to_the_same_bytes() { + let files = corpus(); + assert!(!files.is_empty(), "fixtures should always be found"); + + let mut total = 0usize; + let mut matched = 0usize; + let mut bytes = 0usize; + let mut mismatches: Vec = Vec::new(); + + for path in &files { + let raw = std::fs::read(path).expect("readable"); + let payload = match inflate::inflate_zlib(&raw[4..], inflate::DEFAULT_LIMIT) { + Ok(p) => p, + Err(e) => { + mismatches.push(format!("{}: inflate: {e}", path.display())); + continue; + } + }; + let tree = match e5r_sla::decode::decode(&payload) { + Ok(t) => t, + Err(e) => { + mismatches.push(format!("{}: decode: {e}", path.display())); + continue; + } + }; + total += 1; + let again = encode::encode(&tree).expect("a tree that was read can be written"); + if again == payload { + matched += 1; + bytes += payload.len(); + } else { + // Name the first byte that differs: a wrong chunk count shows up + // at the first value that needs it, and the offset says which. + let at = again + .iter() + .zip(&payload) + .position(|(a, b)| a != b) + .unwrap_or(payload.len().min(again.len())); + mismatches.push(format!( + "{}: first difference at {at} (ours {} bytes, theirs {})", + path.display(), + again.len(), + payload.len() + )); + } + } + + println!("re-encoded {matched}/{total} payloads byte for byte, {bytes} bytes"); + assert!(mismatches.is_empty(), "{mismatches:#?}"); +} + +/// The container as a whole: our compressor, our decompressor, our tree. +#[test] +fn every_sla_survives_a_whole_container_round_trip() { + let files = corpus(); + let mut total = 0usize; + let mut ours = 0usize; + let mut theirs = 0usize; + for path in &files { + let Ok(sla) = Sla::open(path) else { continue }; + let written = sla.to_bytes(Level::Fixed).expect("writes"); + assert_eq!(&written[..4], &[b's', b'l', b'a', sla.format_version]); + let back = Sla::parse(&written).expect("reads back"); + assert_eq!(back.payload_len, sla.payload_len, "{}", path.display()); + assert_eq!( + back.program.symbols.len(), + sla.program.symbols.len(), + "{}", + path.display() + ); + assert!(back.check().is_empty(), "{}", path.display()); + total += 1; + ours += written.len(); + theirs += std::fs::metadata(path) + .map(|m| m.len() as usize) + .unwrap_or(0); + } + // Not a gate, a measurement: the fixed Huffman tables cost size against + // zlib's dynamic ones and the number belongs in the record. + println!( + "{total} files rewritten; {ours} bytes against the reference compiler's {theirs} ({:.2}x)", + ours as f64 / theirs.max(1) as f64 + ); +} + +/// Stored blocks are the control: if the Huffman path ever looks wrong, this +/// says whether the problem is the compressor or everything before it. +#[test] +fn stored_blocks_are_a_working_fallback() { + let files = corpus(); + for path in files.iter().take(8) { + let Ok(sla) = Sla::open(path) else { continue }; + let written = sla.to_bytes(Level::Stored).expect("writes"); + let back = Sla::parse(&written).expect("reads back"); + assert_eq!(back.payload_len, sla.payload_len); + } +} + +/// An external check on the compressor. Our inflater and our deflater sharing +/// a misreading of RFC 1951 would pass every test above, so the streams are +/// also handed to a zlib that is not ours. +#[test] +fn a_foreign_zlib_reads_what_we_write() { + let Ok(out) = std::process::Command::new("python3") + .arg("-c") + .arg("import sys,zlib; sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .spawn() + else { + println!("no python3 on this machine, skipping the foreign zlib check"); + return; + }; + drop(out); + + let mut checked = 0usize; + for path in corpus().iter().take(12) { + let Ok(sla) = Sla::open(path) else { continue }; + let payload = encode::encode(&sla.tree).expect("writes"); + for level in [Level::Fixed, Level::Stored] { + let z = deflate::deflate_zlib(&payload, level); + let got = python_inflate(&z).expect("python zlib accepts our stream"); + assert_eq!(got, payload, "{} at {level:?}", path.display()); + checked += 1; + } + } + println!("{checked} streams decompressed by python's zlib"); +} + +fn python_inflate(data: &[u8]) -> Option> { + use std::io::Write; + let mut child = std::process::Command::new("python3") + .arg("-c") + .arg("import sys,zlib; sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .spawn() + .ok()?; + child.stdin.take()?.write_all(data).ok()?; + let out = child.wait_with_output().ok()?; + out.status.success().then_some(out.stdout) +} + +/// The structured writer, against the same oracle. This is the stronger claim +/// of the two: the tree that comes out of [`emit`] was rebuilt from spaces, +/// symbols, constructors and templates, so byte equality here says the model +/// holds everything the file did, in the order the file had it, with the value +/// types the file used. +#[test] +fn every_sla_rebuilds_from_the_model() { + let files = corpus(); + let mut total = 0usize; + let mut exact = 0usize; + let mut bytes = 0usize; + let mut differed: Vec = Vec::new(); + let mut refused: Vec = Vec::new(); + + for path in &files { + let Ok(sla) = Sla::open(path) else { continue }; + total += 1; + let original = encode::encode(&sla.tree).expect("re-encodes"); + match emit::tree(&sla.program) { + Ok(t) => { + let got = encode::encode(&t).expect("encodes"); + if got == original { + exact += 1; + bytes += got.len(); + } else { + let at = got + .iter() + .zip(&original) + .position(|(a, b)| a != b) + .unwrap_or(original.len().min(got.len())); + differed.push(format!( + "{}: differs at {at} (ours {} theirs {})", + path.display(), + got.len(), + original.len() + )); + } + } + Err(e) => refused.push(format!("{}: {e}", path.display())), + } + } + + println!( + "rebuilt {exact}/{total} files byte for byte from the model, {bytes} bytes; \ + {} differed, {} refused", + differed.len(), + refused.len() + ); + for d in differed.iter().take(10) { + println!(" {d}"); + } + for r in refused.iter().take(10) { + println!(" {r}"); + } + // A ratchet: every file the model can rebuild must rebuild exactly. A file + // it refuses is a gap the coverage number already reports. + assert!(differed.is_empty(), "{differed:#?}"); +} + +/// Where a rebuild differs, said in terms of the tree rather than a byte +/// offset. Kept because "differs at byte 10797" is not a bug report and this +/// is what turns one into a bug report. +fn first_difference( + a: &e5r_sla::Node, + b: &e5r_sla::Node, + path: &mut Vec, +) -> Option { + if a.id != b.id { + return Some(format!("{}: element {} vs {}", path.join("/"), a.id, b.id)); + } + if a.attrs != b.attrs { + return Some(format!( + "E{} at {}: attrs {:?} vs {:?}", + a.id, + path.join("/"), + a.attrs, + b.attrs + )); + } + if a.children.len() != b.children.len() { + return Some(format!( + "E{} at {}: {} children vs {}; ids {:?} vs {:?}", + a.id, + path.join("/"), + a.children.len(), + b.children.len(), + a.children.iter().map(|c| c.id).collect::>(), + b.children.iter().map(|c| c.id).collect::>() + )); + } + for (i, (x, y)) in a.children.iter().zip(&b.children).enumerate() { + path.push(format!("E{}[{i}]", x.id)); + if let Some(d) = first_difference(x, y, path) { + return Some(d); + } + path.pop(); + } + None +} + +#[test] +#[ignore = "diagnostic: prints where a rebuild first differs"] +fn report_rebuild_differences() { + for path in corpus() { + let Ok(sla) = Sla::open(&path) else { continue }; + let Ok(t) = emit::tree(&sla.program) else { + continue; + }; + let mut trail = Vec::new(); + if let Some(d) = first_difference(&t, &sla.tree, &mut trail) { + println!("{}\n {d}", path.display()); + } + } +} diff --git a/crates/e5r-sleigh/Cargo.toml b/crates/e5r-sleigh/Cargo.toml new file mode 100644 index 0000000..bef6179 --- /dev/null +++ b/crates/e5r-sleigh/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "e5r-sleigh" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +description = "SLEIGH processor specification front end: .slaspec source to an in-memory model" + +# No dependencies on purpose. The front end is a lexer, a parser and a bit +# solver over text; anything it pulled in would be a liability at the same +# trust boundary as the loaders. +[dependencies] + +# The gates compare the decode engine against the hand-written decoders and +# against objdump, so the test binaries need them. A dev dependency, so the +# library itself still pulls in nothing and `e5r-arch` stays free to depend on +# this crate. +[dev-dependencies] +e5r-arch = { workspace = true } +e5r-core = { workspace = true } diff --git a/crates/e5r-sleigh/src/context.rs b/crates/e5r-sleigh/src/context.rs new file mode 100644 index 0000000..ffc98fa --- /dev/null +++ b/crates/e5r-sleigh/src/context.rs @@ -0,0 +1,324 @@ +//! The context register, and the database of changes a `globalset` publishes. +//! +//! A SLEIGH context register is a bit vector the decoder carries alongside the +//! instruction bytes. Two things write to it and they are not the same thing: +//! +//! * a disassembly action writes a field with `ctx = value;`, which changes how +//! the *rest of this instruction* parses and then evaporates; +//! * `globalset(addr, ctx)` publishes the field's current value at another +//! address, where it changes how *that* instruction parses. +//! +//! The second is why a single register image is not enough. ARM's `TMode` +//! selects Thumb for every instruction from a `bx` onwards, and AArch64's +//! specification uses the same mechanism, so the decoder needs a map from +//! address to the fields that were published there. That map is +//! [`ContextDb`]. +//! +//! Nothing here allocates per instruction: a decode that publishes nothing +//! touches the database not at all, and the common case of a linear sweep over +//! a section that never calls `globalset` costs one hash lookup per +//! instruction. + +use std::collections::HashMap; + +use crate::model::{ContextField, ContextFieldId, Spec}; + +/// The largest context register this will build an image for, in bytes. +/// +/// Ghidra's widest published context register is eight bytes. The ceiling is +/// here because the size is read from a specification, which is input, and it +/// is a fixed size so a [`Context`] is `Copy` and saving one before a +/// speculative match costs no allocation. A decode tries thousands of +/// constructors and each one may change the context, so this is the +/// difference between an allocation per constructor and none. +pub const MAX_CONTEXT_BYTES: usize = 32; + +/// A context register image: the raw bytes the pattern masks are compared +/// against. +/// +/// Bit numbering is the specification's own, most significant bit of byte zero +/// first, which is what [`ContextField::bit_position`] implements. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Context { + bytes: [u8; MAX_CONTEXT_BYTES], + len: u8, +} + +impl Default for Context { + fn default() -> Context { + Context { + bytes: [0; MAX_CONTEXT_BYTES], + len: 0, + } + } +} + +impl Context { + /// An all-zero image wide enough for `spec`'s context register. + pub fn for_spec(spec: &Spec) -> Context { + Context::zeroed(spec.context_bytes()) + } + + /// An image of a given width, clamped to [`MAX_CONTEXT_BYTES`]. + pub fn zeroed(bytes: usize) -> Context { + Context { + bytes: [0; MAX_CONTEXT_BYTES], + len: bytes.min(MAX_CONTEXT_BYTES) as u8, + } + } + + /// The raw bytes, for [`crate::model::MaskValue::matches`]. + pub fn bytes(&self) -> &[u8] { + &self.bytes[..self.len as usize] + } + + /// How wide the image is. + pub fn len(&self) -> usize { + self.len as usize + } + + /// Whether there is no context register at all. + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// The first eight bytes packed into one integer, byte zero most + /// significant, so a whole context test is one compare. Bytes past the + /// image read as zero, which is what a mask that reaches past it should + /// compare against. + pub fn word(&self) -> u64 { + let mut out = 0u64; + for i in 0..8 { + out |= (self.bytes.get(i).copied().unwrap_or(0) as u64) << (56 - i * 8); + } + out + } + + /// Read one field. `None` when the field runs past the image, which means + /// the specification declared a context register narrower than its own + /// fields. + pub fn get(&self, field: &ContextField) -> Option { + field.extract(self.bytes()) + } + + /// Write one field, keeping the bits around it. Returns false when the + /// field does not fit in the image. + pub fn set(&mut self, field: &ContextField, value: u64) -> bool { + if field.high < field.low { + return false; + } + // The range runs from `low`, the most significant bit, down to `high`, + // so the output bit for `low` is the field's top bit. + for (out, bit) in (field.low..=field.high).rev().enumerate() { + let (byte, offset) = ContextField::bit_position(bit); + if byte >= self.len as usize { + return false; + } + let slot = &mut self.bytes[byte]; + let one = value >> out & 1 == 1; + if one { + *slot |= 1 << offset; + } else { + *slot &= !(1u8 << offset); + } + } + true + } +} + +/// One field's value, published at an address. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Commit { + /// Which field. + pub field: ContextFieldId, + /// Its value there. + pub value: u64, + /// Whether the specification marked the field `noflow`, so the value + /// applies at exactly this address and does not follow the flow onwards. + /// The database is keyed by address either way; the flag is kept because a + /// caller that propagates context along a control flow graph needs it and + /// nothing else records it. + pub noflow: bool, +} + +/// Context values published by `globalset`, keyed by the address they apply +/// at. +/// +/// Bounded: a sweep over a hostile binary could otherwise publish one entry per +/// byte. When the ceiling is reached new addresses are refused and +/// [`ContextDb::overflowed`] says so, rather than the map growing without +/// limit or entries being silently dropped. +#[derive(Debug, Clone)] +pub struct ContextDb { + at: HashMap>, + limit: usize, + overflowed: bool, +} + +impl Default for ContextDb { + fn default() -> ContextDb { + ContextDb::with_limit(1 << 20) + } +} + +impl ContextDb { + /// An empty database holding at most `limit` addresses. + pub fn with_limit(limit: usize) -> ContextDb { + ContextDb { + at: HashMap::new(), + limit, + overflowed: false, + } + } + + /// Whether the ceiling was reached and something was refused. + pub fn overflowed(&self) -> bool { + self.overflowed + } + + /// How many addresses carry a published value. + pub fn len(&self) -> usize { + self.at.len() + } + + /// Whether nothing has been published. + pub fn is_empty(&self) -> bool { + self.at.is_empty() + } + + /// Publish `commit` at `addr`. A second commit of the same field at the + /// same address replaces the first, which is what re-decoding the same + /// instruction must do if decoding is to be idempotent. + pub fn commit(&mut self, addr: u64, commit: Commit) { + if let Some(slot) = self.at.get_mut(&addr) { + if let Some(existing) = slot.iter_mut().find(|c| c.field == commit.field) { + *existing = commit; + // Bounded per address as well: a constructor tree can only hold so + // many distinct fields, and this stops a pathological + // specification growing one list for ever. + } else if slot.len() < 64 { + slot.push(commit); + } else { + self.overflowed = true; + } + return; + } + if self.at.len() >= self.limit { + self.overflowed = true; + return; + } + self.at.insert(addr, vec![commit]); + } + + /// What was published at `addr`. + pub fn at(&self, addr: u64) -> &[Commit] { + self.at.get(&addr).map_or(&[], |v| v.as_slice()) + } + + /// Fold everything published at `addr` into `image`. + pub fn apply(&self, spec: &Spec, addr: u64, image: &mut Context) { + for c in self.at(addr) { + if let Some(f) = spec.context_fields.get(c.field.index()) { + image.set(f, c.value); + } + } + } + + /// Forget everything. + pub fn clear(&mut self) { + self.at.clear(); + self.overflowed = false; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{Attach, NumberBase, VarnodeId}; + + fn field(low: u32, high: u32) -> ContextField { + ContextField { + name: "f".into(), + register: VarnodeId(0), + low, + high, + signed: false, + base: NumberBase::Hex, + noflow: false, + attach: Attach::None, + } + } + + #[test] + fn a_field_round_trips_through_the_image() { + let mut c = Context::zeroed(4); + let f = field(4, 7); + assert!(c.set(&f, 0xd)); + assert_eq!(c.get(&f), Some(0xd)); + assert_eq!(c.bytes(), &[0x0d, 0, 0, 0]); + } + + #[test] + fn writing_a_field_leaves_its_neighbours_alone() { + let mut c = Context::zeroed(4); + let low = field(0, 3); + let high = field(4, 7); + assert!(c.set(&low, 0xf)); + assert!(c.set(&high, 0x5)); + assert_eq!(c.get(&low), Some(0xf)); + assert_eq!(c.get(&high), Some(0x5)); + // And clearing one really clears it, rather than only ever setting + // bits. + assert!(c.set(&low, 0)); + assert_eq!(c.get(&low), Some(0)); + assert_eq!(c.get(&high), Some(0x5)); + } + + #[test] + fn a_field_past_the_end_of_the_image_fails_rather_than_panicking() { + let mut c = Context::zeroed(1); + let f = field(60, 63); + assert!(!c.set(&f, 1)); + assert_eq!(c.get(&f), None); + } + + #[test] + fn the_database_replaces_a_field_at_the_same_address() { + let mut db = ContextDb::default(); + db.commit( + 0x1000, + Commit { + field: ContextFieldId(0), + value: 1, + noflow: false, + }, + ); + db.commit( + 0x1000, + Commit { + field: ContextFieldId(0), + value: 0, + noflow: false, + }, + ); + assert_eq!(db.at(0x1000).len(), 1); + assert_eq!(db.at(0x1000)[0].value, 0); + } + + #[test] + fn the_database_refuses_to_grow_past_its_ceiling() { + let mut db = ContextDb::with_limit(2); + for addr in 0..8 { + db.commit( + addr, + Commit { + field: ContextFieldId(0), + value: 1, + noflow: false, + }, + ); + } + assert_eq!(db.len(), 2); + assert!(db.overflowed()); + } +} diff --git a/crates/e5r-sleigh/src/decode.rs b/crates/e5r-sleigh/src/decode.rs new file mode 100644 index 0000000..779bec5 --- /dev/null +++ b/crates/e5r-sleigh/src/decode.rs @@ -0,0 +1,1351 @@ +//! The decode walk: bytes and a context at an address to a constructor tree. +//! +//! This is the half of a SLEIGH runtime that turns [`crate::model::Spec`] into +//! instructions. The walk is the one [`crate::model`] describes: +//! +//! 1. Start at [`Spec::root`] and try each constructor in the table's own +//! order, because an earlier constructor wins an overlap. +//! 2. Reject on the byte masks first. That is +//! [`crate::model::ResolvedPattern::may_match`] and it is the fast path. +//! 3. Walk [`Constructor::order`], not [`Constructor::operands`]: operands are +//! numbered in display order and arrive in stream order, and only the +//! second resolves an [`Offset`] whose base is another operand. +//! 4. Re-evaluate [`Constructor::pattern`] whenever the masks are only a +//! filter, which [`crate::model::ResolvedPattern::approximation`] and a +//! non-empty [`crate::model::PatternAlt::residual`] both say. +//! 5. Run the disassembly actions, publish any `globalset`, render, and hand +//! back the tree. +//! +//! # Context +//! +//! A constructor's `[ ctx = value; ]` changes how the rest of the *same* +//! instruction parses, so it runs during the descent, before the subtables +//! below it are matched. That is the whole mechanism ARM uses to reach most of +//! its instruction set: two constructors at the root set `ARMcond` and +//! `ARMcondCk` and then build `instruction` again, and without executing the +//! assignment the second pass matches nothing. `globalset` is different: it +//! applies to a *future* address, so it goes in [`ContextDb`] rather than into +//! the image being carried. +//! +//! # Trust +//! +//! A specification is input, and so are the bytes. Every recursion is depth +//! bounded, every walk is step bounded, every slice is checked, and the +//! rendered text has a ceiling. A decode either produces a tree or returns a +//! typed error; it does not panic and it does not run away. + +use std::fmt; + +use crate::context::{Commit, Context, ContextDb}; +use crate::index::Index; +use crate::model::{ + Attach, Builtin, ConstraintOp, Constructor, ConstructorId, ContextFieldId, DisasmBinOp, + DisasmExpr, DisasmStmt, DisasmTarget, DisasmUnOp, DisplayPiece, FieldId, NumberBase, + OperandSource, PatternAlt, PatternExpr, Spec, Stmt, SymbolRef, TableId, VarnodeId, +}; + +/// What stopped a decode. +/// +/// Every one of these is a bound being reached or an input being short, never +/// an internal invariant, because a decoder fed a hostile specification and +/// hostile bytes has to be able to say which. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeError { + /// Nothing in the root table matched the bytes. + NoMatch, + /// The specification has no `instruction` table at all. + NoRootTable, + /// The constructor tree nested deeper than [`DecodeLimits::max_depth`]. + TooDeep, + /// More constructors were tried than [`DecodeLimits::max_steps`] allows. + TooManySteps, + /// The tree grew past [`DecodeLimits::max_nodes`]. + TooManyNodes, + /// The match ran past [`DecodeLimits::max_bytes`]. + TooLong, +} + +impl fmt::Display for DecodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + DecodeError::NoMatch => "no constructor matched", + DecodeError::NoRootTable => "the specification has no instruction table", + DecodeError::TooDeep => "the constructor tree nested too deeply", + DecodeError::TooManySteps => "too many constructors were tried", + DecodeError::TooManyNodes => "the constructor tree grew too large", + DecodeError::TooLong => "the instruction ran past the length ceiling", + }; + f.write_str(s) + } +} + +impl std::error::Error for DecodeError {} + +/// The ceilings a decode runs under. +/// +/// All of them are far above what the published corpus needs: the deepest +/// constructor tree Ghidra ships is well under twenty, and the longest +/// instruction any of its specifications encodes is x86's fifteen bytes plus +/// the slack a specification is allowed. They exist so a corrupted +/// specification costs an error rather than a stack overflow. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecodeLimits { + /// Deepest subtable nesting. + pub max_depth: usize, + /// Most constructors in one instruction's tree. + pub max_nodes: usize, + /// Most constructors tried, matched or not, for one instruction. + pub max_steps: usize, + /// Longest instruction in bytes. + pub max_bytes: usize, + /// Longest rendered text in bytes. + pub max_text: usize, +} + +impl Default for DecodeLimits { + fn default() -> DecodeLimits { + DecodeLimits { + max_depth: 64, + max_nodes: 256, + max_steps: 200_000, + max_bytes: 64, + max_text: 4096, + } + } +} + +/// What one operand of a matched constructor resolved to. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Value { + /// An integer: a token field, a context field, or a value a disassembly + /// action computed. Already through `attach values` where there was one. + Int(i64), + /// A register, either named in the pattern or selected by + /// `attach variables`. + Var(VarnodeId), + /// A display string selected by `attach names`. + Name(String), + /// A subtable, at this index into [`Decoded::nodes`]. + Sub(u32), + /// Nothing in the constructor binds it. The specification's own mistake, + /// kept rather than guessed at. + Unresolved, +} + +/// One operand of a matched constructor. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedOperand { + /// Byte offset from the instruction's own address where its bits begin. + pub start: usize, + /// Byte offset just past its match. Equal to `start` for anything that + /// consumes no instruction bytes, which is what [`Offset::resolve`] wants + /// from the operand it is measured against. + pub end: usize, + /// What it resolved to. + pub value: Value, + /// The integer the bits held before any attachment, which is what a + /// disassembly action and a pattern constraint see. + pub raw: i64, +} + +impl Default for ResolvedOperand { + fn default() -> ResolvedOperand { + ResolvedOperand { + start: 0, + end: 0, + value: Value::Unresolved, + raw: 0, + } + } +} + +/// One matched constructor. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Node { + /// Which constructor matched. + pub constructor: ConstructorId, + /// Byte offset from the instruction's address where it begins. + pub start: usize, + /// Byte offset just past everything it and its subtables matched. + pub end: usize, + /// Its operands, indexed the way [`Constructor::operands`] is. + pub operands: Vec, +} + +/// A decoded instruction: the constructor tree, its length, and the context it +/// was decoded under. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Decoded { + /// Where it starts. + pub addr: u64, + /// How many bytes it occupies. + pub len: usize, + /// The tree. Index zero is the root table's constructor. + pub nodes: Vec, + /// The context image the match ran under, after any `[ ctx = ... ]` the + /// tree executed. + pub context: Context, + /// Values `globalset` published, and where. + pub commits: Vec<(u64, Commit)>, +} + +impl Decoded { + /// The root constructor. + pub fn root(&self) -> &Node { + &self.nodes[0] + } + + /// One past the last byte. + pub fn end(&self) -> u64 { + self.addr.wrapping_add(self.len as u64) + } + + /// The mnemonic, when the root constructor named one. A constructor whose + /// display begins with `^` has none. + pub fn mnemonic<'a>(&self, spec: &'a Spec) -> Option<&'a str> { + spec.constructor(self.nodes[0].constructor) + .display + .mnemonic + .as_deref() + } + + /// Render the whole instruction the way the specification's display + /// sections say. + pub fn text(&self, spec: &Spec) -> String { + self.text_with(spec, &DecodeLimits::default()) + } + + /// The same, under an explicit text ceiling. + pub fn text_with(&self, spec: &Spec, limits: &DecodeLimits) -> String { + let mut out = String::new(); + let c = spec.constructor(self.nodes[0].constructor); + if let Some(m) = &c.display.mnemonic { + out.push_str(m); + } + self.render_into(spec, 0, &mut out, limits.max_text, 0); + out + } + + /// Render one node of the tree, without its mnemonic. + pub fn render(&self, spec: &Spec, node: u32) -> String { + let mut out = String::new(); + if let Some(m) = &spec + .constructor(self.nodes[node as usize].constructor) + .display + .mnemonic + { + out.push_str(m); + } + self.render_into(spec, node, &mut out, DecodeLimits::default().max_text, 0); + out + } + + fn render_into(&self, spec: &Spec, node: u32, out: &mut String, cap: usize, depth: usize) { + if depth > 64 || out.len() >= cap { + return; + } + let Some(n) = self.nodes.get(node as usize) else { + return; + }; + let c = spec.constructor(n.constructor); + for piece in &c.display.pieces { + if out.len() >= cap { + return; + } + match piece { + DisplayPiece::Literal(s) => out.push_str(s), + DisplayPiece::Operand(i) => { + let Some(op) = n.operands.get(*i as usize) else { + continue; + }; + match &op.value { + Value::Sub(child) => { + if let Some(m) = &spec + .constructor(self.nodes[*child as usize].constructor) + .display + .mnemonic + { + out.push_str(m); + } + self.render_into(spec, *child, out, cap, depth + 1); + } + Value::Var(v) => out.push_str(&spec.varnode(*v).name), + Value::Name(s) => out.push_str(s), + Value::Int(v) => { + let base = operand_base(spec, c, *i as usize); + push_int(out, *v, base); + } + Value::Unresolved => out.push_str(""), + } + } + } + } + } + + /// Whether any constructor in the tree declined to model its semantics. + pub fn is_unimplemented(&self, spec: &Spec) -> bool { + self.nodes + .iter() + .any(|n| spec.constructor(n.constructor).is_unimplemented()) + } +} + +/// How a field's integer prints, which is the field's own `hex`/`dec`. +fn operand_base(spec: &Spec, c: &Constructor, operand: usize) -> NumberBase { + match c.operands.get(operand).map(|o| o.source) { + Some(OperandSource::Field(id)) => spec.field(id).base, + Some(OperandSource::Context(id)) => spec.context_field(id).base, + _ => NumberBase::Hex, + } +} + +/// Print an integer the way a SLEIGH display does: hex with an `0x` prefix and +/// the sign outside it, or plain decimal. +fn push_int(out: &mut String, v: i64, base: NumberBase) { + match base { + NumberBase::Dec => { + let _ = fmt::Write::write_fmt(out, format_args!("{v}")); + } + NumberBase::Hex if v < 0 => { + let _ = fmt::Write::write_fmt(out, format_args!("-0x{:x}", v.unsigned_abs())); + } + NumberBase::Hex => { + let _ = fmt::Write::write_fmt(out, format_args!("0x{v:x}")); + } + } +} + +/// A decode engine over one specification. +/// +/// Holds the context database, so a linear sweep sees the context a `globalset` +/// published earlier in the sweep. Decoding the same address twice gives the +/// same answer: a commit replaces rather than accumulates. +#[derive(Debug, Clone)] +pub struct Decoder<'a> { + spec: &'a Spec, + index: Index, + limits: DecodeLimits, + base: Context, + db: ContextDb, +} + +impl<'a> Decoder<'a> { + /// A decoder over `spec`, with an all-zero starting context. + pub fn new(spec: &'a Spec) -> Decoder<'a> { + Decoder { + base: Context::for_spec(spec), + index: Index::build(spec), + spec, + limits: DecodeLimits::default(), + db: ContextDb::default(), + } + } + + /// A decoder over `spec` reusing an index built earlier. Building the + /// index walks every constructor, so a caller decoding with several + /// decoders over one specification should build it once. + pub fn with_index(spec: &'a Spec, index: Index) -> Decoder<'a> { + Decoder { + base: Context::for_spec(spec), + index, + spec, + limits: DecodeLimits::default(), + db: ContextDb::default(), + } + } + + /// The constructor order and prefilter this decoder uses. + pub fn index(&self) -> &Index { + &self.index + } + + /// Replace the ceilings. + pub fn with_limits(mut self, limits: DecodeLimits) -> Decoder<'a> { + self.limits = limits; + self + } + + /// The specification being decoded against. + pub fn spec(&self) -> &'a Spec { + self.spec + } + + /// The context every decode starts from, before anything the database + /// published at the address. This is where a `.pspec` default, or a + /// caller's knowledge that a region is Thumb, goes. + pub fn base_context(&mut self) -> &mut Context { + &mut self.base + } + + /// Set one context field in the starting context, by name. Returns false + /// when the specification has no such field, or the field does not fit. + pub fn set_context(&mut self, name: &str, value: u64) -> bool { + let Some(id) = self.context_field_id(name) else { + return false; + }; + let field = self.spec.context_field(id); + self.base.set(field, value) + } + + /// A context field's index, by name. + pub fn context_field_id(&self, name: &str) -> Option { + match self.spec.lookup(name) { + Some(crate::model::Symbol::Context(id)) => Some(id), + _ => None, + } + } + + /// The database of published context values. + pub fn context_db(&self) -> &ContextDb { + &self.db + } + + /// The same, mutably, so a caller can seed it or clear it between runs. + pub fn context_db_mut(&mut self) -> &mut ContextDb { + &mut self.db + } + + /// Decode one instruction at `addr` from `bytes`, which start at `addr`. + /// + /// Any `globalset` the instruction performs is published into the context + /// database before this returns, so a caller sweeping forwards sees it. + pub fn decode(&mut self, bytes: &[u8], addr: u64) -> Result { + let decoded = self.decode_at(bytes, addr)?; + for (at, commit) in &decoded.commits { + self.db.commit(*at, *commit); + } + Ok(decoded) + } + + /// Decode without publishing, for a caller that wants to decide whether to + /// believe the result first. + pub fn decode_at(&self, bytes: &[u8], addr: u64) -> Result { + if self.spec.tables.is_empty() { + return Err(DecodeError::NoRootTable); + } + let mut ctx = self.base; + self.db.apply(self.spec, addr, &mut ctx); + + let cap = bytes.len().min(self.limits.max_bytes); + let mut walk = Walk { + spec: self.spec, + index: &self.index, + bytes: &bytes[..cap], + addr, + limits: &self.limits, + ctx, + nodes: Vec::new(), + steps: 0, + commits: Vec::new(), + }; + let root = walk + .table(self.spec.root(), 0, 0)? + .ok_or(DecodeError::NoMatch)?; + debug_assert_eq!(root, 0, "the root constructor is always node zero"); + + let len = walk.nodes[0].end; + if len > self.limits.max_bytes { + return Err(DecodeError::TooLong); + } + // Second pass: `inst_next` is only known now, and the disassembly + // actions that compute operand values and publish context are written + // against it. Children first, so a parent reads values its subtables + // have already computed. + walk.actions(0, len, 0); + + Ok(Decoded { + addr, + len, + nodes: walk.nodes, + context: walk.ctx, + commits: walk.commits, + }) + } + + /// Decode and render in one step, which is what a disassembly listing + /// wants. + pub fn disassemble( + &mut self, + bytes: &[u8], + addr: u64, + ) -> Result<(Decoded, String), DecodeError> { + let d = self.decode(bytes, addr)?; + let text = d.text_with(self.spec, &self.limits); + Ok((d, text)) + } +} + +/// What resolving one operand did. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Resolved { + /// It has a place and a value. + Yes, + /// Its place is measured from a subtable not yet matched, so it waits for + /// the pass after the disassembly actions have run. + Deferred, + /// The encoding is not this constructor's after all. An `attach variables` + /// hole and a slice too short both land here. + No, +} + +/// The state of one instruction's walk. +struct Walk<'a> { + spec: &'a Spec, + index: &'a Index, + bytes: &'a [u8], + addr: u64, + limits: &'a DecodeLimits, + ctx: Context, + nodes: Vec, + steps: usize, + commits: Vec<(u64, Commit)>, +} + +impl Walk<'_> { + /// Try every constructor of `table`, in the specification's order, at byte + /// offset `at`. + fn table( + &mut self, + table: TableId, + at: usize, + depth: usize, + ) -> Result, DecodeError> { + if depth > self.limits.max_depth { + return Err(DecodeError::TooDeep); + } + let stream = self.bytes.get(at..).unwrap_or(&[]); + let ctx = self.ctx.word(); + // Most specific first, which is the order the containment rule in the + // SLEIGH manual requires; see `crate::index`. + let candidates = self.index.candidates(table, stream); + for i in 0..candidates.len() { + let cid = self.index.candidates(table, stream)[i]; + if !self.index.may_start(cid, stream, ctx) { + continue; + } + if let Some(node) = self.constructor(cid, at, depth)? { + return Ok(Some(node)); + } + } + Ok(None) + } + + fn constructor( + &mut self, + cid: ConstructorId, + at: usize, + depth: usize, + ) -> Result, DecodeError> { + self.steps += 1; + if self.steps > self.limits.max_steps { + return Err(DecodeError::TooManySteps); + } + let c = self.spec.constructor(cid); + let stream = self.bytes.get(at..).unwrap_or(&[]); + + for alt in &c.resolved.alternatives { + if !alt.instr.matches(stream) || !alt.context.matches(self.ctx.bytes()) { + continue; + } + // Everything past here can fail, and a failure must leave no trace: + // a half-built subtree and a half-applied context change would both + // corrupt the next alternative's attempt. + let mark = self.nodes.len(); + let saved = self.ctx; + match self.try_alt(cid, c, alt, at, depth)? { + Some(node) => return Ok(Some(node)), + None => { + self.nodes.truncate(mark); + self.ctx = saved; + } + } + } + Ok(None) + } + + fn try_alt( + &mut self, + cid: ConstructorId, + c: &Constructor, + alt: &PatternAlt, + at: usize, + depth: usize, + ) -> Result, DecodeError> { + if self.nodes.len() >= self.limits.max_nodes { + return Err(DecodeError::TooManyNodes); + } + let me = self.nodes.len() as u32; + self.nodes.push(Node { + constructor: cid, + start: at, + end: at.saturating_add(alt.length), + operands: vec![ResolvedOperand::default(); c.operands.len()], + }); + for op in &mut self.nodes[me as usize].operands { + op.start = at; + op.end = at; + } + + // Operands not in `order` are ones a malformed specification left out + // of its own pattern. They still need a place rather than being left + // at zero, so the walk is `order` and then whatever it missed. + let mut extra: Vec; + let sequence: &[u16] = if c.order.len() == c.operands.len() { + &c.order + } else { + extra = c.order.clone(); + for oi in 0..c.operands.len() as u16 { + if !extra.contains(&oi) { + extra.push(oi); + } + } + &extra + }; + + // A bitmask rather than a vector: no constructor in the published + // corpus has anywhere near sixty-four operands, and the fallback keeps + // one that does correct rather than fast. + let mut done: u64 = 0; + // Two passes, because a `[ ctx = ... ]` is written against the token + // fields of the constructor it sits in and has to run before the + // subtables below it are matched. AArch64 is the case that forces it: + // + // :^instruction is ImmS_ImmR_TestSet=0 & ImmR & ImmS & instruction + // [ ImmS_LT_ImmR = ...ImmR...ImmS...; ImmS_ImmR_TestSet=1; ] {} + // + // The action reads two token fields and writes five context fields + // that the second pass over `instruction` then matches against, which + // is how `lsl` is preferred over `ubfm`. Running the action before the + // fields are read computes it from nothing; running it after the + // subtable is matched is too late. + for &oi in sequence { + let table = matches!( + c.operands.get(oi as usize).map(|o| o.source), + Some(OperandSource::Table(_)) + ); + if table { + continue; + } + match self.operand(c, alt, me, oi, at, depth, true)? { + Resolved::Yes if oi < 64 => done |= 1 << oi, + Resolved::Yes => {} + // Its offset is measured from a subtable that has not been + // matched yet, so it waits for the second pass. + Resolved::Deferred => {} + Resolved::No => return Ok(None), + } + } + + self.apply_context_assigns(c, me); + + for &oi in sequence { + if oi < 64 && done >> oi & 1 == 1 { + continue; + } + if self.operand(c, alt, me, oi, at, depth, false)? != Resolved::Yes { + return Ok(None); + } + } + + if c.resolved.approximation.is_some() || !alt.residual.is_empty() { + let (ok, _) = self.eval_pattern(&c.pattern, me, at, 0, 0); + if !ok { + return Ok(None); + } + } + Ok(Some(me)) + } + + /// Resolve one operand. + /// + /// `first_pass` asks for the operands a disassembly action can be written + /// against: anything whose place is already known. In that pass an operand + /// measured from a subtable that has not been matched yet is deferred + /// rather than failed. + #[allow(clippy::too_many_arguments)] + fn operand( + &mut self, + c: &Constructor, + alt: &PatternAlt, + me: u32, + oi: u16, + at: usize, + depth: usize, + first_pass: bool, + ) -> Result { + let Some(spec_op) = c.operands.get(oi as usize) else { + return Ok(Resolved::Yes); + }; + let placement = alt + .offsets + .get(oi as usize) + .copied() + .unwrap_or(spec_op.offset); + + // Bases are measured from the constructor's own start, and + // `Constructor::order` guarantees the base was resolved first. + let ops = &self.nodes[me as usize].operands; + let known = |i: u16| -> Option { + let op = ops.get(i as usize)?; + (!matches!(op.value, Value::Unresolved) || op.end != op.start).then(|| op.end - at) + }; + let Some(rel) = placement.resolve(|i| { + if first_pass { + known(i) + } else { + ops.get(i as usize).map(|o| o.end - at) + } + }) else { + return Ok(if first_pass { + Resolved::Deferred + } else { + Resolved::No + }); + }; + let start = at.saturating_add(rel); + if start > self.limits.max_bytes { + return Err(DecodeError::TooLong); + } + + let (value, raw, end) = match spec_op.source { + OperandSource::Field(fid) => { + let field = self.spec.field(fid); + let token = self.spec.token_of(fid); + let Some(slice) = self.bytes.get(start..) else { + return Ok(Resolved::No); + }; + let Some(raw) = field.extract(token, slice) else { + return Ok(Resolved::No); + }; + let signed = if field.signed { + crate::model::sign_extend(raw, field.bits()) + } else { + raw as i64 + }; + let Some(v) = attached(&field.attach, raw, signed) else { + return Ok(Resolved::No); + }; + (v, signed, start + token.size as usize) + } + OperandSource::Context(cid) => { + let field = self.spec.context_field(cid); + let Some(raw) = self.ctx.get(field) else { + return Ok(Resolved::No); + }; + let signed = if field.signed { + crate::model::sign_extend(raw, field.bits()) + } else { + raw as i64 + }; + let Some(v) = attached(&field.attach, raw, signed) else { + return Ok(Resolved::No); + }; + (v, signed, start) + } + OperandSource::Table(tid) => { + let Some(child) = self.table(tid, start, depth + 1)? else { + return Ok(Resolved::No); + }; + let end = self.nodes[child as usize].end; + (Value::Sub(child), 0, end) + } + OperandSource::Varnode(vid) => (Value::Var(vid), 0, start), + // A `define bitrange` names bits inside a register. It has no + // encoding of its own, so there is nothing to read here; the + // semantic body is where it means something. + OperandSource::BitRange(_) => (Value::Unresolved, 0, start), + // Filled in by the disassembly action pass, which needs a length + // this walk has not established yet. + OperandSource::Computed => (Value::Int(0), 0, start), + OperandSource::Unbound => (Value::Unresolved, 0, start), + }; + + let node = &mut self.nodes[me as usize]; + node.operands[oi as usize] = ResolvedOperand { + start, + end, + value, + raw, + }; + if end > node.end { + node.end = end; + } + Ok(Resolved::Yes) + } + + /// Execute the `[ ctx = ... ]` statements of a constructor, which are the + /// only disassembly actions that have to run during the descent. + fn apply_context_assigns(&mut self, c: &Constructor, me: u32) { + for stmt in &c.disasm { + let DisasmStmt::Assign { + target: DisasmTarget::Context(cid), + value, + } = stmt + else { + continue; + }; + // `inst_next` is not known during the descent. A value that needs + // it is left alone rather than computed from a wrong length. + let Some(v) = self.eval_disasm(value, me, None) else { + continue; + }; + let Some(field) = self.spec.context_fields.get(cid.index()) else { + continue; + }; + self.ctx.set(field, v as u64); + } + } + + /// The second pass: operand values and `globalset`, with the instruction's + /// length known. Children first. + fn actions(&mut self, node: u32, len: usize, depth: usize) { + if depth > self.limits.max_depth { + return; + } + let children: Vec = self.nodes[node as usize] + .operands + .iter() + .filter_map(|o| match o.value { + Value::Sub(c) => Some(c), + _ => None, + }) + .collect(); + for child in children { + self.actions(child, len, depth + 1); + } + + let c = self.spec.constructor(self.nodes[node as usize].constructor); + let next = self.addr.wrapping_add(len as u64); + for stmt in &c.disasm { + match stmt { + DisasmStmt::Assign { + target: DisasmTarget::Operand(oi), + value, + } => { + let Some(v) = self.eval_disasm(value, node, Some(next)) else { + continue; + }; + if let Some(op) = self.nodes[node as usize].operands.get_mut(*oi as usize) { + op.value = Value::Int(v); + op.raw = v; + } + } + DisasmStmt::Assign { .. } => {} + DisasmStmt::GlobalSet { address, context } => { + let Some(target) = self.eval_disasm(address, node, Some(next)) else { + continue; + }; + let Some(field) = self.spec.context_fields.get(context.index()) else { + continue; + }; + let Some(value) = self.ctx.get(field) else { + continue; + }; + if self.commits.len() < 256 { + self.commits.push(( + target as u64, + Commit { + field: *context, + value, + noflow: field.noflow, + }, + )); + } + } + } + } + } + + // ---- pattern re-evaluation ---- + + /// Evaluate a constructor's pattern as written, for the cases where the + /// masks are only a filter. + /// + /// Constraints on an *operand* are settled from the operand's resolved + /// offset, which the front end computed with the whole concatenation in + /// view. Only a constraint on a bare field, which is not an operand and so + /// has no recorded offset, is settled positionally, by walking `;` from the + /// constructor's start. That split is what makes x86's `... (m128 & + /// XmmReg ...); XmmCondPD` decidable at all: the suffix byte is an operand + /// and its place came from the reducer. + /// + /// Returns whether the pattern holds and where it ends, both relative to + /// the constructor's start. + fn eval_pattern( + &self, + expr: &PatternExpr, + me: u32, + at: usize, + start: usize, + depth: usize, + ) -> (bool, usize) { + if depth > self.limits.max_depth { + return (true, start); + } + match expr { + PatternExpr::Epsilon => (true, start), + PatternExpr::Symbol(sym) => (true, self.width_of(*sym, me, start)), + PatternExpr::And(a, b) => { + let (oka, ea) = self.eval_pattern(a, me, at, start, depth + 1); + let (okb, eb) = self.eval_pattern(b, me, at, start, depth + 1); + (oka && okb, ea.max(eb)) + } + PatternExpr::Or(a, b) => { + let (oka, ea) = self.eval_pattern(a, me, at, start, depth + 1); + if oka { + return (true, ea); + } + self.eval_pattern(b, me, at, start, depth + 1) + } + PatternExpr::Cat(a, b) => { + let (oka, ea) = self.eval_pattern(a, me, at, start, depth + 1); + let (okb, eb) = self.eval_pattern(b, me, at, ea, depth + 1); + (oka && okb, eb) + } + // `a ...` left justifies a and stops it forcing the length, so it + // holds where a holds and contributes no end of its own. + PatternExpr::EllipsisRight(a) => { + let (ok, _) = self.eval_pattern(a, me, at, start, depth + 1); + (ok, start) + } + // `... a` right justifies a at a length this walk does not know. + // Constraints inside it that name operands still decide, because + // an operand carries its own resolved offset; anything positional + // inside it is admitted rather than guessed at, which is the safe + // direction. + PatternExpr::EllipsisLeft(a) => { + let (ok, _) = self.eval_pattern(a, me, at, start, depth + 1); + (ok, start) + } + PatternExpr::Constraint { lhs, op, rhs } => { + let end = self.width_of(*lhs, me, start); + let (Some(left), Some(right)) = ( + self.symbol_value(*lhs, me, at, start), + self.eval_disasm(rhs, me, None), + ) else { + // Something the model does not give a value to. Admitting + // it keeps the filter a filter. + return (true, end); + }; + let ok = match op { + ConstraintOp::Equal => left == right, + ConstraintOp::NotEqual => left != right, + ConstraintOp::Less => left < right, + ConstraintOp::LessEqual => left <= right, + ConstraintOp::Greater => left > right, + ConstraintOp::GreaterEqual => left >= right, + }; + (ok, end) + } + } + } + + /// Where a symbol's bits end, relative to the constructor's start, given + /// that they begin at `start`. + fn width_of(&self, sym: SymbolRef, me: u32, start: usize) -> usize { + match sym { + SymbolRef::Operand(i) => match self.nodes[me as usize].operands.get(i as usize) { + Some(op) => op.end - self.nodes[me as usize].start, + None => start, + }, + SymbolRef::Field(f) => match self.spec.fields.get(f.index()) { + Some(field) => start + self.spec.tokens[field.token.index()].size as usize, + None => start, + }, + _ => start, + } + } + + /// The integer a pattern constraint compares. + fn symbol_value(&self, sym: SymbolRef, me: u32, at: usize, start: usize) -> Option { + match sym { + SymbolRef::Operand(i) => { + let op = self.nodes[me as usize].operands.get(i as usize)?; + match op.value { + // A subtable constrained by value is not something the + // model gives an integer for. + Value::Sub(_) | Value::Unresolved => None, + _ => Some(op.raw), + } + } + SymbolRef::Field(f) => self.field_at(f, at + start), + SymbolRef::Context(c) => { + let field = self.spec.context_fields.get(c.index())?; + let raw = self.ctx.get(field)?; + Some(if field.signed { + crate::model::sign_extend(raw, field.bits()) + } else { + raw as i64 + }) + } + _ => None, + } + } + + /// Read a token field at an absolute byte offset. + fn field_at(&self, id: FieldId, offset: usize) -> Option { + let field = self.spec.fields.get(id.index())?; + let token = self.spec.tokens.get(field.token.index())?; + let raw = field.extract(token, self.bytes.get(offset..)?)?; + Some(if field.signed { + crate::model::sign_extend(raw, field.bits()) + } else { + raw as i64 + }) + } + + // ---- disassembly time arithmetic ---- + + /// Evaluate a disassembly expression. `next` is the address after the + /// instruction, which is only known in the second pass; `None` makes + /// `inst_next` unavailable rather than wrong. + fn eval_disasm(&self, e: &DisasmExpr, me: u32, next: Option) -> Option { + self.eval_disasm_at(e, me, next, 0) + } + + fn eval_disasm_at( + &self, + e: &DisasmExpr, + me: u32, + next: Option, + depth: usize, + ) -> Option { + if depth > self.limits.max_depth { + return None; + } + match e { + DisasmExpr::Num(v) => Some(*v), + DisasmExpr::Symbol(s) => match s { + SymbolRef::Builtin(Builtin::InstStart) => Some(self.addr as i64), + SymbolRef::Builtin(Builtin::InstNext) => next.map(|n| n as i64), + // The address after the *next* instruction needs a decode this + // one has not done. Delay slots are the only user. + SymbolRef::Builtin(Builtin::InstNext2) => None, + SymbolRef::Builtin(Builtin::Epsilon) => Some(0), + SymbolRef::Operand(i) => { + let op = self.nodes[me as usize].operands.get(*i as usize)?; + match op.value { + Value::Unresolved => None, + _ => Some(op.raw), + } + } + SymbolRef::Field(f) => self.field_at(*f, self.nodes[me as usize].start), + SymbolRef::Context(c) => { + let field = self.spec.context_fields.get(c.index())?; + let raw = self.ctx.get(field)?; + Some(if field.signed { + crate::model::sign_extend(raw, field.bits()) + } else { + raw as i64 + }) + } + SymbolRef::Varnode(v) => self.spec.varnodes.get(v.index()).map(|n| n.offset as i64), + _ => None, + }, + DisasmExpr::Unary(op, a) => { + let a = self.eval_disasm_at(a, me, next, depth + 1)?; + Some(match op { + DisasmUnOp::Negate => a.wrapping_neg(), + DisasmUnOp::Not => !a, + }) + } + DisasmExpr::Binary(op, a, b) => { + let a = self.eval_disasm_at(a, me, next, depth + 1)?; + let b = self.eval_disasm_at(b, me, next, depth + 1)?; + Some(match op { + DisasmBinOp::Add => a.wrapping_add(b), + DisasmBinOp::Sub => a.wrapping_sub(b), + DisasmBinOp::Mul => a.wrapping_mul(b), + DisasmBinOp::Div => { + if b == 0 { + return None; + } + a.wrapping_div(b) + } + // A shift count wider than the type is zero, not undefined + // behaviour and not a panic. + DisasmBinOp::Shl => { + if !(0..64).contains(&b) { + 0 + } else { + a.wrapping_shl(b as u32) + } + } + DisasmBinOp::Shr => { + if !(0..64).contains(&b) { + if a < 0 { -1 } else { 0 } + } else { + a.wrapping_shr(b as u32) + } + } + DisasmBinOp::And => a & b, + DisasmBinOp::Or => a | b, + DisasmBinOp::Xor => a ^ b, + }) + } + } + } +} + +/// Put a field's raw value through its `attach`, if it has one. `None` means +/// the attachment has a hole at this encoding, which makes the encoding +/// invalid rather than falling back to the integer. +fn attached(attach: &Attach, raw: u64, signed: i64) -> Option { + match attach { + Attach::None => Some(Value::Int(signed)), + Attach::Variables(v) => match v.get(raw as usize) { + Some(Some(id)) => Some(Value::Var(*id)), + _ => None, + }, + Attach::Names(v) => match v.get(raw as usize) { + Some(Some(s)) => Some(Value::Name(s.clone())), + _ => None, + }, + Attach::Values(v) => match v.get(raw as usize) { + Some(Some(n)) => Some(Value::Int(*n)), + _ => None, + }, + } +} + +/// What an instruction does to control flow, read off the semantic bodies of +/// the constructors that matched. +/// +/// This is not part of the SLEIGH model: the language says `goto`, `call` and +/// `return`, and what a caller wants is whether the next instruction is a +/// successor and where else control can go. Derived here rather than in the +/// lifter because a disassembly listing wants it without lifting anything. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct FlowSummary { + /// A `goto` with no condition, or a `return`. + pub unconditional: bool, + /// A `goto` under an `if`. + pub conditional: bool, + /// A `call` of either kind. + pub call: bool, + /// A `return`. + pub ret: bool, + /// The target of a direct branch or call, when there was exactly one and + /// it was a constant. + pub target: Option, + /// A `goto [v]` or `call [v]`, whose target is not known here. + pub indirect: bool, +} + +impl FlowSummary { + /// Whether the instruction after this one in address order is a successor. + pub fn falls_through(&self) -> bool { + !(self.unconditional || self.ret) + } +} + +impl Decoded { + /// Summarise what the matched constructors do to control flow. + pub fn flow(&self, spec: &Spec) -> FlowSummary { + let mut out = FlowSummary::default(); + for node in &self.nodes { + let Some(body) = &spec.constructor(node.constructor).body else { + continue; + }; + summarise(body, &mut out, 0); + } + out + } +} + +fn summarise(body: &[Stmt], out: &mut FlowSummary, depth: usize) { + if depth > 16 { + return; + } + for stmt in body { + match stmt { + // A `goto