mirror of
https://github.com/angr/angr
synced 2026-08-17 12:23:11 -04:00
Compare commits
122 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8a5cabdfa | ||
|
|
2165b8e2a5 | ||
|
|
09e59e4204 | ||
|
|
55530509ce | ||
|
|
503b1be066 | ||
|
|
2c95eb92e6 | ||
|
|
b80f4cf2af | ||
|
|
41a7db2f17 | ||
|
|
2bfaa7e8f7 | ||
|
|
0c293dc0de | ||
|
|
ede5faf162 | ||
|
|
bdc1321845 | ||
|
|
666fadabc5 | ||
|
|
fdd66945ea | ||
|
|
b0e3541dfd | ||
|
|
780a80de79 | ||
|
|
c751678d55 | ||
|
|
08c7e48d5b | ||
|
|
c844a13eac | ||
|
|
795eadfe84 | ||
|
|
0f4be85db4 | ||
|
|
04b40a1eec | ||
|
|
4018e26076 | ||
|
|
e76c4118a7 | ||
|
|
7d3c7c82ce | ||
|
|
b948687876 | ||
|
|
1c99579f69 | ||
|
|
f62f45363c | ||
|
|
3333f39fff | ||
|
|
7c2e3501e6 | ||
|
|
f160178837 | ||
|
|
71bf42c9b8 | ||
|
|
fd235fcb90 | ||
|
|
f4b23d4444 | ||
|
|
4a9c1454dc | ||
|
|
233724f38e | ||
|
|
551df09fc5 | ||
|
|
f71b07cebf | ||
|
|
f9c789ff6a | ||
|
|
15757fb2da | ||
|
|
0f8e082f9a | ||
|
|
49434bc3fc | ||
|
|
2f891d1d69 | ||
|
|
d46e56f891 | ||
|
|
61bac8ffd0 | ||
|
|
d38cc5a019 | ||
|
|
a4b05a0ce9 | ||
|
|
7cedfbfa8b | ||
|
|
b9358da5da | ||
|
|
6933b065f0 | ||
|
|
508ac3a44c | ||
|
|
6b2637c446 | ||
|
|
a7ae033c69 | ||
|
|
659f3d7f5d | ||
|
|
4462c849b9 | ||
|
|
22613f4a0a | ||
|
|
2fffb71f86 | ||
|
|
55f059982b | ||
|
|
1a5eedf622 | ||
|
|
2080c15a26 | ||
|
|
3e2e4b90fc | ||
|
|
59400706f8 | ||
|
|
bc72b9e1a6 | ||
|
|
651d9cccc2 | ||
|
|
9c1fb2367e | ||
|
|
e4ff240001 | ||
|
|
6d5860d0ba | ||
|
|
3b41a92e29 | ||
|
|
83f364dc01 | ||
|
|
787c2c7d8e | ||
|
|
b37cca0101 | ||
|
|
fc7ff8e62c | ||
|
|
83d0061e12 | ||
|
|
f74d1c5c1e | ||
|
|
be9c801b16 | ||
|
|
3efd1ec6db | ||
|
|
db21fb0fee | ||
|
|
91cc026062 | ||
|
|
90062a9914 | ||
|
|
fe434a049b | ||
|
|
208ec719a6 | ||
|
|
8b98c5ad6c | ||
|
|
2dd6cb393b | ||
|
|
cf54c35b9b | ||
|
|
1808e7fadd | ||
|
|
c13863214c | ||
|
|
75500dd271 | ||
|
|
85d77f0301 | ||
|
|
0849ddb03c | ||
|
|
8dc3772762 | ||
|
|
8608d976e3 | ||
|
|
2eadec31c6 | ||
|
|
2f8266bdda | ||
|
|
02c374b5b4 | ||
|
|
e8858b82cd | ||
|
|
bcf5902d07 | ||
|
|
21cea3815f | ||
|
|
1102d5fc47 | ||
|
|
95fa7ea003 | ||
|
|
26736ec096 | ||
|
|
0c87229837 | ||
|
|
8ad0dc91aa | ||
|
|
a3c8d835c7 | ||
|
|
0fcf7c050b | ||
|
|
dd1cefa642 | ||
|
|
d099a933bf | ||
|
|
2c6c986f66 | ||
|
|
7d74ec8924 | ||
|
|
41baf181f3 | ||
|
|
ada9fcd585 | ||
|
|
635ddcd134 | ||
|
|
e9d795193a | ||
|
|
d145bd41fd | ||
|
|
3984a816b6 | ||
|
|
1944e72e90 | ||
|
|
bd58a30e6e | ||
|
|
a6f88cd76f | ||
|
|
c213b234b3 | ||
|
|
352d642363 | ||
|
|
07115bb2ad | ||
|
|
d9071a160e | ||
|
|
f70bd5f0e1 |
414 changed files with 20331 additions and 4398 deletions
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
|
|
@ -16,20 +16,26 @@ jobs:
|
|||
ci:
|
||||
uses: angr/ci-settings/.github/workflows/angr-ci.yml@master
|
||||
|
||||
smoketest:
|
||||
name: Test installation
|
||||
installation:
|
||||
name: Test installation (${{ matrix.environment.os }}, py${{ matrix.environment.python-version }})
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-2022, macos-15-intel]
|
||||
environment:
|
||||
- os: windows-2025
|
||||
python-version: 3.12
|
||||
- os: macos-26
|
||||
python-version: 3.12
|
||||
- os: ubuntu-24.04
|
||||
python-version: 3.14
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.environment.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1
|
||||
if: startsWith(runner.os, 'windows')
|
||||
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v6
|
||||
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v6
|
||||
- name: Sync dependencies
|
||||
run: uv sync -p 3.12
|
||||
run: uv sync -p ${{ matrix.environment.python-version }}
|
||||
- name: Collect tests
|
||||
run: uv run pytest --collect-only tests
|
||||
|
||||
|
|
@ -37,7 +43,7 @@ jobs:
|
|||
name: Rust Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
|
|
@ -50,9 +56,9 @@ jobs:
|
|||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v5
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: cargo test --release
|
||||
|
|
|
|||
16
.github/workflows/coverage.yml
vendored
16
.github/workflows/coverage.yml
vendored
|
|
@ -23,9 +23,9 @@ jobs:
|
|||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v6
|
||||
- name: Restore test durations cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
tar -xpf $PWD/env.tzst -C /
|
||||
rm env.tzst
|
||||
- name: Download test binaries
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
repository: angr/binaries
|
||||
path: binaries
|
||||
|
|
@ -96,13 +96,13 @@ jobs:
|
|||
name: Test Rust packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
|
||||
- uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
|
||||
- uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2
|
||||
with:
|
||||
tool: cargo-llvm-cov
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v5
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Run tests
|
||||
|
|
|
|||
12
.github/workflows/nightly-ci.yml
vendored
12
.github/workflows/nightly-ci.yml
vendored
|
|
@ -28,15 +28,15 @@ jobs:
|
|||
runner_id: [1, 2, 3, 4, 5]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
path: angr
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
repository: angr/binaries
|
||||
path: binaries
|
||||
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1
|
||||
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v6
|
||||
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v6
|
||||
- name: Setup Java for pysoot
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
|
|
@ -54,14 +54,14 @@ jobs:
|
|||
runner_id: [1, 2, 3]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
path: angr
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
|
||||
with:
|
||||
repository: angr/binaries
|
||||
path: binaries
|
||||
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v6
|
||||
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v6
|
||||
- name: Sync dependencies
|
||||
run: uv --directory angr sync -p 3.12
|
||||
- name: Run pytest
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -38,4 +38,5 @@ target/
|
|||
.DS_Store
|
||||
*_angr_rtdb
|
||||
*_angr_rtdb_*
|
||||
.claude/
|
||||
.claude
|
||||
angr/protos/*_pb2.py
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ repos:
|
|||
args: [--py310-plus]
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.21
|
||||
rev: v0.16.1
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
|
|
|||
57
Cargo.lock
generated
57
Cargo.lock
generated
|
|
@ -1257,9 +1257,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.29.0"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
|
||||
checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"libc",
|
||||
|
|
@ -1274,18 +1274,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.29.0"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
|
||||
checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
|
||||
dependencies = [
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.29.0"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
|
||||
checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
|
|
@ -1293,9 +1293,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.29.0"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
|
||||
checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
|
|
@ -1305,9 +1305,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.29.0"
|
||||
version = "0.29.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
|
||||
checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
|
@ -1367,9 +1367,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.2"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
|
@ -1379,9 +1379,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
|
@ -1390,9 +1390,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "region"
|
||||
|
|
@ -1497,9 +1497,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
|
|
@ -1529,22 +1529,22 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
"syn 3.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1669,6 +1669,17 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
|
|
|
|||
|
|
@ -3,3 +3,5 @@ include README.md
|
|||
include Cargo.toml
|
||||
include Cargo.lock
|
||||
graft native
|
||||
recursive-include angr/protos *.proto
|
||||
recursive-exclude angr/protos *_pb2.py
|
||||
|
|
|
|||
|
|
@ -43,11 +43,13 @@ import angr
|
|||
|
||||
project = angr.Project("angr-doc/examples/defcamp_r100/r100", auto_load_libs=False)
|
||||
|
||||
|
||||
@project.hook(0x400844)
|
||||
def print_flag(state):
|
||||
print("FLAG SHOULD BE:", state.posix.dumps(0))
|
||||
project.terminate_execution()
|
||||
|
||||
|
||||
project.execute()
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# pylint: disable=wrong-import-position
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "9.3.0.dev0"
|
||||
__version__ = "9.3.3.dev0"
|
||||
|
||||
if bytes is str:
|
||||
raise Exception("""
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from angr.rustylib.ailment import ExpressionKind
|
||||
|
||||
from . import expression, statement
|
||||
from .block import Block
|
||||
from .block_walker import AILBlockRewriter, AILBlockViewer, AILBlockWalker
|
||||
|
|
@ -69,6 +71,7 @@ __all__ = [
|
|||
"Const",
|
||||
"Expr",
|
||||
"Expression",
|
||||
"ExpressionKind",
|
||||
"IRSBConverter",
|
||||
"Manager",
|
||||
"NoOp",
|
||||
|
|
|
|||
|
|
@ -935,11 +935,12 @@ class AILBlockRewriter(AILBlockWalker[Expression, Statement, Block]):
|
|||
return stmt
|
||||
|
||||
def _handle_Return(self, stmt_idx: int, stmt: Return, block: Block | None) -> Statement:
|
||||
if stmt.ret_exprs:
|
||||
ret_exprs_in = stmt.ret_exprs
|
||||
if ret_exprs_in:
|
||||
new_ret_exprs = [
|
||||
self._handle_expr(idx, expr, stmt_idx, stmt, block) for idx, expr in enumerate(stmt.ret_exprs)
|
||||
self._handle_expr(idx, expr, stmt_idx, stmt, block) for idx, expr in enumerate(ret_exprs_in)
|
||||
]
|
||||
changed = any(old is not new for new, old in zip(new_ret_exprs, stmt.ret_exprs))
|
||||
changed = any(old != new for new, old in zip(new_ret_exprs, ret_exprs_in))
|
||||
|
||||
if changed:
|
||||
return Return(stmt.idx, new_ret_exprs, **stmt.tags)
|
||||
|
|
@ -1114,13 +1115,12 @@ class AILBlockRewriter(AILBlockWalker[Expression, Statement, Block]):
|
|||
super()._handle_Phi(expr_idx, expr, stmt_idx, stmt, block)
|
||||
return expr
|
||||
|
||||
changed = False
|
||||
|
||||
src_and_vvars_in = expr.src_and_vvars
|
||||
src_and_vvars = [
|
||||
(src, self._handle_expr(idx, vvar, stmt_idx, stmt, block) if vvar is not None else None)
|
||||
for idx, (src, vvar) in enumerate(expr.src_and_vvars)
|
||||
for idx, (src, vvar) in enumerate(src_and_vvars_in)
|
||||
]
|
||||
changed = any(new is not old for (_, new), (_, old) in zip(src_and_vvars, expr.src_and_vvars))
|
||||
changed = any(new != old for (_, new), (_, old) in zip(src_and_vvars, src_and_vvars_in))
|
||||
|
||||
if changed:
|
||||
assert all(vvar is None or isinstance(vvar, VirtualVariable) for _, vvar in src_and_vvars)
|
||||
|
|
@ -1137,7 +1137,7 @@ class AILBlockRewriter(AILBlockWalker[Expression, Statement, Block]):
|
|||
) -> Expression:
|
||||
operands_in = expr.operands
|
||||
new_operands = [self._handle_expr(0, operand, stmt_idx, stmt, block) for operand in operands_in]
|
||||
changed = any(new is not old for new, old in zip(new_operands, operands_in))
|
||||
changed = any(new != old for new, old in zip(new_operands, operands_in))
|
||||
|
||||
new_guard = None
|
||||
guard_in = expr.guard
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ class PCodeIRSBConverter(Converter):
|
|||
return Convert(self._manager.next_atom(), t.bits, size, False, t, ins_addr=self._manager.ins_addr)
|
||||
|
||||
return Tmp(self._manager.next_atom(), offset, size)
|
||||
if space_name in ["ram", "mem"]:
|
||||
if space_name.lower() in ["ram", "mem"]:
|
||||
assert not is_write
|
||||
addr = Const(self._manager.next_atom(), varnode.offset, self._manager.arch.bits)
|
||||
# Note: Load takes bytes, not bits, for size
|
||||
|
|
@ -359,7 +359,7 @@ class PCodeIRSBConverter(Converter):
|
|||
return Assignment(
|
||||
self._statement_idx, self._convert_varnode(varnode, True), value, ins_addr=self._manager.ins_addr
|
||||
)
|
||||
if space_name in ["ram", "mem"]:
|
||||
if space_name.lower() in ["ram", "mem"]:
|
||||
addr = Const(self._manager.next_atom(), varnode.offset, self._manager.arch.bits)
|
||||
return Store(
|
||||
self._statement_idx,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
||||
import archinfo
|
||||
|
||||
from angr import ailment
|
||||
|
|
@ -11,11 +9,6 @@ try:
|
|||
except ImportError:
|
||||
from typing import Never as Bits
|
||||
|
||||
try:
|
||||
import _md5 as md5lib # type: ignore # stdlib C module without stubs
|
||||
except ImportError:
|
||||
import hashlib as md5lib
|
||||
|
||||
type GetBitsTypeParams = "ailment.expression.Expression"
|
||||
|
||||
|
||||
|
|
@ -27,62 +20,6 @@ def get_bits(expr: GetBitsTypeParams) -> int:
|
|||
raise TypeError(type(expr))
|
||||
|
||||
|
||||
md5_unpacker = struct.Struct("4I")
|
||||
|
||||
|
||||
def stable_hash(t: tuple) -> int:
|
||||
cnt = _dump_tuple(t)
|
||||
hd = md5lib.md5(cnt).digest()
|
||||
return md5_unpacker.unpack(hd)[0] # 32 bits
|
||||
|
||||
|
||||
def _dump_tuple(t: tuple) -> bytes:
|
||||
cnt = b""
|
||||
for item in t:
|
||||
if item is not None:
|
||||
type_ = type(item)
|
||||
if type_ in _DUMP_BY_TYPE:
|
||||
cnt += _DUMP_BY_TYPE[type_](item)
|
||||
else:
|
||||
# for TaggedObjects, hash(item) is stable
|
||||
# other types of items may show up, such as pyvex.expr.CCall and Dirty. they will be removed some day.
|
||||
cnt += struct.pack("<Q", hash(item) & 0xFFFF_FFFF_FFFF_FFFF)
|
||||
cnt += b"\xf0"
|
||||
return cnt
|
||||
|
||||
|
||||
def _dump_str(t: str) -> bytes:
|
||||
return t.encode("utf-8")
|
||||
|
||||
|
||||
def _dump_int(t: int) -> bytes:
|
||||
prefix = b"" if t >= 0 else b"-"
|
||||
t = abs(t)
|
||||
if t <= 0xFFFF:
|
||||
return prefix + struct.pack("<H", t)
|
||||
if t <= 0xFFFF_FFFF:
|
||||
return prefix + struct.pack("<I", t)
|
||||
if t <= 0xFFFF_FFFF_FFFF_FFFF:
|
||||
return prefix + struct.pack("<Q", t)
|
||||
cnt = b""
|
||||
while t > 0:
|
||||
cnt += _dump_int(t & 0xFFFF_FFFF_FFFF_FFFF)
|
||||
t >>= 64
|
||||
return prefix + cnt
|
||||
|
||||
|
||||
def _dump_type(t: type) -> bytes:
|
||||
return t.__name__.encode("ascii")
|
||||
|
||||
|
||||
_DUMP_BY_TYPE = {
|
||||
tuple: _dump_tuple,
|
||||
str: _dump_str,
|
||||
int: _dump_int,
|
||||
type: _dump_type,
|
||||
}
|
||||
|
||||
|
||||
def is_none_or_likeable(arg1, arg2, is_list=False):
|
||||
"""
|
||||
Returns whether two things are both None or can like each other
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ from .proximity_graph import ProximityGraphAnalysis
|
|||
from .reaching_definitions import ReachingDefinitionsAnalysis
|
||||
from .reassembler import Reassembler
|
||||
from .s_liveness import SLivenessAnalysis
|
||||
from .s_propagator import SPropagatorAnalysis
|
||||
from .s_reaching_definitions import SReachingDefinitionsAnalysis
|
||||
from .s_propagator import SPropagator, SPropagatorAnalysis
|
||||
from .s_reaching_definitions import SReachingDefinitions, SReachingDefinitionsAnalysis
|
||||
from .smc import SelfModifyingCodeAnalysis
|
||||
from .soot_class_hierarchy import SootClassHierarchy
|
||||
from .stack_pointer_tracker import StackPointerTracker
|
||||
|
|
@ -97,7 +97,9 @@ __all__ = (
|
|||
"ReachingDefinitionsAnalysis",
|
||||
"Reassembler",
|
||||
"SLivenessAnalysis",
|
||||
"SPropagator",
|
||||
"SPropagatorAnalysis",
|
||||
"SReachingDefinitions",
|
||||
"SReachingDefinitionsAnalysis",
|
||||
"SelfModifyingCodeAnalysis",
|
||||
"SootClassHierarchy",
|
||||
|
|
|
|||
|
|
@ -323,9 +323,7 @@ class BackwardSlice(Analysis):
|
|||
if simrun not in cfg:
|
||||
l.error("SimRun instance %s is not in the CFG.", simrun)
|
||||
|
||||
stack = []
|
||||
for simrun in simruns:
|
||||
stack.append(simrun)
|
||||
stack = simruns.copy()
|
||||
|
||||
self.runs_in_slice = networkx.DiGraph()
|
||||
self.cfg_nodes_in_slice = networkx.DiGraph()
|
||||
|
|
|
|||
|
|
@ -108,12 +108,9 @@ class BinaryOptimizer(Analysis):
|
|||
|
||||
BLOCKS_THRESHOLD = 500 # do not optimize a function if it has more than this number of blocks
|
||||
|
||||
def __init__(self, cfg, techniques):
|
||||
def __init__(self, cfg, techniques: set[str]):
|
||||
self.cfg = cfg
|
||||
|
||||
if techniques is None:
|
||||
raise Exception("At least one optimization technique must be specified.")
|
||||
|
||||
supported_techniques = {
|
||||
"constant_propagation",
|
||||
"redundant_stack_variable_removal",
|
||||
|
|
@ -122,7 +119,7 @@ class BinaryOptimizer(Analysis):
|
|||
}
|
||||
|
||||
if techniques - supported_techniques:
|
||||
raise Exception("At least one optimization technique specified is not supported.")
|
||||
raise ValueError("At least one optimization technique specified is not supported.")
|
||||
|
||||
self._techniques = techniques.copy()
|
||||
|
||||
|
|
|
|||
|
|
@ -747,8 +747,7 @@ class FunctionDiff:
|
|||
ordered_succ.append(x)
|
||||
|
||||
# add the rest (sorting might be better than no order)
|
||||
for s in sorted(succ - set(ordered_succ), key=lambda x: x.addr):
|
||||
ordered_succ.append(s)
|
||||
ordered_succ.extend(sorted(succ - set(ordered_succ), key=lambda x: x.addr))
|
||||
return ordered_succ
|
||||
except (SimMemoryError, SimEngineError):
|
||||
return sorted(succ, key=lambda x: x.addr)
|
||||
|
|
|
|||
|
|
@ -56,9 +56,7 @@ class BoyScout(Analysis):
|
|||
|
||||
l.debug("%s %s hits %d times", arch.name, arch.memory_endness, votes[(arch.name, arch.memory_endness)])
|
||||
|
||||
arch_name, endianness, hits = sorted(
|
||||
[(k[0], k[1], v) for k, v in votes.items()], key=lambda x: x[2], reverse=True
|
||||
)[0]
|
||||
arch_name, endianness, hits = max([(k[0], k[1], v) for k, v in votes.items()], key=lambda x: x[2])
|
||||
|
||||
if hits < self.cookiesize * 2:
|
||||
# this cannot possibly be code
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from angr.sim_type import (
|
|||
SimTypeInt128,
|
||||
SimTypeLongLong,
|
||||
SimTypePointer,
|
||||
SimTypeReg,
|
||||
SimTypeShort,
|
||||
parse_cpp_file,
|
||||
)
|
||||
|
|
@ -183,13 +184,24 @@ class CallingConventionAnalysis(Analysis):
|
|||
|
||||
assert self._function is not None
|
||||
|
||||
cpp_symbol_result: tuple[SimCC, SimTypeCppFunction, str | None] | None = None
|
||||
demangled_name = self._function.demangled_name
|
||||
if demangled_name != self._function.name:
|
||||
r_demangled = self._analyze_demangled_name(demangled_name)
|
||||
if r_demangled is not None:
|
||||
self.cc, self.prototype, self.prototype_libname = r_demangled
|
||||
self.proto_from_symbol = True
|
||||
return
|
||||
# Itanium names usually omit the return type, and a qualified name does
|
||||
# not distinguish a namespace/static function from a non-static member.
|
||||
# parse_cpp_file() consequently carries a possible-this placeholder.
|
||||
# Do not let that incomplete declaration bypass callee/callsite analysis;
|
||||
# refine it with machine facts below. Declarations that encode an explicit
|
||||
# calling convention (such as Microsoft C++ symbols) remain authoritative.
|
||||
demangled_cc, demangled_proto, demangled_libname = r_demangled
|
||||
if isinstance(demangled_proto, SimTypeCppFunction) and demangled_proto.convention is None:
|
||||
cpp_symbol_result = demangled_cc, demangled_proto, demangled_libname
|
||||
else:
|
||||
self.cc, self.prototype, self.prototype_libname = r_demangled
|
||||
self.proto_from_symbol = True
|
||||
return
|
||||
|
||||
if self._function.is_simprocedure:
|
||||
hooker = self.project.hooked_by(self._function.addr)
|
||||
|
|
@ -282,6 +294,9 @@ class CallingConventionAnalysis(Analysis):
|
|||
r = self._analyze_function()
|
||||
if r is None:
|
||||
l.warning("Cannot determine calling convention for %r.", self._function)
|
||||
if cpp_symbol_result is not None:
|
||||
self.cc, self.prototype, self.prototype_libname = cpp_symbol_result
|
||||
self.proto_from_symbol = True
|
||||
else:
|
||||
# adjust prototype if needed
|
||||
cc, prototype = r
|
||||
|
|
@ -296,9 +311,48 @@ class CallingConventionAnalysis(Analysis):
|
|||
else None
|
||||
)
|
||||
|
||||
if cpp_symbol_result is not None and prototype is not None:
|
||||
prototype = self._refine_cpp_symbol_prototype(prototype, cpp_symbol_result[1])
|
||||
self.cc = cc
|
||||
self.prototype = prototype
|
||||
|
||||
@staticmethod
|
||||
def _refine_cpp_symbol_prototype(
|
||||
machine_proto: SimTypeFunction, symbol_proto: SimTypeCppFunction
|
||||
) -> SimTypeFunction:
|
||||
"""Merge encoded C++ types only where machine ABI arity disambiguates them.
|
||||
|
||||
The parser's first pointer is a *possible* ``this``. If machine facts recover
|
||||
one fewer arguments, the qualified name was a namespace/static function and the
|
||||
placeholder is removed. If arity agrees it is retained. Any other disagreement
|
||||
keeps the machine-derived arguments. A non-Bottom encoded template return may
|
||||
refine the return type; ordinary Itanium names keep the machine-derived return.
|
||||
"""
|
||||
machine_args = tuple(machine_proto.args or ())
|
||||
symbol_args = tuple(symbol_proto.args or ())
|
||||
if len(symbol_args) == len(machine_args):
|
||||
selected = symbol_args
|
||||
elif len(symbol_args) == len(machine_args) + 1 and symbol_args and isinstance(symbol_args[0], SimTypePointer):
|
||||
selected = symbol_args[1:]
|
||||
else:
|
||||
selected = machine_args
|
||||
# Opaque C++ classes cannot be laid out by a calling convention. Preserve the
|
||||
# machine-derived slot for those arguments; only scalar/reference or pointer
|
||||
# types from the linkage name are safe refinements.
|
||||
args = tuple(
|
||||
sym if isinstance(sym, (SimTypeReg, SimTypePointer)) else machine
|
||||
for machine, sym in zip(machine_args, selected)
|
||||
)
|
||||
symbol_ret = symbol_proto.returnty
|
||||
ret = (
|
||||
symbol_ret
|
||||
if symbol_ret is not None
|
||||
and not isinstance(symbol_ret, SimTypeBottom)
|
||||
and isinstance(symbol_ret, (SimTypeReg, SimTypePointer))
|
||||
else machine_proto.returnty
|
||||
)
|
||||
return SimTypeFunction(args, ret, variadic=machine_proto.variadic)
|
||||
|
||||
def _analyze_callsite_only(self):
|
||||
assert self.caller_func_addr is not None
|
||||
assert self.callsite_block_addr is not None
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Container, Iterator
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pyvex
|
||||
|
|
@ -467,6 +468,141 @@ class FactCollector(Analysis):
|
|||
state.register_written(offset, self.project.arch.registers[reg_name][1])
|
||||
state.simple_regs[offset] = None
|
||||
|
||||
@staticmethod
|
||||
def _resolve_vex_tmp(
|
||||
expr: pyvex.IRExpr.IRExpr,
|
||||
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
|
||||
seen_tmps: frozenset[int] = frozenset(),
|
||||
) -> pyvex.IRExpr.IRExpr:
|
||||
while isinstance(expr, pyvex.IRExpr.RdTmp) and expr.tmp not in seen_tmps:
|
||||
definition = tmp_definitions.get(expr.tmp)
|
||||
if definition is None:
|
||||
break
|
||||
seen_tmps |= {expr.tmp}
|
||||
expr = definition
|
||||
return expr
|
||||
|
||||
@classmethod
|
||||
def _walk_vex_expr(
|
||||
cls,
|
||||
expr: pyvex.IRExpr.IRExpr,
|
||||
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
|
||||
seen_tmps: frozenset[int] = frozenset(),
|
||||
) -> Iterator[pyvex.IRExpr.IRExpr]:
|
||||
if isinstance(expr, pyvex.IRExpr.RdTmp):
|
||||
if expr.tmp in seen_tmps:
|
||||
return
|
||||
definition = tmp_definitions.get(expr.tmp)
|
||||
if definition is not None:
|
||||
yield from cls._walk_vex_expr(definition, tmp_definitions, seen_tmps | {expr.tmp})
|
||||
return
|
||||
|
||||
yield expr
|
||||
for child in expr.child_expressions:
|
||||
yield from cls._walk_vex_expr(child, tmp_definitions, seen_tmps)
|
||||
|
||||
def _stack_canary_tls_location(self) -> tuple[int, int] | None:
|
||||
if self.project.arch.name == "AMD64":
|
||||
reg_name, offset = "fs", 0x28
|
||||
elif self.project.arch.name == "X86":
|
||||
reg_name, offset = "gs", 0x14
|
||||
else:
|
||||
return None
|
||||
return self.project.arch.registers[reg_name][0], offset
|
||||
|
||||
@classmethod
|
||||
def _is_tls_canary_load(
|
||||
cls,
|
||||
expr: pyvex.IRExpr.IRExpr,
|
||||
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
|
||||
tls_reg_offset: int,
|
||||
canary_offset: int,
|
||||
) -> bool:
|
||||
expr = cls._resolve_vex_tmp(expr, tmp_definitions)
|
||||
if not isinstance(expr, pyvex.IRExpr.Load):
|
||||
return False
|
||||
addr_nodes = tuple(cls._walk_vex_expr(expr.addr, tmp_definitions))
|
||||
return any(isinstance(node, pyvex.IRExpr.Get) and node.offset == tls_reg_offset for node in addr_nodes) and any(
|
||||
isinstance(node, pyvex.IRExpr.Const) and node.con.value == canary_offset for node in addr_nodes
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _is_stack_load(
|
||||
cls,
|
||||
expr: pyvex.IRExpr.IRExpr,
|
||||
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
|
||||
stack_reg_offsets: Container[int | None],
|
||||
) -> bool:
|
||||
expr = cls._resolve_vex_tmp(expr, tmp_definitions)
|
||||
if not isinstance(expr, pyvex.IRExpr.Load):
|
||||
return False
|
||||
return any(
|
||||
isinstance(node, pyvex.IRExpr.Get) and node.offset in stack_reg_offsets
|
||||
for node in cls._walk_vex_expr(expr.addr, tmp_definitions)
|
||||
)
|
||||
|
||||
def _has_terminal_call_successor(self, node: BlockNode) -> bool:
|
||||
func_graph = self.function.transition_graph
|
||||
for _, succ, data in func_graph.out_edges(node, data=True):
|
||||
if data.get("type") != "transition" or data.get("outside", False) or not isinstance(succ, BlockNode):
|
||||
continue
|
||||
succ_block = self.project.factory.block(succ.addr, size=succ.size)
|
||||
if succ_block.vex.jumpkind != "Ijk_Call":
|
||||
continue
|
||||
if not any(
|
||||
edge_data.get("type") == "fake_return" for _, _, edge_data in func_graph.out_edges(succ, data=True)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_stack_canary_retval_write(
|
||||
self,
|
||||
node: BlockNode,
|
||||
block: Block,
|
||||
expr: pyvex.IRExpr.IRExpr,
|
||||
tmp_definitions: dict[int, pyvex.IRExpr.IRExpr],
|
||||
) -> bool:
|
||||
tls_location = self._stack_canary_tls_location()
|
||||
if tls_location is None:
|
||||
return False
|
||||
|
||||
expr = self._resolve_vex_tmp(expr, tmp_definitions)
|
||||
if not isinstance(expr, pyvex.IRExpr.Binop) or expr.op not in {
|
||||
"Iop_Sub32",
|
||||
"Iop_Sub64",
|
||||
"Iop_Xor32",
|
||||
"Iop_Xor64",
|
||||
}:
|
||||
return False
|
||||
|
||||
tls_reg_offset, canary_offset = tls_location
|
||||
stack_reg_offsets = {self.project.arch.sp_offset, self.project.arch.bp_offset}
|
||||
op0, op1 = expr.args
|
||||
if not (
|
||||
(
|
||||
self._is_tls_canary_load(op0, tmp_definitions, tls_reg_offset, canary_offset)
|
||||
and self._is_stack_load(op1, tmp_definitions, stack_reg_offsets)
|
||||
)
|
||||
or (
|
||||
self._is_tls_canary_load(op1, tmp_definitions, tls_reg_offset, canary_offset)
|
||||
and self._is_stack_load(op0, tmp_definitions, stack_reg_offsets)
|
||||
)
|
||||
):
|
||||
return False
|
||||
|
||||
if not self._has_terminal_call_successor(node):
|
||||
return False
|
||||
|
||||
for stmt in block.vex.statements:
|
||||
if not isinstance(stmt, pyvex.IRStmt.Exit):
|
||||
continue
|
||||
guard_nodes = tuple(self._walk_vex_expr(stmt.guard, tmp_definitions))
|
||||
if any(
|
||||
self._is_tls_canary_load(node, tmp_definitions, tls_reg_offset, canary_offset) for node in guard_nodes
|
||||
) and any(self._is_stack_load(node, tmp_definitions, stack_reg_offsets) for node in guard_nodes):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _analyze_endpoints_for_retval_size(self, end_states):
|
||||
"""
|
||||
Analyze all endpoints to determine the return value size.
|
||||
|
|
@ -591,6 +727,7 @@ class FactCollector(Analysis):
|
|||
# to account for the common case where the shorter register (e.g., al) is extended to the full register
|
||||
# (e.g., rax) before returning.
|
||||
block_retval_size = None
|
||||
stack_canary_barrier = False
|
||||
for stmt in reversed(block.vex.statements):
|
||||
if isinstance(stmt, pyvex.IRStmt.Put):
|
||||
assert block.vex.tyenv is not None
|
||||
|
|
@ -610,6 +747,11 @@ class FactCollector(Analysis):
|
|||
size = 4
|
||||
|
||||
if stmt.offset == retreg_offset:
|
||||
if isinstance(node, BlockNode) and self._is_stack_canary_retval_write(
|
||||
node, block, stmt.data, tmp_definitions
|
||||
):
|
||||
stack_canary_barrier = True
|
||||
break
|
||||
block_retval_size = max(size, 1)
|
||||
if stmt.offset == overflow_retreg_offset:
|
||||
overflow_retval_sizes.append(max(size, 1))
|
||||
|
|
@ -617,6 +759,8 @@ class FactCollector(Analysis):
|
|||
if block_retval_size is not None:
|
||||
retval_sizes.append(block_retval_size)
|
||||
continue
|
||||
if stack_canary_barrier:
|
||||
continue
|
||||
|
||||
for pred, _, data in func_graph.in_edges(node, data=True):
|
||||
edge_type = data.get("type")
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ class CFGBase(Analysis):
|
|||
'"auto_load_libs" disabled, or specify "regions" to limit the scope of CFG recovery.'
|
||||
)
|
||||
|
||||
regions_derived_from_objects = regions is None
|
||||
if regions is None:
|
||||
regions = self._exec_mem_regions
|
||||
if not self._skip_unmapped_addrs and not regions:
|
||||
|
|
@ -284,6 +285,14 @@ class CFGBase(Analysis):
|
|||
for start, end in self._regions.items():
|
||||
l.debug("... %#x - %#x", start, end)
|
||||
|
||||
if regions_derived_from_objects and not self._regions_size:
|
||||
l.warning(
|
||||
"CFG recovery has nothing to scan: the regions to analyze cover 0 bytes. If %s does contain code, "
|
||||
'pass the address ranges that hold it in "regions", or set "force_segment" to derive regions from '
|
||||
"segments instead of sections.",
|
||||
self._binary,
|
||||
)
|
||||
|
||||
def __contains__(self, cfg_node):
|
||||
return cfg_node in self.graph
|
||||
|
||||
|
|
@ -325,6 +334,15 @@ class CFGBase(Analysis):
|
|||
"""
|
||||
return self.kb.functions
|
||||
|
||||
@property
|
||||
def regions(self) -> list[tuple[int, int]]:
|
||||
"""
|
||||
The memory regions that this analysis covers. An empty list means it had nothing to scan.
|
||||
|
||||
:return: A sorted list of (start address, end address) tuples.
|
||||
"""
|
||||
return list(self._regions.items())
|
||||
|
||||
#
|
||||
# Methods
|
||||
#
|
||||
|
|
@ -891,6 +909,9 @@ class CFGBase(Analysis):
|
|||
if not memory_regions and not has_executable:
|
||||
memory_regions = [(start, start + len(backer)) for start, backer in self.project.loader.memory.backers()]
|
||||
|
||||
# A section or segment that maps no bytes, such as the empty .text of a data-only relocatable, is not a region.
|
||||
memory_regions = [(start, end) for start, end in memory_regions if end > start]
|
||||
|
||||
return sorted(memory_regions, key=lambda x: x[0])
|
||||
|
||||
def _addr_in_exec_memory_regions(self, addr):
|
||||
|
|
|
|||
|
|
@ -1139,7 +1139,7 @@ class CFGEmulated(ForwardAnalysis, CFGBase): # pylint: disable=abstract-method
|
|||
self._update_function_callsites(funcaddrs_do_not_return)
|
||||
|
||||
# Create all pending edges
|
||||
for _, edges in self._pending_edges.items():
|
||||
for edges in self._pending_edges.values():
|
||||
for src_node, dst_node, data in edges:
|
||||
self._graph_add_edge(src_node, dst_node, **data)
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ from angr.knowledge_plugins.cfg.spilling_cfg import block_key_to_addr, block_key
|
|||
from angr.knowledge_plugins.xrefs import XRef, XRefType
|
||||
from angr.misc.ux import once
|
||||
from angr.rustylib import SegmentList
|
||||
from angr.simos import SimWindows
|
||||
from angr.utils.constants import DEFAULT_STATEMENT
|
||||
from angr.utils.funcid import (
|
||||
is_function_likely_security_init_cookie,
|
||||
|
|
@ -869,6 +870,10 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
# mapping to all known thunks
|
||||
self._known_thunks = {}
|
||||
|
||||
# when True, jump/call targets loaded from registered read-only regions (e.g. PE IAT slots) are
|
||||
# constant-folded at lift time and consumed in _create_jobs without invoking indirect jump resolvers
|
||||
self._fold_ro_const_loads = False
|
||||
|
||||
self._initial_state = None
|
||||
self._next_addr: int | None = None
|
||||
|
||||
|
|
@ -1233,6 +1238,86 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
return repeating_length
|
||||
return 0
|
||||
|
||||
def _scan_for_fp_constants(self, start_addr: int, threshold: int = 4) -> int:
|
||||
"""
|
||||
Scan from a given address for a run of plausible floating-point constants.
|
||||
|
||||
A double-precision value qualifies when its biased exponent falls within a band covering magnitudes
|
||||
between 2 ** -64 and 2 ** 64, which is where constants in compiler- and libm-generated tables (polynomial
|
||||
coefficients, logarithm and trigonometry tables, etc.) almost always live. Code bytes rarely produce
|
||||
multiple consecutive qualifying values.
|
||||
|
||||
Single-precision values are detected as well, but with a tighter magnitude band (2 ** -32 to 2 ** 32) and
|
||||
twice the run-length requirement: an 8-bit exponent in a 4-byte value is a much weaker signal than an
|
||||
11-bit exponent in an 8-byte value, and anything looser starts matching real code.
|
||||
|
||||
:param start_addr: The address to start scanning from.
|
||||
:param threshold: The minimum number of consecutive qualifying double-precision values.
|
||||
:return: The total size in bytes of the qualifying values, or 0 if not enough values are found.
|
||||
"""
|
||||
|
||||
for size, exp_shift, exp_mask, exp_lo, exp_hi, min_count in (
|
||||
(8, 52, 0x7FF, 959, 1087, threshold), # doubles: 1023 +/- 64
|
||||
(4, 23, 0xFF, 95, 159, threshold * 2), # floats: 127 +/- 32
|
||||
):
|
||||
addr = start_addr
|
||||
fp_count = 0
|
||||
first_val = None
|
||||
has_multiple_values = False
|
||||
|
||||
uniform_mul = ((1 << (size * 8)) - 1) // 0xFF
|
||||
|
||||
while self._inside_regions(addr):
|
||||
val = self._fast_memory_load_pointer(addr, size=size)
|
||||
if val is None:
|
||||
break
|
||||
if val == (val & 0xFF) * uniform_mul:
|
||||
# all bytes are identical: this is filler (e.g., 0xCC padding or "????", whose bit patterns
|
||||
# carry in-band exponents), not a constant
|
||||
break
|
||||
exponent = (val >> exp_shift) & exp_mask
|
||||
if not exp_lo <= exponent <= exp_hi:
|
||||
break
|
||||
if first_val is None:
|
||||
first_val = val
|
||||
elif val != first_val:
|
||||
has_multiple_values = True
|
||||
fp_count += 1
|
||||
addr += size
|
||||
|
||||
# a run of one repeated value carries no table evidence
|
||||
if fp_count >= min_count and has_multiple_values:
|
||||
return fp_count * size
|
||||
return 0
|
||||
|
||||
def _scan_for_monotonic_byte_ramp(self, start_addr: int, threshold: int = 16) -> int:
|
||||
"""
|
||||
Scan from a given address for a run of monotonically increasing bytes, where each byte equals the previous
|
||||
byte plus one, modulo 256. Character case-conversion and translation tables are laid out this way.
|
||||
|
||||
:param start_addr: The address to start scanning from.
|
||||
:param threshold: The minimum run length.
|
||||
:return: The length of the run, or 0 if the run is shorter than threshold.
|
||||
"""
|
||||
|
||||
addr = start_addr
|
||||
last_byte = None
|
||||
ramp_length = 0
|
||||
|
||||
while self._inside_regions(addr):
|
||||
val = self._load_a_byte_as_int(addr)
|
||||
if val is None:
|
||||
break
|
||||
if last_byte is not None and val != (last_byte + 1) & 0xFF:
|
||||
break
|
||||
last_byte = val
|
||||
ramp_length += 1
|
||||
addr += 1
|
||||
|
||||
if ramp_length >= threshold:
|
||||
return ramp_length
|
||||
return 0
|
||||
|
||||
def _scan_for_consecutive_pointers(self, start_addr: int, threshold: int = 2) -> int:
|
||||
"""
|
||||
Scan from a given address and determine if there are at least `threshold` of pointers.
|
||||
|
|
@ -1343,11 +1428,19 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
)
|
||||
start_addr += pointer_length
|
||||
|
||||
elif start_addr <= 0x100000:
|
||||
# for high addresses, all pointers have been found in _scan_for_consecutive_pointers() because we
|
||||
# set threshold there to 1
|
||||
threshold = 4
|
||||
pointer_count = self._scan_for_mixed_pointers(start_addr, threshold=threshold, window=6)
|
||||
else:
|
||||
if start_addr <= 0x100000:
|
||||
# for low addresses, in-object values are common false positives, so
|
||||
# _scan_for_consecutive_pointers() ran with a high threshold and may have missed
|
||||
# non-consecutive pointers; require a high pointer density here
|
||||
threshold, window = 4, 6
|
||||
else:
|
||||
# for high addresses, all consecutive pointers have been found in
|
||||
# _scan_for_consecutive_pointers() because we set threshold there to 1. what remains are
|
||||
# interleaved tables (e.g., alternating value-pointer pairs), which have at most window // 2
|
||||
# pointers; use a wider window with the same evidence requirement
|
||||
threshold, window = 4, 8
|
||||
pointer_count = self._scan_for_mixed_pointers(start_addr, threshold=threshold, window=window)
|
||||
pointer_length = pointer_count * self.project.arch.bytes
|
||||
|
||||
if pointer_length:
|
||||
|
|
@ -1359,24 +1452,54 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
start_addr += pointer_length
|
||||
|
||||
if not matched_something:
|
||||
# find strings
|
||||
# find floating-point constant tables; this must run before the string and repeating-zero scans
|
||||
# because the low mantissa bytes of table entries are frequently zero or incidentally printable,
|
||||
# which would misphase the table. since scanning misclassified code often dumps us in the middle
|
||||
# of a table entry, probe the next 4- and 8-byte boundaries as well.
|
||||
fp_addr_4 = start_addr + (-start_addr % 4)
|
||||
fp_addr_8 = start_addr + (-start_addr % 8)
|
||||
for fp_addr in (fp_addr_4,) if fp_addr_4 == fp_addr_8 else (fp_addr_4, fp_addr_8):
|
||||
fp_length = self._scan_for_fp_constants(fp_addr)
|
||||
if fp_length:
|
||||
matched_something = True
|
||||
if fp_addr > start_addr:
|
||||
self._seg_list.occupy(start_addr, fp_addr - start_addr, "alignment")
|
||||
self.model.memory_data[start_addr] = MemoryData(
|
||||
start_addr, fp_addr - start_addr, MemoryDataSort.Alignment
|
||||
)
|
||||
self._seg_list.occupy(fp_addr, fp_length, "fp")
|
||||
self.model.memory_data[fp_addr] = MemoryData(fp_addr, fp_length, MemoryDataSort.FloatingPoint)
|
||||
start_addr = fp_addr + fp_length
|
||||
break
|
||||
|
||||
if not matched_something:
|
||||
# find strings; tolerate a single leading null byte, which is usually the leftover of a multi-null
|
||||
# string separator (string scans only consume one null terminator of the preceding string, and a
|
||||
# single remaining null byte is not caught by the repeating-zero scan below)
|
||||
leading_nulls = 1 if self._load_a_byte_as_int(start_addr) == 0 else 0
|
||||
str_addr = start_addr + leading_nulls
|
||||
is_widestring = False
|
||||
string_length = self._scan_for_printable_strings(start_addr)
|
||||
string_length = self._scan_for_printable_strings(str_addr)
|
||||
if string_length == 0:
|
||||
is_widestring = True
|
||||
string_length = self._scan_for_printable_widestrings(start_addr)
|
||||
string_length = self._scan_for_printable_widestrings(str_addr)
|
||||
|
||||
if string_length:
|
||||
matched_something = True
|
||||
self._seg_list.occupy(start_addr, string_length, "string")
|
||||
if leading_nulls:
|
||||
self._seg_list.occupy(start_addr, leading_nulls, "alignment")
|
||||
self.model.memory_data[start_addr] = MemoryData(
|
||||
start_addr, leading_nulls, MemoryDataSort.Alignment
|
||||
)
|
||||
self._seg_list.occupy(str_addr, string_length, "string")
|
||||
md = MemoryData(
|
||||
start_addr,
|
||||
str_addr,
|
||||
string_length,
|
||||
MemoryDataSort.String if not is_widestring else MemoryDataSort.UnicodeString,
|
||||
)
|
||||
md.fill_content(self.project.loader)
|
||||
self.model.memory_data[start_addr] = md
|
||||
start_addr += string_length
|
||||
self.model.memory_data[str_addr] = md
|
||||
start_addr = str_addr + string_length
|
||||
|
||||
if not matched_something and self.project.arch.name in {"X86", "AMD64"}:
|
||||
cc_length = self._scan_for_repeating_bytes(start_addr, 0xCC, threshold=1)
|
||||
|
|
@ -1416,6 +1539,14 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
)
|
||||
start_addr += repeating_byte_length
|
||||
|
||||
# a long run of monotonically increasing bytes is a character or translation table, not code
|
||||
ramp_length = self._scan_for_monotonic_byte_ramp(start_addr, threshold=16)
|
||||
if ramp_length:
|
||||
matched_something = True
|
||||
self._seg_list.occupy(start_addr, ramp_length, "nodecode")
|
||||
self.model.memory_data[start_addr] = MemoryData(start_addr, ramp_length, MemoryDataSort.Unknown)
|
||||
start_addr += ramp_length
|
||||
|
||||
if not matched_something:
|
||||
# umm now it's probably code
|
||||
break
|
||||
|
|
@ -1461,28 +1592,7 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
def _nodecode_bytes_ratio(self, cutoff_addr: int, window_size: int) -> float:
|
||||
if cutoff_addr - 1 < 0:
|
||||
return 0.0
|
||||
idx = self._seg_list.search(cutoff_addr - 1)
|
||||
if idx is None or idx >= len(self._seg_list):
|
||||
return 0.0
|
||||
segment = self._seg_list[idx]
|
||||
if segment.sort != "nodecode":
|
||||
return 0.0
|
||||
|
||||
total_bytes = 0
|
||||
nodecode_bytes = 0
|
||||
while idx >= 0:
|
||||
segment = self._seg_list[idx]
|
||||
if segment.sort == "nodecode":
|
||||
nodecode_bytes += segment.size
|
||||
total_bytes += segment.size
|
||||
if total_bytes >= window_size:
|
||||
break
|
||||
idx -= 1
|
||||
|
||||
if total_bytes < window_size:
|
||||
return 0.0
|
||||
|
||||
return nodecode_bytes / total_bytes
|
||||
return self._seg_list.sort_ratio_backwards(cutoff_addr - 1, window_size, "nodecode")
|
||||
|
||||
def _next_code_addr_smart(self) -> int | None:
|
||||
# in the smart scanning mode, if there are more than N consecutive no-decode cases, we skip an entire window of
|
||||
|
|
@ -3133,6 +3243,34 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
|
||||
return entries
|
||||
|
||||
def _resolve_const_folded_next(self, irsb: pyvex.IRSB | None, jumpkind: str) -> int | None:
|
||||
"""
|
||||
Check whether the jump/call target of this block was constant-folded from a registered read-only region
|
||||
at lift time (recorded in IRSB.const_vals), e.g. an import call through the IAT (or delay-load IAT) in a PE
|
||||
binary. This reproduces the decision of the timeless load-based resolvers (AMD64PeIatResolver and
|
||||
MemoryLoadResolver) without re-lifting the block or dispatching the resolvers: the loaded pointer is the
|
||||
same value they would read, and it is accepted when it is a valid jump target (executable or hooked), which
|
||||
is exactly MemoryLoadResolver's ``_is_target_valid`` criterion (a superset of AMD64PeIatResolver's hooked
|
||||
check). Everything else falls back to the regular indirect jump resolution logic.
|
||||
|
||||
:param irsb: The (possibly statement-less) IRSB of the block.
|
||||
:param jumpkind: The jumpkind of the default exit.
|
||||
:return: The resolved target, or None if unavailable.
|
||||
"""
|
||||
if not self._fold_ro_const_loads or irsb is None:
|
||||
return None
|
||||
if jumpkind not in ("Ijk_Call", "Ijk_Boring"):
|
||||
return None
|
||||
if not irsb.const_vals or not isinstance(irsb.next, pyvex.IRExpr.RdTmp):
|
||||
return None
|
||||
next_tmp = irsb.next.tmp
|
||||
for cv in irsb.const_vals:
|
||||
if cv.tmp == next_tmp:
|
||||
if self._addr_in_exec_memory_regions(cv.value) or self.project.is_hooked(cv.value):
|
||||
return cv.value
|
||||
return None
|
||||
return None
|
||||
|
||||
def _create_jobs(
|
||||
self,
|
||||
target: Any,
|
||||
|
|
@ -3218,14 +3356,24 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
jumpkind in ("Ijk_Boring", "Ijk_Call", "Ijk_InvalICache") or jumpkind.startswith("Ijk_Sys")
|
||||
):
|
||||
# This is an indirect jump. Try to resolve it.
|
||||
# FIXME: in some cases, a statementless irsb will be missing its instr addresses
|
||||
# and this next part will fail. Use the real IRSB instead
|
||||
irsb = self._lift(cfg_node.addr, size=cfg_node.size).vex
|
||||
assert irsb is not None
|
||||
cfg_node.instruction_addrs = InsAddrList.from_addr_list(irsb.instruction_addresses)
|
||||
resolved, resolved_targets, ij = self._indirect_jump_encountered(
|
||||
addr, cfg_node, irsb, current_function_addr, stmt_idx
|
||||
)
|
||||
# fast path: the target may have been constant-folded from a read-only region at lift time
|
||||
# (e.g. an AMD64 PE IAT slot); consuming it here avoids re-lifting the block and running the
|
||||
# indirect jump resolvers
|
||||
folded_target = self._resolve_const_folded_next(irsb, jumpkind)
|
||||
if folded_target is not None:
|
||||
# the statement-less irsb already carries the instruction addresses that the resolver path
|
||||
# would recompute from a re-lift
|
||||
cfg_node.instruction_addrs = InsAddrList.from_addr_list(irsb.instruction_addresses)
|
||||
resolved, resolved_targets, ij = True, {folded_target}, None
|
||||
else:
|
||||
# FIXME: in some cases, a statementless irsb will be missing its instr addresses
|
||||
# and this next part will fail. Use the real IRSB instead
|
||||
irsb = self._lift(cfg_node.addr, size=cfg_node.size).vex
|
||||
assert irsb is not None
|
||||
cfg_node.instruction_addrs = InsAddrList.from_addr_list(irsb.instruction_addresses)
|
||||
resolved, resolved_targets, ij = self._indirect_jump_encountered(
|
||||
addr, cfg_node, irsb, current_function_addr, stmt_idx
|
||||
)
|
||||
if resolved:
|
||||
for resolved_target in resolved_targets:
|
||||
if jumpkind == "Ijk_Call":
|
||||
|
|
@ -3596,7 +3744,10 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
target_func_addr = node.function_address
|
||||
# case 2: if the source instruction is the first instruction of the current function, has only one branch
|
||||
# to the target address, and is a jump (Ijk_Boring, not a call), then the target address is likely the
|
||||
# start of another function
|
||||
# start of another function. A compiler may also begin a function with an
|
||||
# unconditional jump to an internal loop guard (loop rotation). When the loader
|
||||
# supplies a non-empty function symbol, its extent is stronger evidence than this
|
||||
# tail-jump heuristic: keep a target inside that extent in the current function.
|
||||
if (
|
||||
target_func_addr is None
|
||||
and len(src_node.instruction_addrs) == 1
|
||||
|
|
@ -3605,7 +3756,17 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
and all_successors is not None
|
||||
and len(all_successors) == 1
|
||||
):
|
||||
target_func_addr = target_addr
|
||||
current_symbol = self.project.loader.find_symbol(current_function_addr)
|
||||
current_symbol_size = getattr(current_symbol, "size", 0) or 0
|
||||
target_is_inside_current_symbol = (
|
||||
current_symbol is not None
|
||||
and current_symbol.is_function
|
||||
and current_symbol.rebased_addr == current_function_addr
|
||||
and current_symbol_size > 0
|
||||
and current_function_addr <= target_addr < current_function_addr + current_symbol_size
|
||||
)
|
||||
if not target_is_inside_current_symbol:
|
||||
target_func_addr = target_addr
|
||||
# last resort: the block probably belongs to the current function
|
||||
if target_func_addr is None:
|
||||
target_func_addr = current_function_addr
|
||||
|
|
@ -5193,6 +5354,30 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
self._ro_region_cdata_cache.append(content_buf)
|
||||
pyvex.pvc.register_readonly_region(section.vaddr, section.memsize, content_buf)
|
||||
|
||||
elif self.project.arch.name in {"AMD64", "X86"} and isinstance(self.project.simos, SimWindows):
|
||||
# register sections that hold jump/call targets so that rip-relative import calls and jumps
|
||||
# (call/jmp qword ptr [rip+disp]) can be constant-folded at lift time and resolved without a re-lift
|
||||
# and resolver dispatch:
|
||||
# - non-writable sections (e.g. .rdata, the bound IAT), and
|
||||
# - the delay-load import table (.didat): although writable, its slots are static during CFG recovery
|
||||
# and point to the delay-load thunks, which MemoryLoadResolver already resolves by reading them.
|
||||
# The folded value is only accepted when it is a valid jump target, so registering these regions cannot
|
||||
# introduce edges the timeless load resolvers would not also produce.
|
||||
self._ro_region_cdata_cache = []
|
||||
for section in self.project.loader.main_object.sections:
|
||||
register = (section.is_readable and not section.is_writable and section.memsize >= 8) or (
|
||||
section.name == ".didat" and section.is_readable and section.memsize >= 8
|
||||
)
|
||||
if register:
|
||||
try:
|
||||
content = self.project.loader.memory.load(section.vaddr, section.memsize)
|
||||
except KeyError:
|
||||
continue
|
||||
content_buf = pyvex.ffi.from_buffer(content)
|
||||
self._ro_region_cdata_cache.append(content_buf)
|
||||
pyvex.pvc.register_readonly_region(section.vaddr, section.memsize, content_buf)
|
||||
self._fold_ro_const_loads = bool(self._ro_region_cdata_cache)
|
||||
|
||||
def _lifter_deregister_readonly_regions(self):
|
||||
pyvex.pvc.deregister_all_readonly_regions()
|
||||
self._ro_region_cdata_cache = None
|
||||
|
|
@ -5456,6 +5641,7 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
collect_data_refs=True,
|
||||
strict_block_end=True,
|
||||
load_from_ro_regions=True,
|
||||
const_prop=self._fold_ro_const_loads,
|
||||
initial_regs=initial_regs,
|
||||
)
|
||||
irsb = lifted_block.vex_nostmt # may raise SimTranslationError
|
||||
|
|
@ -5500,6 +5686,7 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
collect_data_refs=True,
|
||||
strict_block_end=True,
|
||||
load_from_ro_regions=True,
|
||||
const_prop=self._fold_ro_const_loads,
|
||||
initial_regs=initial_regs,
|
||||
)
|
||||
irsb = lifted_block.vex_nostmt
|
||||
|
|
@ -5678,6 +5865,16 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
)
|
||||
irsb_string = irsb_string[: irsb.size]
|
||||
|
||||
if (
|
||||
cfg_job.job_type == CFGJobType.COMPLETE_SCANNING
|
||||
and addr == current_function_addr
|
||||
and irsb.jumpkind == "Ijk_NoDecode"
|
||||
):
|
||||
# linear sweep decided that this block is undecodable. because drop_bad_functions() will remove this
|
||||
# function anyway, we bail out early and mark the whole block as nodecode for performance.
|
||||
self._seg_list.occupy(real_addr, max(irsb.size, 1), "nodecode")
|
||||
return None, None, None, None
|
||||
|
||||
# Occupy the block in segment list
|
||||
if irsb is not None and irsb.size > 0:
|
||||
self._seg_list.occupy(real_addr, irsb.size, "code")
|
||||
|
|
|
|||
|
|
@ -341,7 +341,6 @@ class CFGFastSoot(CFGFast):
|
|||
addr = cfg_node.addr
|
||||
stmts_count = cfg_node.size
|
||||
else:
|
||||
addr = addr
|
||||
stmts_count = size
|
||||
|
||||
if addr is None:
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class AMD64PeIatResolver(IndirectJumpResolver):
|
|||
if jumpkind not in {"Ijk_Call", "Ijk_Boring"}:
|
||||
return False
|
||||
|
||||
insns = self.project.factory.block(addr).capstone.insns
|
||||
insns = self.project.factory.block(addr, size=block.size).capstone.insns
|
||||
if not insns:
|
||||
return False
|
||||
if not insns[-1].insn.operands:
|
||||
|
|
@ -36,7 +36,7 @@ class AMD64PeIatResolver(IndirectJumpResolver):
|
|||
return bool(opnd.type == X86_OP_MEM and opnd.mem.disp and opnd.mem.base == X86_REG_RIP and opnd.mem.index == 0)
|
||||
|
||||
def resolve(self, cfg, addr, func_addr, block, jumpkind, func_graph_complete: bool = True, **kwargs): # pylint:disable=unused-argument
|
||||
call_insn = self.project.factory.block(addr).capstone.insns[-1].insn
|
||||
call_insn = self.project.factory.block(addr, size=block.size).capstone.insns[-1].insn
|
||||
addr = (call_insn.disp + call_insn.address + call_insn.size) & 0xFFFF_FFFF_FFFF_FFFF
|
||||
target = cfg._fast_memory_load_pointer(addr)
|
||||
if target is None:
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
import struct
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from angr.analyses.analysis import Analysis, register_analysis
|
|||
from angr.analyses.calling_convention import CallingConventionAnalysis
|
||||
from angr.analyses.cfg import CFGFast
|
||||
from angr.analyses.variable_recovery import VariableRecoveryFast
|
||||
from angr.errors import AngrRuntimeError
|
||||
from angr.knowledge_plugins.cfg import CFGModel
|
||||
from angr.knowledge_plugins.functions.function import PrototypeSource
|
||||
from angr.simos import SimWindows
|
||||
|
|
@ -34,6 +35,10 @@ _l = logging.getLogger(name=__name__)
|
|||
|
||||
_mp_context = mp_context()
|
||||
|
||||
# How long (in seconds) the result collector keeps polling the results queue after it observes that every worker
|
||||
# process has exited.
|
||||
DEAD_WORKER_GRACE_PERIOD = 5.0
|
||||
|
||||
|
||||
class CallingConventionAnalysisMode(Enum):
|
||||
"""
|
||||
|
|
@ -295,6 +300,9 @@ class CompleteCallingConventionsAnalysis(Analysis):
|
|||
self._update_progress(0)
|
||||
idx = 0
|
||||
assert self._results_lock is not None
|
||||
# the timestamp when we first noticed that all workers had exited.
|
||||
# None while at least one worker is still alive.
|
||||
all_workers_dead_since: float | None = None
|
||||
while idx < total_funcs:
|
||||
try:
|
||||
with self._results_lock:
|
||||
|
|
@ -302,9 +310,26 @@ class CompleteCallingConventionsAnalysis(Analysis):
|
|||
True, timeout=0.01
|
||||
)
|
||||
except queue.Empty:
|
||||
# No result is available right now.
|
||||
# If all workers have exited then no result will ever become available, and looping here would
|
||||
# hang forever (angr #6529). Detect that and fail loudly.
|
||||
if any(proc.is_alive() for proc in procs):
|
||||
all_workers_dead_since = None
|
||||
elif all_workers_dead_since is None:
|
||||
all_workers_dead_since = time.time()
|
||||
elif time.time() - all_workers_dead_since >= DEAD_WORKER_GRACE_PERIOD:
|
||||
exitcodes = ", ".join(f"{proc.name}: {proc.exitcode}" for proc in procs)
|
||||
raise AngrRuntimeError(
|
||||
f"All {len(procs)} CompleteCallingConventions worker processes exited before the "
|
||||
f"analysis finished; only {idx} of {total_funcs} functions were analyzed. Worker exit "
|
||||
f"codes: {exitcodes}."
|
||||
) from None
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# we made progress, so any previously observed all-dead state is no longer interesting
|
||||
all_workers_dead_since = None
|
||||
|
||||
func = self.kb.functions.get_by_addr(func_addr)
|
||||
if cc is not None or proto is not None:
|
||||
func.calling_convention = cc
|
||||
|
|
@ -376,8 +401,13 @@ class CompleteCallingConventionsAnalysis(Analysis):
|
|||
except Exception: # pylint:disable=broad-except
|
||||
_l.error("Worker %d: Exception occurred during _analyze_core().", worker_id, exc_info=True)
|
||||
cc, proto, proto_libname, proto_source, varman = None, None, None, None, None
|
||||
with self._results_lock:
|
||||
self._results.put((func_addr, cc, proto, proto_libname, proto_source, varman))
|
||||
try:
|
||||
with self._results_lock:
|
||||
self._results.put((func_addr, cc, proto, proto_libname, proto_source, varman))
|
||||
except Exception: # pylint:disable=broad-except
|
||||
_l.error(
|
||||
"Worker %d: Failed to report the result for function %#x.", worker_id, func_addr, exc_info=True
|
||||
)
|
||||
|
||||
def _analyze_core(
|
||||
self, func_addr: int
|
||||
|
|
|
|||
|
|
@ -756,7 +756,7 @@ class DDG(Analysis):
|
|||
matched = True
|
||||
except (SimUnsatError, SimSolverModeError, ZeroDivisionError):
|
||||
# ignore
|
||||
matched = matched
|
||||
pass
|
||||
if not matched:
|
||||
break
|
||||
|
||||
|
|
@ -1017,7 +1017,7 @@ class DDG(Analysis):
|
|||
self._stmt_graph_annotate_edges(self._register_edges[reg_offset], subtype="mem_addr")
|
||||
reg_variable = SimRegisterVariable(reg_offset, self._get_register_size(reg_offset))
|
||||
prev_defs = self._def_lookup(reg_variable)
|
||||
for loc, _ in prev_defs.items():
|
||||
for loc in prev_defs:
|
||||
v = ProgramVariable(reg_variable, loc, arch=self.project.arch)
|
||||
self._data_graph_add_edge(v, prog_var, type="mem_addr")
|
||||
|
||||
|
|
@ -1039,7 +1039,7 @@ class DDG(Analysis):
|
|||
self._stmt_graph_annotate_edges(self._register_edges[reg_offset], subtype="mem_data")
|
||||
reg_variable = SimRegisterVariable(reg_offset, self._get_register_size(reg_offset))
|
||||
prev_defs = self._def_lookup(reg_variable)
|
||||
for loc, _ in prev_defs.items():
|
||||
for loc in prev_defs:
|
||||
v = ProgramVariable(reg_variable, loc, arch=self.project.arch)
|
||||
self._data_graph_add_edge(v, prog_var, type="mem_data")
|
||||
|
||||
|
|
@ -1156,9 +1156,11 @@ class DDG(Analysis):
|
|||
if not action.reg_deps and not action.tmp_deps:
|
||||
# moving a constant into the register
|
||||
# try to parse out the constant from statement
|
||||
const_variable = SimConstantVariable(size=1)
|
||||
if statement is not None and isinstance(statement.data, pyvex.IRExpr.Const):
|
||||
const_variable = SimConstantVariable(value=statement.data.con.value, size=statement.data.con.size)
|
||||
else:
|
||||
# use a default value of 0 if we cannot find the constant
|
||||
const_variable = SimConstantVariable(1, value=0)
|
||||
const_pv = ProgramVariable(const_variable, location, arch=self.project.arch)
|
||||
self._data_graph_add_edge(const_pv, pv)
|
||||
|
||||
|
|
@ -1229,7 +1231,7 @@ class DDG(Analysis):
|
|||
if not action.tmp_deps and not self._variables_per_statement and not ast:
|
||||
# read in a constant
|
||||
# try to parse out the constant from statement
|
||||
const_variable = SimConstantVariable(size=1)
|
||||
const_variable = SimConstantVariable(size=1, value=0) # default value if we can't find the constant
|
||||
if statement is not None:
|
||||
if isinstance(statement, pyvex.IRStmt.Dirty):
|
||||
l.warning("Dirty statements are not supported in DDG for now.")
|
||||
|
|
@ -1497,7 +1499,7 @@ class DDG(Analysis):
|
|||
# Group all dependencies first
|
||||
|
||||
block_addr_to_func = {}
|
||||
for _, func in self.kb.functions.items():
|
||||
for func in self.kb.functions.values():
|
||||
for block in func.blocks:
|
||||
block_addr_to_func[block.addr] = func
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from collections.abc import Container, Iterable
|
||||
from collections.abc import Iterable
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
|
|
@ -41,8 +41,8 @@ from angr.ailment.statement import (
|
|||
WeakAssignment,
|
||||
)
|
||||
from angr.analyses.analysis import AnalysesHub, Analysis
|
||||
from angr.analyses.s_propagator import SPropagatorAnalysis
|
||||
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitionsAnalysis
|
||||
from angr.analyses.s_propagator import SPropagator
|
||||
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitions
|
||||
from angr.code_location import AILCodeLocation
|
||||
from angr.errors import AngrRuntimeError
|
||||
from angr.knowledge_plugins.functions.function import Function
|
||||
|
|
@ -80,6 +80,58 @@ _l = logging.getLogger(__name__)
|
|||
_VERIFY_INCREMENTAL_RD = os.environ.get("VERIFY_INCREMENTAL_RD", "").lower() not in {"", "0", "no", "false"}
|
||||
|
||||
|
||||
def _strongly_connected_components(succs: dict[int, set[int]]):
|
||||
"""
|
||||
Iterative Tarjan SCC over a plain adjacency map. Yields sets of node IDs, like
|
||||
networkx.strongly_connected_components().
|
||||
"""
|
||||
index_of: dict[int, int] = {}
|
||||
lowlink: dict[int, int] = {}
|
||||
on_stack: set[int] = set()
|
||||
stack: list[int] = []
|
||||
counter = 0
|
||||
|
||||
for root, root_succs in succs.items():
|
||||
if root in index_of:
|
||||
continue
|
||||
work = [(root, iter(root_succs))]
|
||||
index_of[root] = lowlink[root] = counter
|
||||
counter += 1
|
||||
stack.append(root)
|
||||
on_stack.add(root)
|
||||
|
||||
while work:
|
||||
node, it = work[-1]
|
||||
advanced = False
|
||||
for succ in it:
|
||||
if succ not in index_of:
|
||||
index_of[succ] = lowlink[succ] = counter
|
||||
counter += 1
|
||||
stack.append(succ)
|
||||
on_stack.add(succ)
|
||||
work.append((succ, iter(succs[succ])))
|
||||
advanced = True
|
||||
break
|
||||
if succ in on_stack and index_of[succ] < lowlink[node]:
|
||||
lowlink[node] = index_of[succ]
|
||||
if advanced:
|
||||
continue
|
||||
|
||||
work.pop()
|
||||
if work:
|
||||
parent = work[-1][0]
|
||||
lowlink[parent] = min(lowlink[parent], lowlink[node])
|
||||
if lowlink[node] == index_of[node]:
|
||||
scc = set()
|
||||
while True:
|
||||
member = stack.pop()
|
||||
on_stack.discard(member)
|
||||
scc.add(member)
|
||||
if member == node:
|
||||
break
|
||||
yield scc
|
||||
|
||||
|
||||
class HasVVarNotification(Exception):
|
||||
"""
|
||||
Notifies the existence of a VirtualVariable.
|
||||
|
|
@ -189,7 +241,7 @@ class AILSimplifier(Analysis):
|
|||
self.func = func
|
||||
self.func_graph = func_graph
|
||||
self._reaching_definitions: SRDAModel | None = None
|
||||
self._propagator: SPropagatorAnalysis | None = None
|
||||
self._propagator: SPropagator | None = None
|
||||
|
||||
self._remove_dead_memdefs = remove_dead_memdefs
|
||||
self._stackarg_offset_manager = stackarg_offset_manager
|
||||
|
|
@ -208,6 +260,11 @@ class AILSimplifier(Analysis):
|
|||
self._arg_vvars = arg_vvars
|
||||
self._avoid_vvar_ids = avoid_vvar_ids if avoid_vvar_ids is not None else set()
|
||||
self._propagator_dead_vvar_ids: set[int] = set()
|
||||
# per-block cache of dirty/ccall-defined vvar IDs, keyed by block key, validated by block identity
|
||||
self._dirty_vvar_scan_cache: dict[tuple[int, int | None], tuple[Block, set[int]]] = {}
|
||||
# only set to True when any simplification pass has modified the graph or updated any blocks.
|
||||
# skips _remove_dead_assignments if this flag is False.
|
||||
self._should_eliminate_dead_assignments: bool = True
|
||||
|
||||
self._calls_to_remove: set[AILCodeLocation] = set()
|
||||
self._assignments_to_remove: set[AILCodeLocation] = set()
|
||||
|
|
@ -335,33 +392,32 @@ class AILSimplifier(Analysis):
|
|||
|
||||
AILGraphWalker(self.func_graph, _handler, replace_nodes=True).walk()
|
||||
self.blocks = {}
|
||||
self._should_eliminate_dead_assignments = True
|
||||
|
||||
def _compute_reaching_definitions(self) -> SRDAModel:
|
||||
# Computing reaching definitions or return the cached one
|
||||
if self._reaching_definitions is not None:
|
||||
return self._reaching_definitions
|
||||
func_args = {vvar for vvar, _ in self._arg_vvars.values()} if self._arg_vvars else set()
|
||||
rd = (
|
||||
self.project.analyses[SReachingDefinitionsAnalysis]
|
||||
.prep()(
|
||||
subject=self.func,
|
||||
func_graph=self.func_graph,
|
||||
func_args=func_args,
|
||||
use_callee_saved_regs_at_return=self._use_callee_saved_regs_at_return,
|
||||
# track_tmps=True,
|
||||
)
|
||||
.model
|
||||
)
|
||||
rd = SReachingDefinitions(
|
||||
self.project,
|
||||
subject=self.func,
|
||||
func_graph=self.func_graph,
|
||||
func_args=func_args,
|
||||
use_callee_saved_regs_at_return=self._use_callee_saved_regs_at_return,
|
||||
# track_tmps=True,
|
||||
).model
|
||||
self._reaching_definitions = rd
|
||||
return rd
|
||||
|
||||
@timethis
|
||||
def _compute_propagation(self) -> SPropagatorAnalysis:
|
||||
def _compute_propagation(self) -> SPropagator:
|
||||
# Propagate expressions or return the existing result
|
||||
if self._propagator is not None:
|
||||
return self._propagator
|
||||
func_args = {vvar for vvar, _ in self._arg_vvars.values()} if self._arg_vvars else set()
|
||||
prop = self.project.analyses[SPropagatorAnalysis].prep(fail_fast=self._fail_fast)(
|
||||
prop = SPropagator(
|
||||
self.project,
|
||||
subject=self.func,
|
||||
func_graph=self.func_graph,
|
||||
func_args=func_args,
|
||||
|
|
@ -374,6 +430,7 @@ class AILSimplifier(Analysis):
|
|||
)
|
||||
self._propagator = prop
|
||||
self._propagator_dead_vvar_ids = prop.dead_vvar_ids
|
||||
self._should_eliminate_dead_assignments = True
|
||||
return prop
|
||||
|
||||
@timethis
|
||||
|
|
@ -527,8 +584,8 @@ class AILSimplifier(Analysis):
|
|||
vvar, simvar = self._arg_vvars[func_arg_idx]
|
||||
if vvar.varid == new_vvar.varid:
|
||||
simvar_new = simvar.copy()
|
||||
simvar_new._hash = None
|
||||
simvar_new.size = new_vvar.size
|
||||
simvar_new.clear_hash()
|
||||
self._arg_vvars[func_arg_idx] = new_vvar, simvar_new
|
||||
|
||||
return narrowed
|
||||
|
|
@ -933,11 +990,12 @@ class AILSimplifier(Analysis):
|
|||
}
|
||||
reps = filtered_reps
|
||||
|
||||
r, new_block = BlockSimplifier._replace_and_build(
|
||||
r, new_block = BlockSimplifier.replace_and_build(
|
||||
block, reps, self._ail_manager, gp=self._gp, replace_loads=replace_loads
|
||||
)
|
||||
replaced |= r
|
||||
self.blocks[block] = new_block
|
||||
if r:
|
||||
self.blocks[block] = new_block
|
||||
|
||||
if replaced:
|
||||
# blocks have been rebuilt - expression propagation results are no longer reliable
|
||||
|
|
@ -1099,6 +1157,9 @@ class AILSimplifier(Analysis):
|
|||
# out-of-date
|
||||
updated_locs: set[AILCodeLocation] = set()
|
||||
|
||||
# built on-demand
|
||||
stack_defs_by_offset: dict[int, list[Definition[atoms.VirtualVariable, AILCodeLocation]]] | None = None
|
||||
|
||||
for _, atom in sorted_loc_and_atoms:
|
||||
eqs = equivalences[atom]
|
||||
filtered_eqs: list[tuple[Equivalence, VirtualVariable, bool]] = []
|
||||
|
|
@ -1169,14 +1230,9 @@ class AILSimplifier(Analysis):
|
|||
rd = self._compute_reaching_definitions()
|
||||
the_def = None
|
||||
if to_replace_is_def:
|
||||
# find defs
|
||||
defs: Container[Definition[atoms.VirtualVariable, AILCodeLocation]] = []
|
||||
for def_ in rd.all_definitions:
|
||||
if def_.atom.varid == to_replace.varid:
|
||||
defs.append(def_)
|
||||
if len(defs) != 1:
|
||||
the_def = rd.get_definition_by_varid(to_replace.varid)
|
||||
if the_def is None:
|
||||
continue
|
||||
the_def = defs[0]
|
||||
else:
|
||||
# find uses
|
||||
defs = rd.get_uses_by_location(eq.codeloc)
|
||||
|
|
@ -1202,38 +1258,42 @@ class AILSimplifier(Analysis):
|
|||
# (a) the on-stack or in-register copy of it has never been modified in this function
|
||||
# (b) the function argument register has never been updated.
|
||||
# TODO: we may loosen requirement (b) once we have real register versioning in AIL.
|
||||
defs = [def_ for def_ in rd.all_definitions if def_.codeloc == eq.codeloc]
|
||||
defs = rd.get_defs_by_location(eq.codeloc)
|
||||
all_uses_with_def = None
|
||||
replace_with = None
|
||||
remove_initial_assignment = None
|
||||
def_eq_rel = DefEqRelation.DEF_IS_FUNCARG
|
||||
|
||||
if defs and len(defs) == 1:
|
||||
arg_copy_def = defs[0]
|
||||
if len(defs) == 1:
|
||||
arg_copy_def = next(iter(defs))
|
||||
if (isinstance(arg_copy_def.atom, atoms.VirtualVariable) and arg_copy_def.atom.was_stack) or (
|
||||
isinstance(arg_copy_def.atom, atoms.VirtualVariable) and arg_copy_def.atom.was_reg
|
||||
):
|
||||
# found the copied definition (either a stack variable or a register variable)
|
||||
|
||||
# Make sure there is no other write to this stack location if the copy is a stack variable
|
||||
if (
|
||||
isinstance(arg_copy_def.atom, atoms.VirtualVariable)
|
||||
and arg_copy_def.atom.was_stack
|
||||
and any(
|
||||
(def_ != arg_copy_def and def_.atom.stack_offset == arg_copy_def.atom.stack_offset)
|
||||
for def_ in rd.all_definitions
|
||||
if isinstance(def_.atom, atoms.VirtualVariable) and def_.atom.was_stack
|
||||
)
|
||||
):
|
||||
continue
|
||||
if isinstance(arg_copy_def.atom, atoms.VirtualVariable) and arg_copy_def.atom.was_stack:
|
||||
if stack_defs_by_offset is None:
|
||||
stack_defs_by_offset = defaultdict(list)
|
||||
for def_ in rd.all_definitions:
|
||||
if def_.atom.was_stack:
|
||||
stack_defs_by_offset[def_.atom.stack_offset].append(def_)
|
||||
if any(
|
||||
def_ != arg_copy_def
|
||||
for def_ in stack_defs_by_offset.get(arg_copy_def.atom.stack_offset, ())
|
||||
):
|
||||
continue
|
||||
|
||||
# Make sure the register is never updated across this function
|
||||
if any(
|
||||
(def_ != the_def and def_.atom == the_def.atom)
|
||||
for def_ in rd.all_definitions
|
||||
if isinstance(def_.atom, atoms.VirtualVariable)
|
||||
and def_.atom.was_reg
|
||||
and rd.get_vvar_uses(def_.atom)
|
||||
# Make sure the register is never updated across this function. Only the definition of
|
||||
# the_def's own vvar id can have an equal atom (atom equality is (varid, size)), and SSA gives
|
||||
# that id exactly one definition, so this is a single lookup rather than a full scan.
|
||||
other_def = rd.get_definition_by_varid(the_def.atom.varid)
|
||||
if (
|
||||
other_def is not None
|
||||
and other_def != the_def
|
||||
and other_def.atom == the_def.atom
|
||||
and other_def.atom.was_reg
|
||||
and rd.get_vvar_uses(other_def.atom)
|
||||
):
|
||||
continue
|
||||
|
||||
|
|
@ -1269,13 +1329,7 @@ class AILSimplifier(Analysis):
|
|||
def_eq_rel = DefEqRelation.DEF_EQ_SAME_BLOCK
|
||||
else:
|
||||
# the definition is in the predecessor block of the eq
|
||||
eq_block = next(
|
||||
iter(
|
||||
bb
|
||||
for bb in self.func_graph
|
||||
if bb.addr == eq.codeloc.block_addr and bb.idx == eq.codeloc.block_idx
|
||||
)
|
||||
)
|
||||
eq_block = addr_and_idx_to_block[(eq.codeloc.block_addr, eq.codeloc.block_idx)]
|
||||
eq_block_preds = set(self.func_graph.predecessors(eq_block))
|
||||
if not any(
|
||||
pred.addr == the_def.codeloc.block_addr and pred.idx == the_def.codeloc.block_idx
|
||||
|
|
@ -1738,13 +1792,6 @@ class AILSimplifier(Analysis):
|
|||
if isinstance(eq.atom0, VirtualVariable):
|
||||
src = used_expr
|
||||
dst: Expression = call.copy()
|
||||
|
||||
if isinstance(dst, SideEffectStatement):
|
||||
dst_bits = dst.ret_expr.bits if dst.ret_expr is not None else dst.bits
|
||||
# extract the Call expression from the SideEffectStatement
|
||||
dst = dst.expr
|
||||
dst.bits = dst_bits
|
||||
|
||||
if src.bits != dst.bits and not eq.is_weakassignment:
|
||||
dst = Convert(
|
||||
self._ail_manager.next_atom(),
|
||||
|
|
@ -1849,6 +1896,14 @@ class AILSimplifier(Analysis):
|
|||
|
||||
@timethis
|
||||
def _iteratively_remove_dead_assignments(self) -> bool:
|
||||
if (
|
||||
not self._should_eliminate_dead_assignments
|
||||
and not self.blocks
|
||||
and not self._calls_to_remove
|
||||
and not self._assignments_to_remove
|
||||
):
|
||||
# nothing that _remove_dead_assignments() reads has changed since it last reported nothing to remove
|
||||
return False
|
||||
anything_removed = False
|
||||
while True:
|
||||
r, changed_block_keys = self._remove_dead_assignments()
|
||||
|
|
@ -1869,6 +1924,8 @@ class AILSimplifier(Analysis):
|
|||
# propagation results are no longer reliable after removing statements
|
||||
self._propagator = None
|
||||
|
||||
self._should_eliminate_dead_assignments = False
|
||||
|
||||
# NoOp placeholders are left in the graph and the reaching-definitions cache is kept valid: subsequent
|
||||
# simplification steps reuse it instead of rebuilding from scratch. The placeholders are compacted away once,
|
||||
# at the end of _simplify().
|
||||
|
|
@ -1891,16 +1948,13 @@ class AILSimplifier(Analysis):
|
|||
# rebuild on the current (NoOp-containing) graph.
|
||||
assert self._reaching_definitions is not None
|
||||
func_args = {vvar for vvar, _ in self._arg_vvars.values()} if self._arg_vvars else set()
|
||||
reference = (
|
||||
self.project.analyses[SReachingDefinitionsAnalysis]
|
||||
.prep()(
|
||||
subject=self.func,
|
||||
func_graph=self.func_graph,
|
||||
func_args=func_args,
|
||||
use_callee_saved_regs_at_return=self._use_callee_saved_regs_at_return,
|
||||
)
|
||||
.model
|
||||
)
|
||||
reference = SReachingDefinitions(
|
||||
self.project,
|
||||
subject=self.func,
|
||||
func_graph=self.func_graph,
|
||||
func_args=func_args,
|
||||
use_callee_saved_regs_at_return=self._use_callee_saved_regs_at_return,
|
||||
).model
|
||||
if self._reaching_definitions.canonical_form() != reference.canonical_form():
|
||||
raise AssertionError("Incremental SRDA update diverged from a full rebuild")
|
||||
|
||||
|
|
@ -1938,7 +1992,7 @@ class AILSimplifier(Analysis):
|
|||
|
||||
if uses is None:
|
||||
vvar = rd.varid_to_vvar[vvar_id]
|
||||
def_codeloc = rd.all_vvar_definitions[vvar_id]
|
||||
def_codeloc = codeloc
|
||||
if def_codeloc.is_extern:
|
||||
def_stmt = None
|
||||
else:
|
||||
|
|
@ -2052,12 +2106,22 @@ class AILSimplifier(Analysis):
|
|||
# this statement declares more than one variable. we should handle it surgically
|
||||
# case 1: stmt.ret_expr and stmt.fp_ret_expr are both set, but one of them is not used
|
||||
if isinstance(stmt.ret_expr, VirtualVariable) and stmt.ret_expr.varid in dead_vvar_ids:
|
||||
stmt = stmt.copy()
|
||||
stmt.ret_expr = None
|
||||
stmt = SideEffectStatement(
|
||||
self._ail_manager.next_atom(),
|
||||
stmt.expr,
|
||||
ret_expr=None,
|
||||
fp_ret_expr=stmt.fp_ret_expr,
|
||||
**stmt.tags,
|
||||
)
|
||||
simplified = True
|
||||
if isinstance(stmt.fp_ret_expr, VirtualVariable) and stmt.fp_ret_expr.varid in dead_vvar_ids:
|
||||
stmt = stmt.copy()
|
||||
stmt.fp_ret_expr = None
|
||||
stmt = SideEffectStatement(
|
||||
self._ail_manager.next_atom(),
|
||||
stmt.expr,
|
||||
ret_expr=stmt.ret_expr,
|
||||
fp_ret_expr=None,
|
||||
**stmt.tags,
|
||||
)
|
||||
simplified = True
|
||||
|
||||
if idx in stmts_to_remove and idx not in stmts_to_keep and not isinstance(stmt, DirtyStatement):
|
||||
|
|
@ -2118,9 +2182,9 @@ class AILSimplifier(Analysis):
|
|||
isinstance(stmt.ret_expr, VirtualVariable) and stmt.ret_expr.was_combo_reg
|
||||
):
|
||||
# both the return expr and the fp_ret_expr are not used
|
||||
stmt = stmt.copy()
|
||||
stmt.ret_expr = None
|
||||
stmt.fp_ret_expr = None
|
||||
stmt = SideEffectStatement(
|
||||
self._ail_manager.next_atom(), stmt.expr, ret_expr=None, fp_ret_expr=None, **stmt.tags
|
||||
)
|
||||
simplified = True
|
||||
else:
|
||||
# Should not happen!
|
||||
|
|
@ -2170,21 +2234,32 @@ class AILSimplifier(Analysis):
|
|||
def _find_cyclic_dependent_phis_and_dirty_vvars(self, rd: SRDAModel, dead_vvar_ids: set[int]) -> set[int]:
|
||||
blocks_dict: dict[tuple[int, int | None], Block] = {(bb.addr, bb.idx): bb for bb in self.func_graph}
|
||||
|
||||
# find dirty vvars and vexccall vvars
|
||||
dirty_vvar_ids = set()
|
||||
# cache dirty or ccall vvar IDs per block to avoid re-scanning
|
||||
# TODO: Move this cache to ailment.Block once per-block defs/uses cache lands on master.
|
||||
cache = self._dirty_vvar_scan_cache
|
||||
for bb in self.func_graph:
|
||||
for stmt in bb.statements:
|
||||
# reg/tmp = ccall(...)
|
||||
# we see tmps when it's used in a cycle;
|
||||
# see binary ddc2b4cbf6ac841524375cdf82b93b9948f8ea09bbf6e8bf3410e6bc410a9d95 function 0x18001722c
|
||||
# block 0x18001724c
|
||||
if (
|
||||
isinstance(stmt, Assignment)
|
||||
and isinstance(stmt.dst, VirtualVariable)
|
||||
and (stmt.dst.was_reg or stmt.dst.was_tmp)
|
||||
and isinstance(stmt.src, (DirtyExpression, VEXCCallExpression))
|
||||
):
|
||||
dirty_vvar_ids.add(stmt.dst.varid)
|
||||
key = bb.addr, bb.idx
|
||||
entry = cache.get(key)
|
||||
if entry is not None and entry[0] is bb:
|
||||
block_dirty_ids = entry[1]
|
||||
else:
|
||||
block_dirty_ids = set()
|
||||
for stmt in bb.statements:
|
||||
# reg/tmp = ccall(...)
|
||||
# we see tmps when it's used in a cycle;
|
||||
# see binary ddc2b4cbf6ac841524375cdf82b93b9948f8ea09bbf6e8bf3410e6bc410a9d95 function 0x18001722c
|
||||
# block 0x18001724c
|
||||
if (
|
||||
isinstance(stmt, Assignment)
|
||||
and isinstance(stmt.dst, VirtualVariable)
|
||||
and (stmt.dst.was_reg or stmt.dst.was_tmp)
|
||||
and isinstance(stmt.src, (DirtyExpression, VEXCCallExpression))
|
||||
):
|
||||
block_dirty_ids.add(stmt.dst.varid)
|
||||
cache[key] = bb, block_dirty_ids
|
||||
if block_dirty_ids:
|
||||
dirty_vvar_ids |= block_dirty_ids
|
||||
|
||||
phi_and_dirty_vvar_ids = (rd.phi_vvar_ids | dirty_vvar_ids).difference(dead_vvar_ids)
|
||||
|
||||
|
|
@ -2202,18 +2277,25 @@ class AILSimplifier(Analysis):
|
|||
vvar_used_by[used_by_varid].add(var_id) # probably unnecessary
|
||||
vvar_used_by[var_id] |= self._get_vvar_used_by(var_id, rd, blocks_dict).difference(dead_vvar_ids)
|
||||
|
||||
g = networkx.DiGraph()
|
||||
# build a plain adjacency map instead of a throwaway networkx DiGraph for better performance. the performance
|
||||
# improvement is observable on notepad.exe:NPInit
|
||||
# TODO: Investigate if switching to rustworkx eliminates the need for this optimization.
|
||||
dummy_vvar_id = -1
|
||||
succs_map: dict[int, set[int]] = {}
|
||||
for var_id, used_by_initial in vvar_used_by.items():
|
||||
for u in used_by_initial:
|
||||
if u is None:
|
||||
# we can't have None in networkx.DiGraph
|
||||
g.add_edge(var_id, dummy_vvar_id)
|
||||
else:
|
||||
g.add_edge(var_id, u)
|
||||
if not used_by_initial:
|
||||
continue
|
||||
targets = {dummy_vvar_id if u is None else u for u in used_by_initial}
|
||||
if var_id in succs_map:
|
||||
succs_map[var_id] |= targets
|
||||
else:
|
||||
succs_map[var_id] = set(targets)
|
||||
for target in targets:
|
||||
if target not in succs_map:
|
||||
succs_map[target] = set()
|
||||
|
||||
cyclic_dependent_phi_varids = set()
|
||||
for scc in networkx.strongly_connected_components(g):
|
||||
for scc in _strongly_connected_components(succs_map):
|
||||
if len(scc) == 1:
|
||||
continue
|
||||
|
||||
|
|
@ -2224,11 +2306,9 @@ class AILSimplifier(Analysis):
|
|||
if varid in vvar_used_by and None in vvar_used_by[varid]:
|
||||
bail = True
|
||||
break
|
||||
if bail is False:
|
||||
succs = list(g.successors(varid))
|
||||
if any(succ_varid not in scc for succ_varid in succs):
|
||||
bail = True
|
||||
break
|
||||
if any(succ_varid not in scc for succ_varid in succs_map[varid]):
|
||||
bail = True
|
||||
break
|
||||
if bail:
|
||||
continue
|
||||
|
||||
|
|
@ -2258,7 +2338,14 @@ class AILSimplifier(Analysis):
|
|||
def _handle_VEXCCallExpression(
|
||||
expr_idx: int, expr: VEXCCallExpression, stmt_idx: int, stmt: Statement | None, block: Block | None
|
||||
) -> Expression:
|
||||
r_expr = AILBlockRewriter._handle_VEXCCallExpression(walker, expr_idx, expr, stmt_idx, stmt, block)
|
||||
r_expr = AILBlockRewriter._handle_VEXCCallExpression( # pylint:disable=protected-access
|
||||
walker,
|
||||
expr_idx,
|
||||
expr,
|
||||
stmt_idx,
|
||||
stmt,
|
||||
block,
|
||||
)
|
||||
rewriter = rewriter_cls(r_expr, self.project, self._ail_manager, rename_ccalls=self._should_rename_ccalls)
|
||||
if rewriter.result is not None:
|
||||
_any_update.v = True
|
||||
|
|
@ -2304,7 +2391,7 @@ class AILSimplifier(Analysis):
|
|||
rewriter = rewriter_cls(stmt, self.project.arch, self._ail_manager)
|
||||
if rewriter.result is not None:
|
||||
_any_update.v = True
|
||||
if walker._update_block and block is not None:
|
||||
if walker._update_block and block is not None: # pylint:disable=protected-access
|
||||
block.statements[stmt_idx] = rewriter.result # type: ignore
|
||||
assert isinstance(rewriter.result, Statement)
|
||||
return rewriter.result
|
||||
|
|
@ -2313,7 +2400,14 @@ class AILSimplifier(Analysis):
|
|||
def _handle_DirtyExpression(
|
||||
expr_idx: int, expr: DirtyExpression, stmt_idx: int, stmt: Statement | None, block: Block | None
|
||||
):
|
||||
r_expr = AILBlockRewriter._handle_DirtyExpression(walker, expr_idx, expr, stmt_idx, stmt, block)
|
||||
r_expr = AILBlockRewriter._handle_DirtyExpression( # pylint:disable=protected-access
|
||||
walker,
|
||||
expr_idx,
|
||||
expr,
|
||||
stmt_idx,
|
||||
stmt,
|
||||
block,
|
||||
)
|
||||
assert isinstance(r_expr, DirtyExpression)
|
||||
rewriter = rewriter_cls(r_expr, self.project.arch, self._ail_manager)
|
||||
if rewriter.result is not None:
|
||||
|
|
|
|||
|
|
@ -8,9 +8,8 @@ from typing import TYPE_CHECKING
|
|||
from angr.ailment.expression import Call, Const, Convert, Expression, Load, Register, Tmp, VirtualVariable
|
||||
from angr.ailment.manager import Manager
|
||||
from angr.ailment.statement import Assignment, Jump, SideEffectStatement, Statement, Store
|
||||
from angr.analyses.analysis import Analysis, register_analysis
|
||||
from angr.analyses.s_propagator import SPropagatorAnalysis
|
||||
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitionsAnalysis
|
||||
from angr.analyses.s_propagator import SPropagator
|
||||
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitions
|
||||
from angr.code_location import AILCodeLocation
|
||||
from angr.knowledge_plugins.key_definitions import atoms
|
||||
from angr.utils.ssa import has_reference_to_vvar
|
||||
|
|
@ -34,6 +33,7 @@ from .utils import (
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from angr.ailment.block import Block
|
||||
from angr.project import Project
|
||||
|
||||
|
||||
_l = logging.getLogger(name=__name__)
|
||||
|
|
@ -42,35 +42,118 @@ _l = logging.getLogger(name=__name__)
|
|||
_HAS_CALL_EXPR_WALKER = HasCallExprWalker()
|
||||
|
||||
|
||||
class BlockSimplifier(Analysis):
|
||||
class PeepholeOptimizationBundle:
|
||||
"""
|
||||
PeepholeOptimizationBundle describes a set of initialized peephole optimizer instances and the dispatch structures
|
||||
derived from them. This bundle of peephole optimizations is reusable across `BlockSimplifier` invocations (so we
|
||||
avoid rebuilding the same optimizer instances).
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_params",
|
||||
"expr_opts",
|
||||
"expr_walker",
|
||||
"multistmt_opts",
|
||||
"stmt_opts",
|
||||
"stmt_opts_by_kind",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
project,
|
||||
kb,
|
||||
ail_manager: Manager,
|
||||
func_addr: int | None = None,
|
||||
preserve_vvar_ids: set[int] | None = None,
|
||||
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]] | None = None,
|
||||
peephole_optimizations: Iterable[
|
||||
type[PeepholeOptimizationStmtBase | PeepholeOptimizationExprBase | PeepholeOptimizationMultiStmtBase]
|
||||
]
|
||||
| None = None,
|
||||
):
|
||||
if peephole_optimizations is None:
|
||||
expr_classes: Iterable = EXPR_OPTS
|
||||
stmt_classes: Iterable = STMT_OPTS
|
||||
multistmt_classes: Iterable = MULTI_STMT_OPTS
|
||||
else:
|
||||
peephole_optimizations = tuple(peephole_optimizations)
|
||||
expr_classes = [cls for cls in peephole_optimizations if issubclass(cls, PeepholeOptimizationExprBase)]
|
||||
stmt_classes = [cls for cls in peephole_optimizations if issubclass(cls, PeepholeOptimizationStmtBase)]
|
||||
multistmt_classes = [
|
||||
cls for cls in peephole_optimizations if issubclass(cls, PeepholeOptimizationMultiStmtBase)
|
||||
]
|
||||
|
||||
args = (project, kb, ail_manager, func_addr, preserve_vvar_ids, type_hints)
|
||||
self.expr_opts = [cls(*args) for cls in expr_classes]
|
||||
self.stmt_opts = [cls(*args) for cls in stmt_classes]
|
||||
self.multistmt_opts = [cls(*args) for cls in multistmt_classes]
|
||||
self.stmt_opts_by_kind = build_stmt_opts_by_kind(self.stmt_opts)
|
||||
self.expr_walker = _PeepholeExprsWalker(expr_opts=self.expr_opts)
|
||||
self._params = (project, ail_manager, func_addr, preserve_vvar_ids, type_hints, peephole_optimizations)
|
||||
|
||||
def matches(
|
||||
self,
|
||||
project,
|
||||
ail_manager: Manager,
|
||||
func_addr: int | None,
|
||||
preserve_vvar_ids: set[int] | None,
|
||||
type_hints: list | None,
|
||||
peephole_optimizations,
|
||||
) -> bool:
|
||||
p_project, p_manager, p_func_addr, p_preserve, p_hints, p_opts = self._params
|
||||
return (
|
||||
p_project is project
|
||||
and p_manager is ail_manager
|
||||
and p_func_addr == func_addr
|
||||
and p_preserve is preserve_vvar_ids
|
||||
and p_hints is type_hints
|
||||
and (
|
||||
p_opts is peephole_optimizations
|
||||
or (
|
||||
p_opts is not None
|
||||
and peephole_optimizations is not None
|
||||
and p_opts == tuple(peephole_optimizations)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class BlockSimplifier:
|
||||
"""
|
||||
Simplify an AIL block.
|
||||
|
||||
Deliberately not an :class:`Analysis`: it is instantiated once per block, hundreds of times per decompilation,
|
||||
so it skips the analysis-factory ceremony. Instantiate it directly with the project as the first argument;
|
||||
exceptions always propagate.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
project: Project,
|
||||
block: Block | None,
|
||||
ail_manager: Manager,
|
||||
func_addr: int | None = None,
|
||||
stack_pointer_tracker=None,
|
||||
peephole_optimizations: None
|
||||
| (
|
||||
Iterable[
|
||||
type[PeepholeOptimizationStmtBase]
|
||||
| type[PeepholeOptimizationExprBase]
|
||||
| type[PeepholeOptimizationMultiStmtBase]
|
||||
]
|
||||
) = None,
|
||||
peephole_optimizations: Iterable[
|
||||
type[PeepholeOptimizationStmtBase | PeepholeOptimizationExprBase | PeepholeOptimizationMultiStmtBase]
|
||||
]
|
||||
| None = None,
|
||||
preserve_vvar_ids: set[int] | None = None,
|
||||
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]] | None = None,
|
||||
cached_reaching_definitions=None,
|
||||
cached_propagator=None,
|
||||
peephole_bundle: PeepholeOptimizationBundle | None = None,
|
||||
):
|
||||
"""
|
||||
:param block: The AIL block to simplify. Setting it to None to skip calling self._analyze(), which is useful
|
||||
in test cases.
|
||||
:param peephole_bundle: A pre-built PeepholeOptimizationBundle to reuse. Its construction parameters must
|
||||
match this BlockSimplifier's; callers that simplify many blocks should build one bundle and
|
||||
pass it to every BlockSimplifier they create.
|
||||
"""
|
||||
|
||||
self.project = project
|
||||
self.kb = project.kb
|
||||
self.block = block
|
||||
self.func_addr = func_addr
|
||||
|
||||
|
|
@ -79,42 +162,25 @@ class BlockSimplifier(Analysis):
|
|||
self._type_hints = type_hints
|
||||
self._ail_manager = ail_manager
|
||||
|
||||
if peephole_optimizations is None:
|
||||
self._expr_peephole_opts = [
|
||||
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
|
||||
for cls in EXPR_OPTS
|
||||
]
|
||||
self._stmt_peephole_opts = [
|
||||
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
|
||||
for cls in STMT_OPTS
|
||||
]
|
||||
self._multistmt_peephole_opts = [
|
||||
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
|
||||
for cls in MULTI_STMT_OPTS
|
||||
]
|
||||
self._stmt_peephole_opts_by_kind = build_stmt_opts_by_kind(self._stmt_peephole_opts)
|
||||
else:
|
||||
self._expr_peephole_opts = [
|
||||
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
|
||||
for cls in peephole_optimizations
|
||||
if issubclass(cls, PeepholeOptimizationExprBase)
|
||||
]
|
||||
self._stmt_peephole_opts = [
|
||||
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
|
||||
for cls in peephole_optimizations
|
||||
if issubclass(cls, PeepholeOptimizationStmtBase)
|
||||
]
|
||||
self._multistmt_peephole_opts = [
|
||||
cls(self.project, self.kb, ail_manager, self.func_addr, self._preserve_vvar_ids, self._type_hints)
|
||||
for cls in peephole_optimizations
|
||||
if issubclass(cls, PeepholeOptimizationMultiStmtBase)
|
||||
]
|
||||
self._stmt_peephole_opts_by_kind = build_stmt_opts_by_kind(self._stmt_peephole_opts)
|
||||
if peephole_bundle is None:
|
||||
peephole_bundle = PeepholeOptimizationBundle(
|
||||
self.project,
|
||||
self.kb,
|
||||
ail_manager,
|
||||
func_addr=self.func_addr,
|
||||
preserve_vvar_ids=self._preserve_vvar_ids,
|
||||
type_hints=self._type_hints,
|
||||
peephole_optimizations=peephole_optimizations,
|
||||
)
|
||||
self._expr_peephole_opts = peephole_bundle.expr_opts
|
||||
self._stmt_peephole_opts = peephole_bundle.stmt_opts
|
||||
self._multistmt_peephole_opts = peephole_bundle.multistmt_opts
|
||||
self._stmt_peephole_opts_by_kind = peephole_bundle.stmt_opts_by_kind
|
||||
|
||||
self.result_block = None
|
||||
|
||||
# cached peephole expression walker
|
||||
self._expr_peephole_walker = _PeepholeExprsWalker(expr_opts=self._expr_peephole_opts)
|
||||
self._expr_peephole_walker = peephole_bundle.expr_walker
|
||||
|
||||
# cached Propagator and ReachingDefinitions results. Clear them if the block is updated
|
||||
self._propagator = cached_propagator
|
||||
|
|
@ -125,25 +191,33 @@ class BlockSimplifier(Analysis):
|
|||
|
||||
def _analyze(self):
|
||||
block = self.block
|
||||
assert block is not None
|
||||
ctr = 0
|
||||
max_ctr = 30
|
||||
|
||||
new_block = self._eliminate_self_assignments(block)
|
||||
new_block, changed = self._eliminate_self_assignments(block)
|
||||
# True once dead-assignment elimination is known to have nothing to do on the block the loop below starts
|
||||
# from -- either because it just ran over it without a change, or because its gate is off for that block.
|
||||
dead_assignments_clean = True
|
||||
if self._count_nonconstant_statements(new_block) >= 2 and self._has_propagatable_assignments(new_block):
|
||||
new_block = self._eliminate_dead_assignments(new_block)
|
||||
# Structural ``likes`` (idx-agnostic) instead of ``!=`` which always trips on fresh ``manager.next_atom()``
|
||||
# ids even when nothing changed structurally.
|
||||
# TODO: Keep track of changes and skip .likes(); .likes() is expensive.
|
||||
if not new_block.likes(block):
|
||||
new_block, dead_changed = self._eliminate_dead_assignments(new_block)
|
||||
changed |= dead_changed
|
||||
dead_assignments_clean = not dead_changed
|
||||
if changed:
|
||||
self._clear_cache()
|
||||
block = new_block
|
||||
|
||||
while True:
|
||||
ctr += 1
|
||||
new_block = self._simplify_block_once(block)
|
||||
# TODO: Keep track of changes and skip .likes(); .likes() is expensive.
|
||||
if new_block.likes(block):
|
||||
# the entry peephole pass is only useful on the first iteration: every later iteration receives the
|
||||
# output of the previous iteration's exit peephole pass, so running peephole again on entry is redundant.
|
||||
new_block, changed = self._simplify_block_once(
|
||||
block, entry_peephole=ctr == 1, dead_assignments_clean=dead_assignments_clean
|
||||
)
|
||||
if not changed:
|
||||
break
|
||||
|
||||
assert new_block is not None
|
||||
self._clear_cache()
|
||||
block = new_block
|
||||
if ctr >= max_ctr:
|
||||
|
|
@ -156,9 +230,10 @@ class BlockSimplifier(Analysis):
|
|||
|
||||
self.result_block = block
|
||||
|
||||
def _compute_propagation(self, block) -> SPropagatorAnalysis:
|
||||
def _compute_propagation(self, block) -> SPropagator:
|
||||
if self._propagator is None:
|
||||
self._propagator = self.project.analyses[SPropagatorAnalysis].prep(fail_fast=self._fail_fast)(
|
||||
self._propagator = SPropagator(
|
||||
self.project,
|
||||
subject=block,
|
||||
func_addr=self.func_addr,
|
||||
stack_pointer_tracker=self._stack_pointer_tracker,
|
||||
|
|
@ -168,15 +243,12 @@ class BlockSimplifier(Analysis):
|
|||
|
||||
def _compute_reaching_definitions(self, block) -> SRDAModel:
|
||||
if self._reaching_definitions is None:
|
||||
self._reaching_definitions = (
|
||||
self.project.analyses[SReachingDefinitionsAnalysis]
|
||||
.prep(fail_fast=self._fail_fast)(
|
||||
subject=block,
|
||||
track_tmps=True,
|
||||
func_addr=self.func_addr,
|
||||
)
|
||||
.model
|
||||
)
|
||||
self._reaching_definitions = SReachingDefinitions(
|
||||
self.project,
|
||||
subject=block,
|
||||
track_tmps=True,
|
||||
func_addr=self.func_addr,
|
||||
).model
|
||||
return self._reaching_definitions
|
||||
|
||||
def _clear_cache(self):
|
||||
|
|
@ -191,35 +263,56 @@ class BlockSimplifier(Analysis):
|
|||
def _count_nonconstant_statements(block) -> int:
|
||||
return sum(1 for stmt in block.statements if not (isinstance(stmt, Jump) and isinstance(stmt.target, Const)))
|
||||
|
||||
def _simplify_block_once(self, block):
|
||||
block = self._peephole_optimize(block)
|
||||
def _simplify_block_once(
|
||||
self, block, entry_peephole: bool = True, dead_assignments_clean: bool = False
|
||||
) -> tuple[Block, bool]:
|
||||
"""
|
||||
Run one round of simplification. Returns the new block and if any step reported a change.
|
||||
|
||||
:param dead_assignments_clean: True if dead-assignment elimination is known to have nothing to do on
|
||||
``block`` as passed in. Only meaningful together with ``entry_peephole``.
|
||||
"""
|
||||
changed = False
|
||||
# True once we know ``block`` is untouched and already at the fixpoint of every pass that has run over it:
|
||||
# re-running those passes on it cannot report a change.
|
||||
clean = False
|
||||
if entry_peephole:
|
||||
block, peephole_changed, exprs_updated = self._peephole_optimize(block)
|
||||
changed |= peephole_changed
|
||||
clean = dead_assignments_clean and not peephole_changed and not exprs_updated
|
||||
|
||||
nonconstant_stmts = self._count_nonconstant_statements(block)
|
||||
has_propagatable_assignments = self._has_propagatable_assignments(block)
|
||||
|
||||
# propagator
|
||||
# only call propagation if something is potentially propagatable
|
||||
if nonconstant_stmts >= 2 and has_propagatable_assignments:
|
||||
propagator = self._compute_propagation(block)
|
||||
new_block = block
|
||||
if propagator.model is not None:
|
||||
replacements = propagator.model.replacements
|
||||
if replacements:
|
||||
_, new_block = self._replace_and_build(
|
||||
replaced, new_block = self.replace_and_build(
|
||||
block, replacements, self._ail_manager, replace_registers=True
|
||||
)
|
||||
new_block = self._eliminate_self_assignments(new_block)
|
||||
changed |= replaced
|
||||
new_block, self_assign_changed = self._eliminate_self_assignments(new_block)
|
||||
changed |= self_assign_changed
|
||||
self._clear_cache()
|
||||
else:
|
||||
# Skipped calling Propagator
|
||||
new_block = block
|
||||
|
||||
if nonconstant_stmts >= 2 and has_propagatable_assignments:
|
||||
new_block = self._eliminate_dead_assignments(new_block)
|
||||
if clean and new_block is block:
|
||||
return block, False
|
||||
|
||||
return self._peephole_optimize(new_block)
|
||||
if nonconstant_stmts >= 2 and has_propagatable_assignments:
|
||||
new_block, dead_changed = self._eliminate_dead_assignments(new_block)
|
||||
changed |= dead_changed
|
||||
|
||||
new_block, peephole_changed, _ = self._peephole_optimize(new_block)
|
||||
return new_block, changed | peephole_changed
|
||||
|
||||
@staticmethod
|
||||
def _replace_and_build(
|
||||
def replace_and_build(
|
||||
block: Block,
|
||||
replacements: Mapping[AILCodeLocation, Mapping[Expression, Expression]],
|
||||
ail_manager: Manager,
|
||||
|
|
@ -309,7 +402,7 @@ class BlockSimplifier(Analysis):
|
|||
return True, new_block
|
||||
|
||||
@staticmethod
|
||||
def _eliminate_self_assignments(block):
|
||||
def _eliminate_self_assignments(block) -> tuple[Block, bool]:
|
||||
new_statements = []
|
||||
|
||||
for stmt in block.statements:
|
||||
|
|
@ -329,9 +422,12 @@ class BlockSimplifier(Analysis):
|
|||
continue
|
||||
new_statements.append(stmt)
|
||||
|
||||
return block.copy(statements=new_statements)
|
||||
if len(new_statements) == len(block.statements):
|
||||
# nothing was eliminated; keep the original block
|
||||
return block, False
|
||||
return block.copy(statements=new_statements), True
|
||||
|
||||
def _eliminate_dead_assignments(self, block):
|
||||
def _eliminate_dead_assignments(self, block) -> tuple[Block, bool]:
|
||||
def _statement_has_calls(stmt: Statement) -> bool:
|
||||
"""
|
||||
Check if a statement has any Call expressions.
|
||||
|
|
@ -354,7 +450,7 @@ class BlockSimplifier(Analysis):
|
|||
|
||||
new_statements = []
|
||||
if not block.statements:
|
||||
return block
|
||||
return block, False
|
||||
|
||||
rd = self._compute_reaching_definitions(block)
|
||||
block_loc = (block.addr, block.idx)
|
||||
|
|
@ -382,6 +478,7 @@ class BlockSimplifier(Analysis):
|
|||
used_tmps.add(tmp.tmp_idx)
|
||||
|
||||
# Remove dead assignments
|
||||
changed = False
|
||||
for idx, stmt in enumerate(block.statements):
|
||||
if isinstance(stmt, Assignment):
|
||||
# tmps can't execute new code
|
||||
|
|
@ -390,39 +487,50 @@ class BlockSimplifier(Analysis):
|
|||
|
||||
# does .src involve any Call expressions? if so, we cannot remove it
|
||||
if not _expression_has_calls(stmt.src):
|
||||
changed = True
|
||||
continue
|
||||
|
||||
if isinstance(stmt.dst, Tmp) and isinstance(stmt.src, Call):
|
||||
# eliminate the assignment and replace it with the call
|
||||
stmt = SideEffectStatement(self._ail_manager.next_atom(), stmt.src, **stmt.tags)
|
||||
changed = True
|
||||
|
||||
if isinstance(stmt, Assignment) and stmt.src == stmt.dst:
|
||||
changed = True
|
||||
continue
|
||||
|
||||
new_statements.append(stmt)
|
||||
|
||||
return block.copy(statements=new_statements)
|
||||
if not changed:
|
||||
# nothing was eliminated; keep the original block
|
||||
return block, False
|
||||
return block.copy(statements=new_statements), True
|
||||
|
||||
#
|
||||
# Peephole optimization
|
||||
#
|
||||
|
||||
def _peephole_optimize(self, block):
|
||||
# expressions are updated in place
|
||||
peephole_optimize_exprs(block, self._expr_peephole_opts, walker=self._expr_peephole_walker)
|
||||
def _peephole_optimize(self, block) -> tuple[Block, bool, bool]:
|
||||
"""
|
||||
Run all three peephole optimization levels on the block.
|
||||
|
||||
:return: (block, changed, exprs_updated), where ``changed`` is True if any optimization applied and
|
||||
``exprs_updated`` is True if the expression walker rewrote any expression.
|
||||
"""
|
||||
exprs_updated = peephole_optimize_exprs(block, self._expr_peephole_opts, walker=self._expr_peephole_walker)
|
||||
|
||||
# run statement-level optimizations
|
||||
statements, stmts_updated = peephole_optimize_stmts(
|
||||
block, self._stmt_peephole_opts, stmt_opts_by_kind=self._stmt_peephole_opts_by_kind
|
||||
block,
|
||||
self._stmt_peephole_opts,
|
||||
stmt_opts_by_kind=self._stmt_peephole_opts_by_kind,
|
||||
fixpoint_exprs=self._expr_peephole_walker.fixpoint_stmts,
|
||||
)
|
||||
|
||||
new_block = block.copy(statements=statements) if stmts_updated else block
|
||||
|
||||
statements, multi_stmts_updated = peephole_optimize_multistmts(new_block, self._multistmt_peephole_opts)
|
||||
|
||||
if not multi_stmts_updated:
|
||||
return new_block
|
||||
return new_block.copy(statements=statements)
|
||||
|
||||
|
||||
register_analysis(BlockSimplifier, "AILBlockSimplifier")
|
||||
if multi_stmts_updated:
|
||||
new_block = new_block.copy(statements=statements)
|
||||
return new_block, stmts_updated or multi_stmts_updated, exprs_updated
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import archinfo
|
|||
|
||||
from angr.ailment import Const, Expr, Stmt
|
||||
from angr.ailment.manager import Manager
|
||||
from angr.analyses.analysis import Analysis, register_analysis
|
||||
from angr.analyses.s_reaching_definitions import SRDAView
|
||||
from angr.calling_conventions import (
|
||||
SimCC,
|
||||
|
|
@ -38,19 +37,28 @@ if TYPE_CHECKING:
|
|||
from angr.analyses.s_reaching_definitions import SRDAModel
|
||||
from angr.knowledge_plugins.functions import Function
|
||||
from angr.knowledge_plugins.key_definitions.definition import Definition
|
||||
from angr.project import Project
|
||||
|
||||
|
||||
l = logging.getLogger(name=__name__)
|
||||
|
||||
|
||||
class CallSiteMaker(Analysis):
|
||||
class CallSiteMaker:
|
||||
"""
|
||||
Add calling convention, declaration, and args to a call site.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, block, *, ail_manager: Manager, reaching_definitions: SRDAModel | None = None, stack_pointer_tracker=None
|
||||
self,
|
||||
project: Project,
|
||||
block,
|
||||
*,
|
||||
ail_manager: Manager,
|
||||
reaching_definitions: SRDAModel | None = None,
|
||||
stack_pointer_tracker=None,
|
||||
):
|
||||
self.project = project
|
||||
self.kb = project.kb
|
||||
self.block = block
|
||||
|
||||
self._reaching_definitions = reaching_definitions
|
||||
|
|
@ -652,6 +660,3 @@ class CallSiteMaker(Analysis):
|
|||
|
||||
def _atom_idx(self) -> int:
|
||||
return self._ail_manager.next_atom()
|
||||
|
||||
|
||||
register_analysis(CallSiteMaker, "AILCallSiteMaker")
|
||||
|
|
|
|||
|
|
@ -245,7 +245,8 @@ class AMD64CCallRewriter(CCallRewriterBase):
|
|||
self.ail_manager.next_atom(), "And", [dep_1, flag], False, **ccall.tags
|
||||
)
|
||||
zero = Expr.Const(self.ail_manager.next_atom(), 0, dep_1.bits)
|
||||
expr_op = "CmpEQ" if cond_v == AMD64_CondTypes["CondZ"] else "CmpNE"
|
||||
# dep_1 holds the old flags: ZF is *set* iff the masked bit is non-zero
|
||||
expr_op = "CmpNE" if cond_v == AMD64_CondTypes["CondZ"] else "CmpEQ"
|
||||
|
||||
r = Expr.BinaryOp(ccall.idx, expr_op, (masked_dep, zero), False, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
|
|
@ -319,6 +320,55 @@ class AMD64CCallRewriter(CCallRewriterBase):
|
|||
r = Expr.BinaryOp(ccall.idx, "CmpLT", (dep_1, zero), True, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
|
||||
elif cond_v == AMD64_CondTypes["CondNL"]:
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBW"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBQ"],
|
||||
}:
|
||||
# CondNL (jge) is SF == OF, i.e. dep_1 >=s dep_2
|
||||
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBW"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
ccall.tags,
|
||||
)
|
||||
dep_2 = self._fix_size(
|
||||
dep_2,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBW"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
ccall.tags,
|
||||
)
|
||||
|
||||
r = Expr.BinaryOp(ccall.idx, "CmpGE", (dep_1, dep_2), True, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_LOGICB"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICW"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICL"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICQ"],
|
||||
}:
|
||||
# and/or/xor clear OF, so CondNL = SF == 0, i.e. the result dep_1 >=s 0
|
||||
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_LOGICB"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICW"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICL"],
|
||||
ccall.tags,
|
||||
)
|
||||
zero = Expr.Const(self.ail_manager.next_atom(), 0, dep_1.bits)
|
||||
r = Expr.BinaryOp(ccall.idx, "CmpGE", (dep_1, zero), True, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
|
||||
elif cond_v == AMD64_CondTypes["CondNBE"]:
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
|
|
@ -347,8 +397,10 @@ class AMD64CCallRewriter(CCallRewriterBase):
|
|||
|
||||
r = Expr.BinaryOp(ccall.idx, "CmpGT", (dep_1, dep_2), False, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
elif cond_v == AMD64_CondTypes["CondB"]:
|
||||
if op_v in {
|
||||
elif cond_v in {AMD64_CondTypes["CondB"], AMD64_CondTypes["CondBE"]}:
|
||||
# CondB tests CF; CondBE tests CF | ZF
|
||||
is_be = cond_v == AMD64_CondTypes["CondBE"]
|
||||
if not is_be and op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_ADDB"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDW"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDL"],
|
||||
|
|
@ -390,7 +442,8 @@ class AMD64CCallRewriter(CCallRewriterBase):
|
|||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBQ"],
|
||||
}:
|
||||
# dep_1 <u dep_2
|
||||
# CF is dep_1 <u dep_2 and ZF is dep_1 == dep_2, so
|
||||
# dep_1 <u dep_2 for CondB, dep_1 <=u dep_2 for CondBE
|
||||
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
|
|
@ -411,12 +464,110 @@ class AMD64CCallRewriter(CCallRewriterBase):
|
|||
|
||||
r = Expr.BinaryOp(
|
||||
ccall.idx,
|
||||
"CmpLT",
|
||||
"CmpLE" if is_be else "CmpLT",
|
||||
(dep_1, dep_2),
|
||||
False,
|
||||
**ccall.tags,
|
||||
)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_LOGICB"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICW"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICL"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICQ"],
|
||||
}:
|
||||
# and/or/xor always clear CF, so CondB is never true and CondBE degenerates to ZF
|
||||
if not is_be:
|
||||
return Expr.Const(self.ail_manager.next_atom(), 0, ccall.bits, **ccall.tags)
|
||||
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_LOGICB"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICW"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICL"],
|
||||
ccall.tags,
|
||||
)
|
||||
zero = Expr.Const(self.ail_manager.next_atom(), 0, dep_1.bits)
|
||||
r = Expr.BinaryOp(ccall.idx, "CmpEQ", (dep_1, zero), False, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
elif cond_v == AMD64_CondTypes["CondNB"]:
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_ADDB"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDW"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDL"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDQ"],
|
||||
}:
|
||||
# CondNB is !CF, i.e. the negation of the __CFADD__ carry test that CondB
|
||||
# emits. An inline (a + b) >=u a comparison would be wrong here: C integer
|
||||
# promotion keeps sub-int additions from wrapping, making it a tautology at
|
||||
# 8/16-bit widths.
|
||||
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_ADDB"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDW"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDL"],
|
||||
ccall.tags,
|
||||
)
|
||||
dep_2 = self._fix_size(
|
||||
dep_2,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_ADDB"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDW"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDL"],
|
||||
ccall.tags,
|
||||
)
|
||||
|
||||
cfadd_call = Expr.Call(
|
||||
self.ail_manager.next_atom(),
|
||||
"__CFADD__",
|
||||
args=[dep_1, dep_2],
|
||||
bits=ccall.bits,
|
||||
**ccall.tags,
|
||||
)
|
||||
variable_map_of(self.ail_manager).set_calling_convention(
|
||||
cfadd_call, SimCCUsercall(self.project.arch, [], None)
|
||||
)
|
||||
zero = Expr.Const(self.ail_manager.next_atom(), 0, cfadd_call.bits)
|
||||
r = Expr.BinaryOp(ccall.idx, "CmpEQ", (cfadd_call, zero), False, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBW"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBQ"],
|
||||
}:
|
||||
# dep_1 >=u dep_2
|
||||
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBW"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
ccall.tags,
|
||||
)
|
||||
dep_2 = self._fix_size(
|
||||
dep_2,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBW"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
ccall.tags,
|
||||
)
|
||||
|
||||
r = Expr.BinaryOp(ccall.idx, "CmpGE", (dep_1, dep_2), False, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_LOGICB"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICW"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICL"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICQ"],
|
||||
}:
|
||||
# and/or/xor always clear CF, so CondNB is always true
|
||||
return Expr.Const(self.ail_manager.next_atom(), 1, ccall.bits, **ccall.tags)
|
||||
elif (
|
||||
cond_v == AMD64_CondTypes["CondS"]
|
||||
and op_v
|
||||
|
|
@ -496,6 +647,184 @@ class AMD64CCallRewriter(CCallRewriterBase):
|
|||
)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
|
||||
elif cond_v in {AMD64_CondTypes["CondO"], AMD64_CondTypes["CondNO"]}:
|
||||
# overflow flag (jo / jno)
|
||||
is_no = cond_v == AMD64_CondTypes["CondNO"]
|
||||
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_LOGICB"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICW"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICL"],
|
||||
AMD64_OpTypes["G_CC_OP_LOGICQ"],
|
||||
}:
|
||||
# and/or/xor always clear OF: CondO -> 0, CondNO -> 1
|
||||
return Expr.Const(self.ail_manager.next_atom(), 1 if is_no else 0, ccall.bits, **ccall.tags)
|
||||
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_ADDB"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDW"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDL"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDQ"],
|
||||
}:
|
||||
# signed overflow of dep_1 + dep_2
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_ADDB"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDW"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDL"],
|
||||
ccall.tags,
|
||||
)
|
||||
dep_2 = self._fix_size(
|
||||
dep_2,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_ADDB"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDW"],
|
||||
AMD64_OpTypes["G_CC_OP_ADDL"],
|
||||
ccall.tags,
|
||||
)
|
||||
return self._overflow_helper(ccall, "__OFADD__", dep_1, dep_2, is_no)
|
||||
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBW"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBQ"],
|
||||
}:
|
||||
# signed overflow of dep_1 - dep_2
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBW"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
ccall.tags,
|
||||
)
|
||||
dep_2 = self._fix_size(
|
||||
dep_2,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_SUBB"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBW"],
|
||||
AMD64_OpTypes["G_CC_OP_SUBL"],
|
||||
ccall.tags,
|
||||
)
|
||||
return self._overflow_helper(ccall, "__OFSUB__", dep_1, dep_2, is_no)
|
||||
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_UMULB"],
|
||||
AMD64_OpTypes["G_CC_OP_UMULW"],
|
||||
AMD64_OpTypes["G_CC_OP_UMULL"],
|
||||
AMD64_OpTypes["G_CC_OP_UMULQ"],
|
||||
}:
|
||||
# unsigned multiply overflow: high half of the full product is nonzero
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_UMULB"],
|
||||
AMD64_OpTypes["G_CC_OP_UMULW"],
|
||||
AMD64_OpTypes["G_CC_OP_UMULL"],
|
||||
ccall.tags,
|
||||
)
|
||||
dep_2 = self._fix_size(
|
||||
dep_2,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_UMULB"],
|
||||
AMD64_OpTypes["G_CC_OP_UMULW"],
|
||||
AMD64_OpTypes["G_CC_OP_UMULL"],
|
||||
ccall.tags,
|
||||
)
|
||||
return self._overflow_helper(ccall, "__OFUMUL__", dep_1, dep_2, is_no)
|
||||
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_SMULB"],
|
||||
AMD64_OpTypes["G_CC_OP_SMULW"],
|
||||
AMD64_OpTypes["G_CC_OP_SMULL"],
|
||||
AMD64_OpTypes["G_CC_OP_SMULQ"],
|
||||
}:
|
||||
# signed multiply overflow
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_SMULB"],
|
||||
AMD64_OpTypes["G_CC_OP_SMULW"],
|
||||
AMD64_OpTypes["G_CC_OP_SMULL"],
|
||||
ccall.tags,
|
||||
)
|
||||
dep_2 = self._fix_size(
|
||||
dep_2,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_SMULB"],
|
||||
AMD64_OpTypes["G_CC_OP_SMULW"],
|
||||
AMD64_OpTypes["G_CC_OP_SMULL"],
|
||||
ccall.tags,
|
||||
)
|
||||
return self._overflow_helper(ccall, "__OFSMUL__", dep_1, dep_2, is_no)
|
||||
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_INCB"],
|
||||
AMD64_OpTypes["G_CC_OP_INCW"],
|
||||
AMD64_OpTypes["G_CC_OP_INCL"],
|
||||
AMD64_OpTypes["G_CC_OP_INCQ"],
|
||||
}:
|
||||
# inc overflows only when the result is the signed minimum
|
||||
nbits = self._op_nbits(
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_INCB"],
|
||||
AMD64_OpTypes["G_CC_OP_INCW"],
|
||||
AMD64_OpTypes["G_CC_OP_INCL"],
|
||||
)
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_INCB"],
|
||||
AMD64_OpTypes["G_CC_OP_INCW"],
|
||||
AMD64_OpTypes["G_CC_OP_INCL"],
|
||||
ccall.tags,
|
||||
)
|
||||
signmin = Expr.Const(self.ail_manager.next_atom(), 1 << (nbits - 1), dep_1.bits)
|
||||
expr_op = "CmpNE" if is_no else "CmpEQ"
|
||||
r = Expr.BinaryOp(ccall.idx, expr_op, (dep_1, signmin), False, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
|
||||
if op_v in {
|
||||
AMD64_OpTypes["G_CC_OP_DECB"],
|
||||
AMD64_OpTypes["G_CC_OP_DECW"],
|
||||
AMD64_OpTypes["G_CC_OP_DECL"],
|
||||
AMD64_OpTypes["G_CC_OP_DECQ"],
|
||||
}:
|
||||
# dec overflows only when the result is the signed maximum
|
||||
nbits = self._op_nbits(
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_DECB"],
|
||||
AMD64_OpTypes["G_CC_OP_DECW"],
|
||||
AMD64_OpTypes["G_CC_OP_DECL"],
|
||||
)
|
||||
dep_1 = self._fix_size(
|
||||
dep_1,
|
||||
op_v,
|
||||
AMD64_OpTypes["G_CC_OP_DECB"],
|
||||
AMD64_OpTypes["G_CC_OP_DECW"],
|
||||
AMD64_OpTypes["G_CC_OP_DECL"],
|
||||
ccall.tags,
|
||||
)
|
||||
signmax = Expr.Const(self.ail_manager.next_atom(), (1 << (nbits - 1)) - 1, dep_1.bits)
|
||||
expr_op = "CmpNE" if is_no else "CmpEQ"
|
||||
r = Expr.BinaryOp(ccall.idx, expr_op, (dep_1, signmax), False, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
|
||||
if op_v == AMD64_OpTypes["G_CC_OP_COPY"]:
|
||||
# dep_1 holds the packed flags; test the stored OF bit
|
||||
bitmask = AMD64_CondBitMasks["G_CC_MASK_O"]
|
||||
assert isinstance(bitmask, int)
|
||||
flag = Expr.Const(self.ail_manager.next_atom(), bitmask, dep_1.bits)
|
||||
masked_dep = Expr.BinaryOp(
|
||||
self.ail_manager.next_atom(), "And", [dep_1, flag], False, **ccall.tags
|
||||
)
|
||||
zero = Expr.Const(self.ail_manager.next_atom(), 0, dep_1.bits)
|
||||
expr_op = "CmpEQ" if is_no else "CmpNE"
|
||||
r = Expr.BinaryOp(ccall.idx, expr_op, (masked_dep, zero), False, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
|
||||
elif ccall.callee == "amd64g_calculate_rflags_c":
|
||||
# calculate the carry flag
|
||||
op = ccall.operands[0]
|
||||
|
|
@ -611,6 +940,33 @@ class AMD64CCallRewriter(CCallRewriterBase):
|
|||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _op_nbits(op_v: int, type_8bit, type_16bit, type_32bit) -> int:
|
||||
if op_v == type_8bit:
|
||||
return 8
|
||||
if op_v == type_16bit:
|
||||
return 16
|
||||
if op_v == type_32bit:
|
||||
return 32
|
||||
return 64
|
||||
|
||||
def _overflow_helper(self, ccall, name: str, dep_1, dep_2, is_no: bool):
|
||||
# Emit a named overflow-helper call (mirrors the __CFADD__ arm). The helper
|
||||
# returns a 0/1 flag; for the negated condition (CondNO) compare it to 0.
|
||||
call = Expr.Call(
|
||||
ccall.idx,
|
||||
name,
|
||||
args=[dep_1, dep_2],
|
||||
bits=ccall.bits,
|
||||
**ccall.tags,
|
||||
)
|
||||
variable_map_of(self.ail_manager).set_calling_convention(call, SimCCUsercall(self.project.arch, [], None))
|
||||
if not is_no:
|
||||
return call
|
||||
zero = Expr.Const(self.ail_manager.next_atom(), 0, ccall.bits)
|
||||
r = Expr.BinaryOp(self.ail_manager.next_atom(), "CmpEQ", (call, zero), False, **ccall.tags)
|
||||
return Expr.Convert(self.ail_manager.next_atom(), r.bits, ccall.bits, False, r, **ccall.tags)
|
||||
|
||||
def _fix_size(self, expr, op_v: int, type_8bit, type_16bit, type_32bit, tags):
|
||||
if op_v == type_8bit:
|
||||
bits = 8
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from angr.ailment.manager import Manager
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -995,7 +995,11 @@ class ConditionProcessor:
|
|||
if not isinstance(condition.offset, ailment.expression.Const)
|
||||
else condition.offset.value
|
||||
)
|
||||
var = claripy.BVS(f"ailexpr_Extract({offset_expr}, {hash(var_)})", condition.bits, explicit_name=True)
|
||||
var = claripy.BVS(
|
||||
f"ailexpr_Extract({condition.bits}, {condition.endness}, {offset_expr}, {hash(var_)})",
|
||||
condition.bits,
|
||||
explicit_name=True,
|
||||
)
|
||||
self._condition_mapping[var.args[0]] = condition
|
||||
return var
|
||||
if isinstance(condition, ailment.expression.Insert):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from collections import defaultdict
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.analyses.decompiler.sequence_walker import SequenceWalker
|
||||
from angr.analyses.decompiler.structurer_nodes import LoopNode
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,174 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from angr.protos import decompilation_cache_pb2
|
||||
from angr.serializable import Serializable
|
||||
from angr.utils.ail_serialization import (
|
||||
pack_arg_vvars,
|
||||
pack_ite_exprs,
|
||||
pack_static_buffers,
|
||||
pack_static_vvars,
|
||||
parse_arg_vvars,
|
||||
parse_ite_exprs,
|
||||
parse_static_buffers,
|
||||
parse_static_vvars,
|
||||
)
|
||||
|
||||
from .clinic import Clinic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from angr import ailment
|
||||
from angr.analyses.decompiler.optimization_passes.expr_op_swapper import OpDescriptor
|
||||
from angr.analyses.typehoon.typevars import TypeConstraint, TypeVariable
|
||||
from angr.knowledge_plugins.cfg import CFGModel
|
||||
|
||||
from .notes import DecompilationNote
|
||||
from .structured_codegen import BaseStructuredCodeGenerator
|
||||
from .variable_map import VariableMap
|
||||
|
||||
|
||||
class DecompilationCache:
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
# Serialization helpers.
|
||||
#
|
||||
# Conventions:
|
||||
# - Heavy sub-objects (``clinic``, ``codegen``) are embedded as already-serialized bytes (each manages its own format).
|
||||
# - AIL-typed top-level slots (``arg_vvars``, ``ite_exprs``) use the typed messages from ``ail_types.proto``.
|
||||
# - ``cfg`` is intentionally not serialized — it comes from the parent Project. Decompilation variables live on
|
||||
# kb.dec_variables.
|
||||
# - The 4 typehoon-typed slots are skipped entirely (typehoon is out of scope for now).
|
||||
# ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _simvar_to_bytes(v) -> bytes:
|
||||
return type(v).__name__.encode("ascii") + b"\0" + v.serialize()
|
||||
|
||||
|
||||
def _simvar_from_bytes(b: bytes):
|
||||
import angr.sim_variable as sv_mod # pylint:disable=import-outside-toplevel
|
||||
|
||||
sep = b.index(b"\0")
|
||||
return getattr(sv_mod, b[:sep].decode("ascii")).parse(b[sep + 1 :])
|
||||
|
||||
|
||||
def _serialize_binop_operators(binop_operators, out_msg, set_flag=None) -> None:
|
||||
if binop_operators is None:
|
||||
return
|
||||
if set_flag is not None:
|
||||
setattr(set_flag[0], set_flag[1], True)
|
||||
for op_desc, value in binop_operators.items():
|
||||
entry = out_msg.add()
|
||||
entry.key_json = op_desc.to_json()
|
||||
entry.value = value
|
||||
|
||||
|
||||
def _parse_binop_operators(entries):
|
||||
from angr.analyses.decompiler.optimization_passes.expr_op_swapper import ( # pylint:disable=import-outside-toplevel
|
||||
OpDescriptor,
|
||||
)
|
||||
|
||||
return {OpDescriptor.from_json(e.key_json): e.value for e in entries}
|
||||
|
||||
|
||||
def _serialize_parameters(params: dict, out_msg) -> None:
|
||||
"""Translate the 15-key parameters dict into a DecompilationParameters cmessage."""
|
||||
from angr.analyses.decompiler.optimization_pass_registry import ( # pylint:disable=import-outside-toplevel
|
||||
pass_to_name,
|
||||
)
|
||||
|
||||
if params.get("flavor") is not None:
|
||||
out_msg.flavor = params["flavor"]
|
||||
if "sp_tracker_track_memory" in params:
|
||||
out_msg.sp_tracker_track_memory = bool(params["sp_tracker_track_memory"])
|
||||
# Collection-typed parameters are never None (the Decompiler normalizes them to empty collections), so each is
|
||||
# written directly; an empty collection is left unset and parses back to empty.
|
||||
out_msg.vars_must_struct.extend(sorted(params.get("vars_must_struct") or ()))
|
||||
out_msg.desired_variables.extend(sorted(params.get("desired_variables") or ()))
|
||||
out_msg.inline_functions.extend(sorted(params.get("inline_functions") or ()))
|
||||
for option, value in params.get("options") or ():
|
||||
entry = out_msg.options.add()
|
||||
entry.param = option.param
|
||||
try:
|
||||
entry.value_json = json.dumps(value)
|
||||
except (TypeError, ValueError):
|
||||
entry.value_json = json.dumps(None)
|
||||
for cls in params.get("optimization_passes") or ():
|
||||
out_msg.optimization_passes.append(pass_to_name(cls))
|
||||
# peephole_optimizations is the one None-able collection: None means "use the default peephole set"
|
||||
peepholes = params.get("peephole_optimizations")
|
||||
out_msg.peephole_optimizations_use_default = peepholes is None
|
||||
if peepholes is not None:
|
||||
for cls in peepholes:
|
||||
out_msg.peephole_optimizations.append(pass_to_name(cls))
|
||||
for k, v in (params.get("expr_comments") or {}).items():
|
||||
out_msg.expr_comments[k] = v
|
||||
for k, v in (params.get("stmt_comments") or {}).items():
|
||||
out_msg.stmt_comments[k] = v
|
||||
_serialize_binop_operators(params.get("binop_operators") or {}, out_msg.binop_operators)
|
||||
if params.get("ite_exprs"):
|
||||
out_msg.ite_exprs.CopyFrom(pack_ite_exprs(params["ite_exprs"]))
|
||||
if params.get("static_vvars"):
|
||||
out_msg.static_vvars.CopyFrom(pack_static_vvars(params["static_vvars"]))
|
||||
if params.get("static_buffers"):
|
||||
out_msg.static_buffers.CopyFrom(pack_static_buffers(params["static_buffers"]))
|
||||
out_msg.save_unoptimized_graph = bool(params.get("save_unoptimized_graph"))
|
||||
|
||||
|
||||
def _parse_parameters(msg) -> dict:
|
||||
"""Always populate every one of the 15 keys in the returned dict; scalar fields that were not set come back as
|
||||
None and collection fields come back empty, except peephole_optimizations where None means "use the default
|
||||
peephole set". This matches the decompiler's normalized _cache_parameters, which _can_use_decompilation_cache
|
||||
compares key by key against the deserialized cache."""
|
||||
from angr.analyses.decompiler.decompilation_options import PARAM_TO_OPTION # pylint:disable=import-outside-toplevel
|
||||
from angr.analyses.decompiler.optimization_pass_registry import ( # pylint:disable=import-outside-toplevel
|
||||
name_to_pass,
|
||||
)
|
||||
|
||||
# Collection-typed values come back as empty collections (never None) so they match the Decompiler's normalized
|
||||
# _cache_parameters during cache-validity comparison.
|
||||
return {
|
||||
"flavor": msg.flavor if msg.HasField("flavor") else None,
|
||||
"sp_tracker_track_memory": msg.sp_tracker_track_memory if msg.HasField("sp_tracker_track_memory") else None,
|
||||
"vars_must_struct": set(msg.vars_must_struct),
|
||||
"desired_variables": frozenset(msg.desired_variables),
|
||||
"inline_functions": frozenset(msg.inline_functions),
|
||||
"options": {
|
||||
(PARAM_TO_OPTION[e.param], json.loads(e.value_json) if e.value_json else None)
|
||||
for e in msg.options
|
||||
if e.param in PARAM_TO_OPTION
|
||||
},
|
||||
# unresolvable pass names (defining module not imported) drop out; the resulting shorter list will not match
|
||||
# the live decompiler's parameters, so the cache falls through to a fresh decompilation
|
||||
"optimization_passes": [cls for n in msg.optimization_passes if (cls := name_to_pass(n)) is not None],
|
||||
"peephole_optimizations": (
|
||||
None
|
||||
if msg.peephole_optimizations_use_default
|
||||
else [cls for n in msg.peephole_optimizations if (cls := name_to_pass(n)) is not None]
|
||||
),
|
||||
"expr_comments": dict(msg.expr_comments),
|
||||
"stmt_comments": dict(msg.stmt_comments),
|
||||
"binop_operators": _parse_binop_operators(msg.binop_operators),
|
||||
"ite_exprs": parse_ite_exprs(msg.ite_exprs) if msg.HasField("ite_exprs") else set(),
|
||||
"static_vvars": parse_static_vvars(msg.static_vvars) if msg.HasField("static_vvars") else {},
|
||||
"static_buffers": parse_static_buffers(msg.static_buffers) if msg.HasField("static_buffers") else {},
|
||||
"save_unoptimized_graph": msg.save_unoptimized_graph,
|
||||
}
|
||||
|
||||
|
||||
class DecompilationCache(Serializable):
|
||||
"""
|
||||
Caches key data structures that can be used later for refining decompilation results, such as retyping variables.
|
||||
"""
|
||||
|
||||
# ``cfg`` is a decompile-time input used only for cache-validity checks. It is not serialized; after
|
||||
# deserialization it is None until the caller re-attaches it.
|
||||
__slots__ = (
|
||||
"addr",
|
||||
"arg_vvars",
|
||||
"binop_operators",
|
||||
"cfg",
|
||||
"clinic",
|
||||
"codegen",
|
||||
"errors",
|
||||
|
|
@ -32,32 +180,139 @@ class DecompilationCache:
|
|||
"parameters",
|
||||
"stack_offset_typevars",
|
||||
"stackvar_max_sizes",
|
||||
"timestamp",
|
||||
"type_constraints",
|
||||
"var_to_typevar",
|
||||
"variable_map",
|
||||
"version",
|
||||
)
|
||||
|
||||
def __init__(self, addr):
|
||||
import angr # pylint:disable=import-outside-toplevel,cyclic-import
|
||||
|
||||
self.parameters: dict[str, Any] = {}
|
||||
# angr version and creation time of this decompilation
|
||||
self.version: str = angr.__version__
|
||||
self.timestamp: int = int(time.time())
|
||||
self.addr = addr
|
||||
self.type_constraints: dict[TypeVariable, set[TypeConstraint]] | None = None
|
||||
self.arg_vvars: dict | None = None
|
||||
self.cfg: CFGModel | None = None
|
||||
# Collection-typed fields default to empty containers rather than None, so serialization never has to
|
||||
# distinguish None from empty.
|
||||
self.type_constraints: dict[TypeVariable, set[TypeConstraint]] = {}
|
||||
self.arg_vvars: dict = {}
|
||||
self.func_typevar: TypeVariable | None = None
|
||||
self.var_to_typevar: dict | None = None
|
||||
self.stackvar_max_sizes: dict | None = None
|
||||
self.stack_offset_typevars: dict | None = None
|
||||
self.var_to_typevar: dict = {}
|
||||
self.stackvar_max_sizes: dict = {}
|
||||
self.stack_offset_typevars: dict = {}
|
||||
self.codegen: BaseStructuredCodeGenerator | None = None
|
||||
self.clinic: Clinic | None = None
|
||||
self.variable_map: VariableMap | None = None
|
||||
self.ite_exprs: set[tuple[int, Any]] | None = None
|
||||
self.binop_operators: dict[OpDescriptor, str] | None = None
|
||||
self.ite_exprs: set[tuple[int, ailment.Expression]] = set()
|
||||
self.binop_operators: dict[OpDescriptor, str] = {}
|
||||
self.errors: list[str] = []
|
||||
self.function_summary: str | None = None
|
||||
self.notes: dict[str, str] = {}
|
||||
self.notes: dict[str, DecompilationNote] = {}
|
||||
self.max_tv_id: int = 0
|
||||
|
||||
@property
|
||||
def local_types(self):
|
||||
if self.clinic is None or self.clinic.variable_kb is None:
|
||||
if self.clinic is None or self.clinic.kb is None or self.addr not in self.clinic.kb.dec_variables:
|
||||
return None
|
||||
return self.clinic.variable_kb.variables[self.addr].types
|
||||
return self.clinic.kb.dec_variables[self.addr].types
|
||||
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
# Protobuf serialization. Heavy sub-objects (clinic, codegen) are embedded as already-serialized bytes; AIL-typed
|
||||
# top-level fields (arg_vvars, ite_exprs) use the typed messages from ail_types.proto. The four typehoon-typed
|
||||
# slots and the ``cfg`` input are not serialized and come back as None.
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def _get_cmsg(cls):
|
||||
return decompilation_cache_pb2.DecompilationCache() # pylint:disable=no-member
|
||||
|
||||
def serialize_to_cmessage(self):
|
||||
msg = decompilation_cache_pb2.DecompilationCache(addr=self.addr) # pylint:disable=no-member
|
||||
|
||||
if self.clinic is not None:
|
||||
msg.clinic = self.clinic.serialize()
|
||||
if self.codegen is not None:
|
||||
msg.codegen = self.codegen.serialize()
|
||||
|
||||
msg.errors.extend(self.errors)
|
||||
if self.function_summary is not None:
|
||||
msg.function_summary = self.function_summary
|
||||
# Collection fields are never None; an empty collection is simply left unset and parses back to empty.
|
||||
if self.arg_vvars:
|
||||
msg.arg_vvars.CopyFrom(pack_arg_vvars(self.arg_vvars))
|
||||
if self.ite_exprs:
|
||||
msg.ite_exprs.CopyFrom(pack_ite_exprs(self.ite_exprs))
|
||||
_serialize_binop_operators(self.binop_operators, msg.binop_operators)
|
||||
for simvar, size in self.stackvar_max_sizes.items():
|
||||
entry = msg.stackvar_max_sizes.add()
|
||||
entry.simvar = _simvar_to_bytes(simvar)
|
||||
entry.max_size = size
|
||||
|
||||
msg.version = self.version
|
||||
msg.timestamp = self.timestamp
|
||||
|
||||
# An unset parameters message means "no recorded parameters"; cache-validity checks treat such a cache as
|
||||
# always usable (matching runs with use_cache=False).
|
||||
if self.parameters:
|
||||
_serialize_parameters(self.parameters, msg.parameters)
|
||||
|
||||
for k, note in self.notes.items():
|
||||
msg.notes_json[k] = note.to_json()
|
||||
|
||||
return msg
|
||||
|
||||
@classmethod
|
||||
def parse_from_cmessage(
|
||||
cls,
|
||||
cmsg,
|
||||
*,
|
||||
project=None,
|
||||
kb=None,
|
||||
function=None,
|
||||
cfg=None,
|
||||
**_,
|
||||
):
|
||||
"""Parse a DecompilationCache from a cmessage. Runtime back-references (project, kb, function, cfg) are
|
||||
passed through to the embedded Clinic / codegen parsers so the parsed cache is functional for cache-hit
|
||||
validity checks. Decompilation variables live on kb.dec_variables."""
|
||||
from .notes import DecompilationNote # pylint:disable=import-outside-toplevel
|
||||
from .structured_codegen.c import CStructuredCodeGenerator # pylint:disable=import-outside-toplevel
|
||||
|
||||
cache = cls(cmsg.addr)
|
||||
# cfg is not serialized; reattach from kwargs so cache-validity checks still work.
|
||||
cache.cfg = cfg
|
||||
|
||||
if cmsg.HasField("clinic"):
|
||||
cache.clinic = Clinic.parse(cmsg.clinic, project=project, kb=kb, function=function, cfg=cfg)
|
||||
if cmsg.HasField("codegen"):
|
||||
cache.codegen = CStructuredCodeGenerator.parse(cmsg.codegen, project=project, kb=kb, func=function)
|
||||
|
||||
cache.errors = list(cmsg.errors)
|
||||
if cmsg.HasField("function_summary"):
|
||||
cache.function_summary = cmsg.function_summary
|
||||
# Collection fields default to empty (set in __init__); only assign when the message carries content.
|
||||
if cmsg.HasField("arg_vvars"):
|
||||
cache.arg_vvars = parse_arg_vvars(cmsg.arg_vvars)
|
||||
if cmsg.HasField("ite_exprs"):
|
||||
cache.ite_exprs = parse_ite_exprs(cmsg.ite_exprs)
|
||||
cache.binop_operators = _parse_binop_operators(cmsg.binop_operators)
|
||||
cache.stackvar_max_sizes = {_simvar_from_bytes(e.simvar): e.max_size for e in cmsg.stackvar_max_sizes}
|
||||
|
||||
# legacy blobs carry the proto3 defaults ""/0, meaning "unknown"; do not re-stamp them with current values
|
||||
cache.version = cmsg.version
|
||||
cache.timestamp = cmsg.timestamp
|
||||
if cache.codegen is not None:
|
||||
# mirror the stamps onto the codegen (a fresh decompile does the same in Decompiler._decompile)
|
||||
cache.codegen.version = cache.version
|
||||
cache.codegen.timestamp = cache.timestamp
|
||||
|
||||
if cmsg.HasField("parameters"):
|
||||
cache.parameters = _parse_parameters(cmsg.parameters)
|
||||
|
||||
cache.notes = {k: DecompilationNote.from_json(v) for k, v in cmsg.notes_json.items()}
|
||||
|
||||
return cache
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ class DecompilationOption[T]:
|
|||
|
||||
O = DecompilationOption
|
||||
|
||||
# Serialization contract for display options (cls="codegen"): to survive Codegen serialization, an option's param
|
||||
# must have a matching optional scalar proto field, named identically, in the trailing display-option block of the
|
||||
# Codegen message (protos/codegen.proto). Options without such a field are dropped on round-trip.
|
||||
|
||||
options = [
|
||||
O(
|
||||
"Aggressively remove dead memdefs",
|
||||
|
|
|
|||
|
|
@ -16,11 +16,9 @@ from angr.analyses.s_propagator import sprop_cache_scope
|
|||
from angr.analyses.typehoon.typehoon import Typehoon
|
||||
from angr.analyses.typehoon.typevars import TypeVariableManager
|
||||
from angr.errors import AngrAIError
|
||||
from angr.knowledge_base import KnowledgeBase
|
||||
from angr.knowledge_plugins.functions.function import Function
|
||||
from angr.rust.optimization_passes import get_rust_optimization_passes
|
||||
from angr.rust.typehoon.typehoon import RustTypehoon
|
||||
from angr.sim_type import parse_type
|
||||
from angr.sim_variable import SimMemoryVariable, SimRegisterVariable, SimStackVariable
|
||||
from angr.utils import timethis
|
||||
|
||||
|
|
@ -29,11 +27,21 @@ from .clinic import ClinicStage
|
|||
from .condition_processor import ConditionProcessor
|
||||
from .decompilation_cache import DecompilationCache
|
||||
from .decompilation_options import PARAM_TO_OPTION, DecompilationOption
|
||||
from .edits import (
|
||||
DecompilationEditError,
|
||||
list_variable_names,
|
||||
reflow_types,
|
||||
rename_function,
|
||||
rename_variable,
|
||||
resolve_variable,
|
||||
set_variable_type,
|
||||
)
|
||||
from .notes import DecompilationNote
|
||||
from .optimization_passes.optimization_pass import OptimizationPassStage
|
||||
from .presets import DECOMPILATION_PRESETS, DecompilationPreset
|
||||
from .region_identifier import RegionIdentifier
|
||||
from .sequence_walker import SequenceWalker
|
||||
from .structured_codegen import DummyStructuredCodeGenerator
|
||||
from .structured_codegen.c import CStructuredCodeGenerator
|
||||
from .structured_codegen.rust import RustStructuredCodeGenerator
|
||||
from .structurer_nodes import SequenceNode
|
||||
|
|
@ -62,6 +70,15 @@ class Decompiler(Analysis):
|
|||
|
||||
Run this on a Function object for which a normalized CFG has been constructed.
|
||||
The fully processed output can be found in result.codegen.text
|
||||
|
||||
AIL graphs exposed on the result (both on a fresh run and on a cache hit, including caches reloaded from
|
||||
angrdb or the runtime-db spill):
|
||||
|
||||
- ``ail_graph`` (= ``clinic.cc_graph``): the simplified graph before region identification.
|
||||
- ``clinic.graph``: the final graph after region identification and region simplification.
|
||||
- ``unoptimized_ail_graph`` (= ``clinic.unoptimized_graph``): a snapshot before the first structure-altering
|
||||
optimization pass; use it for an exact instruction-to-AIL mapping. Only built when
|
||||
``save_unoptimized_graph=True`` is passed; otherwise this attribute is None on both fresh runs and cache hits.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -72,7 +89,6 @@ class Decompiler(Analysis):
|
|||
preset: str | DecompilationPreset | None = None,
|
||||
optimization_passes=None,
|
||||
sp_tracker_track_memory=True,
|
||||
variable_kb=None,
|
||||
peephole_optimizations: _PEEPHOLE_OPTIMIZATIONS_TYPE = None,
|
||||
vars_must_struct: set[str] | None = None,
|
||||
flavor="pseudocode",
|
||||
|
|
@ -81,7 +97,7 @@ class Decompiler(Analysis):
|
|||
ite_exprs=None,
|
||||
binop_operators=None,
|
||||
decompile=True,
|
||||
regen_clinic=True,
|
||||
regen_clinic=False,
|
||||
inline_functions=None,
|
||||
desired_variables=None,
|
||||
update_memory_data: bool = True,
|
||||
|
|
@ -98,6 +114,7 @@ class Decompiler(Analysis):
|
|||
static_vvars: dict | None = None,
|
||||
static_buffers: dict | None = None,
|
||||
codegen_cls=CStructuredCodeGenerator,
|
||||
save_unoptimized_graph: bool = False,
|
||||
):
|
||||
if not isinstance(func, Function):
|
||||
func = self.kb.functions[func]
|
||||
|
|
@ -138,7 +155,6 @@ class Decompiler(Analysis):
|
|||
self._sp_tracker_track_memory = sp_tracker_track_memory
|
||||
self._peephole_optimizations = peephole_optimizations
|
||||
self._vars_must_struct = vars_must_struct
|
||||
self._variable_kb = variable_kb
|
||||
self._expr_comments = expr_comments
|
||||
self._stmt_comments = stmt_comments
|
||||
self._ite_exprs = ite_exprs
|
||||
|
|
@ -151,24 +167,29 @@ class Decompiler(Analysis):
|
|||
self._desired_variables = frozenset(desired_variables) if desired_variables else set()
|
||||
self._static_vvars = static_vvars if static_vvars is not None else {}
|
||||
self._static_buffers = static_buffers if static_buffers is not None else {}
|
||||
self._save_unoptimized_graph = save_unoptimized_graph
|
||||
# ``cfg`` is not in this dict: it is an input, not part of the decompilation result. Its identity is
|
||||
# checked separately in :meth:`_can_use_decompilation_cache`.
|
||||
# Collection-typed values are normalized to empty collections (never None) so the serialized cache does not
|
||||
# need to distinguish None from empty. The exception is peephole_optimizations, where None means "use the
|
||||
# default peephole set" and is distinct from an explicitly empty list.
|
||||
self._cache_parameters = (
|
||||
{
|
||||
"cfg": self._cfg,
|
||||
"variable_kb": self._variable_kb,
|
||||
"options": {(o, v) for o, v in self._options if o.category != "Display" and v != o.default_value},
|
||||
"optimization_passes": self._optimization_passes,
|
||||
"sp_tracker_track_memory": self._sp_tracker_track_memory,
|
||||
"peephole_optimizations": self._peephole_optimizations,
|
||||
"vars_must_struct": self._vars_must_struct,
|
||||
"vars_must_struct": self._vars_must_struct or set(),
|
||||
"flavor": self._flavor,
|
||||
"expr_comments": self._expr_comments,
|
||||
"stmt_comments": self._stmt_comments,
|
||||
"ite_exprs": self._ite_exprs,
|
||||
"binop_operators": self._binop_operators,
|
||||
"expr_comments": self._expr_comments or {},
|
||||
"stmt_comments": self._stmt_comments or {},
|
||||
"ite_exprs": self._ite_exprs or set(),
|
||||
"binop_operators": self._binop_operators or {},
|
||||
"inline_functions": self._inline_functions,
|
||||
"desired_variables": self._desired_variables,
|
||||
"static_vvars": self._static_vvars,
|
||||
"static_buffers": self._static_buffers,
|
||||
"save_unoptimized_graph": self._save_unoptimized_graph,
|
||||
}
|
||||
if use_cache
|
||||
else None
|
||||
|
|
@ -238,9 +259,14 @@ class Decompiler(Analysis):
|
|||
def _can_use_decompilation_cache(self, cache: DecompilationCache) -> bool:
|
||||
if self._cache_parameters is None or cache.parameters is None:
|
||||
return False
|
||||
# deserialized caches come back with cfg unset until the caller re-attaches it; unset is not a mismatch
|
||||
if cache.cfg is not None and cache.cfg is not self._cfg:
|
||||
return False
|
||||
a, b = self._cache_parameters, cache.parameters
|
||||
id_checks = {"cfg", "variable_kb"}
|
||||
return all(a[k] is b[k] if k in id_checks else a[k] == b[k] for k in self._cache_parameters)
|
||||
if not b:
|
||||
# AngrDB-loaded caches carry no recorded parameters; there is nothing to validate against
|
||||
return True
|
||||
return all(k in b and a[k] == b[k] for k in a)
|
||||
|
||||
@staticmethod
|
||||
def _parse_options(options: list[tuple[DecompilationOption | str, Any]]) -> list[tuple[DecompilationOption, Any]]:
|
||||
|
|
@ -260,6 +286,30 @@ class Decompiler(Analysis):
|
|||
with sprop_cache_scope(self._sprop_walker_cache):
|
||||
self._decompile()
|
||||
|
||||
def _reuse_cached_decompilation(self, cache, clinic, codegen) -> None:
|
||||
"""Full-reuse fast path: expose the cached clinic and codegen as this run's results without re-running the
|
||||
pipeline. A live codegen's text is re-rendered to pick up in-place display edits; a freshly-deserialized
|
||||
codegen (``_handlers is None``) keeps its stored text. The codegen inherits the cache's version and
|
||||
timestamp."""
|
||||
codegen.version = cache.version
|
||||
codegen.timestamp = cache.timestamp
|
||||
if codegen._handlers is not None:
|
||||
codegen.regenerate_text()
|
||||
|
||||
self.cache = cache
|
||||
self.clinic = clinic
|
||||
self.codegen = codegen
|
||||
self.seq_node = None
|
||||
self.ail_graph = clinic.cc_graph
|
||||
self.unoptimized_ail_graph = clinic.unoptimized_graph
|
||||
self._variable_map = clinic.variable_map
|
||||
self.vvar_id_start = clinic.vvar_id_start
|
||||
self._copied_var_ids = clinic.copied_var_ids
|
||||
|
||||
if self.update_cache:
|
||||
self.kb.decompilations[(self.func.addr, self._flavor)] = cache
|
||||
self._finish_progress()
|
||||
|
||||
@timethis
|
||||
def _decompile(self):
|
||||
if self.func.is_simprocedure:
|
||||
|
|
@ -284,10 +334,28 @@ class Decompiler(Analysis):
|
|||
else:
|
||||
old_codegen = None
|
||||
old_clinic = None
|
||||
ite_exprs = self._ite_exprs
|
||||
binop_operators = self._binop_operators
|
||||
# normalize to empty collections so the cache never stores None (passes treat None and empty the same)
|
||||
ite_exprs = self._ite_exprs or set()
|
||||
binop_operators = self._binop_operators or {}
|
||||
l.debug("Decompilation cache miss")
|
||||
|
||||
# Full-reuse fast path: with use_cache and without regen_clinic (the default), a valid cache short-circuits
|
||||
# the entire pipeline and hands back the cached clinic and codegen. Requires an AST-carrying codegen (not
|
||||
# DummyStructuredCodeGenerator) and this function's variables in kb.dec_variables; anything else falls
|
||||
# through to a fresh decompilation.
|
||||
if (
|
||||
self.use_cache
|
||||
and not self._regen_clinic
|
||||
and cache is not None
|
||||
and old_clinic is not None
|
||||
and old_codegen is not None
|
||||
and not isinstance(old_codegen, DummyStructuredCodeGenerator)
|
||||
and self.func.addr in self.kb.dec_variables
|
||||
and self.func.prototype is not None
|
||||
):
|
||||
self._reuse_cached_decompilation(cache, old_clinic, old_codegen)
|
||||
return
|
||||
|
||||
self.options_by_class = defaultdict(list)
|
||||
|
||||
if self._options:
|
||||
|
|
@ -298,15 +366,7 @@ class Decompiler(Analysis):
|
|||
self._set_global_variables()
|
||||
self._update_progress(5.0, text="Converting to AIL")
|
||||
|
||||
variable_kb = self._variable_kb
|
||||
# fall back to old codegen
|
||||
if variable_kb is None and old_codegen is not None and isinstance(old_codegen, CStructuredCodeGenerator):
|
||||
variable_kb = old_codegen._variable_kb
|
||||
|
||||
if variable_kb is None:
|
||||
reset_variable_names = True
|
||||
else:
|
||||
reset_variable_names = self.func.addr not in variable_kb.variables.function_managers
|
||||
reset_variable_names = self.func.addr not in self.kb.dec_variables.function_managers
|
||||
|
||||
# determine a few arguments according to the structuring algorithm
|
||||
fold_callexprs_into_conditions = False
|
||||
|
|
@ -327,6 +387,7 @@ class Decompiler(Analysis):
|
|||
fold_callexprs_into_conditions = True
|
||||
|
||||
cache = DecompilationCache(self.func.addr)
|
||||
cache.cfg = self._cfg
|
||||
if self._cache_parameters is not None:
|
||||
cache.parameters = self._cache_parameters
|
||||
cache.ite_exprs = ite_exprs
|
||||
|
|
@ -341,12 +402,17 @@ class Decompiler(Analysis):
|
|||
def progress_callback(p, **kwargs):
|
||||
return self._update_progress(p * (70 - 5) / 100.0 + 5, **kwargs)
|
||||
|
||||
if self._regen_clinic or old_clinic is None or self.func.prototype is None:
|
||||
# a deserialized clinic whose function has no dec_variables cannot drive codegen; re-run Clinic instead
|
||||
if (
|
||||
self._regen_clinic
|
||||
or old_clinic is None
|
||||
or self.func.prototype is None
|
||||
or self.func.addr not in self.kb.dec_variables
|
||||
):
|
||||
clinic = self.project.analyses.Clinic(
|
||||
self.func,
|
||||
kb=self.kb,
|
||||
fail_fast=self._fail_fast,
|
||||
variable_kb=variable_kb,
|
||||
reset_variable_names=reset_variable_names,
|
||||
optimization_passes=self._optimization_passes,
|
||||
sp_tracker_track_memory=self._sp_tracker_track_memory,
|
||||
|
|
@ -371,22 +437,28 @@ class Decompiler(Analysis):
|
|||
notes=self.notes,
|
||||
static_vvars=self._static_vvars,
|
||||
static_buffers=self._static_buffers,
|
||||
save_unoptimized_graph=self._save_unoptimized_graph,
|
||||
flavor=self._flavor,
|
||||
variable_map=variable_map,
|
||||
**self.options_to_params(self.options_by_class["clinic"]),
|
||||
)
|
||||
else:
|
||||
clinic = old_clinic
|
||||
# the deserialized clinic may carry peephole-optimization names that were unresolvable at parse time
|
||||
# (their defining module was not imported then); retry resolving before its passes run again
|
||||
clinic.resolve_peephole_optimizations()
|
||||
# reuse the old, unaltered graph
|
||||
clinic.graph = clinic.cc_graph
|
||||
clinic.cc_graph = clinic.copy_graph()
|
||||
# the SRDA model is tied to the previous run's graph; drop it so the simplification passes below
|
||||
# regenerate it fresh for the reused graph
|
||||
clinic.reaching_definitions = None
|
||||
|
||||
self.clinic = clinic
|
||||
self.cache = cache
|
||||
# Make the VariableMap available on the cache regardless of whether Clinic re-linked variables (a partial
|
||||
# Clinic run, or the reuse-cached-Clinic path, may not repopulate cache.variable_map during linking).
|
||||
cache.variable_map = clinic.variable_map
|
||||
self._variable_kb = clinic.variable_kb
|
||||
self._variable_map = clinic.variable_map
|
||||
self._update_progress(70.0, text="Identifying regions")
|
||||
self.vvar_id_start = clinic.vvar_id_start
|
||||
|
|
@ -396,11 +468,13 @@ class Decompiler(Analysis):
|
|||
# the function is empty
|
||||
return
|
||||
|
||||
# expose a copy of the graph before any optimizations that may change the graph occur;
|
||||
# use this graph if you need a reference of exact mapping of instructions to AIL statements
|
||||
self.unoptimized_ail_graph = (
|
||||
clinic.unoptimized_graph if clinic.unoptimized_graph is not None else clinic.copy_graph()
|
||||
)
|
||||
# expose a copy of the graph before any optimizations that may change the graph occur; use this graph if you
|
||||
# need an exact instruction-to-AIL mapping. Only built when save_unoptimized_graph is set. clinic captured
|
||||
# the snapshot iff a structure-altering pass ran; if none did, the current graph is itself unoptimized.
|
||||
if self._save_unoptimized_graph:
|
||||
self.unoptimized_ail_graph = (
|
||||
clinic.unoptimized_graph if clinic.unoptimized_graph is not None else clinic.copy_graph()
|
||||
)
|
||||
cond_proc = ConditionProcessor(self.project.arch, clinic._ail_manager)
|
||||
|
||||
clinic.graph = self._run_graph_simplification_passes(
|
||||
|
|
@ -458,8 +532,8 @@ class Decompiler(Analysis):
|
|||
# simplify it
|
||||
# Get variable manager for loop counter naming in RegionSimplifier
|
||||
variable_manager = None
|
||||
if clinic.variable_kb is not None and self.func.addr in clinic.variable_kb.variables:
|
||||
variable_manager = clinic.variable_kb.variables[self.func.addr]
|
||||
if self.func.addr in self.kb.dec_variables:
|
||||
variable_manager = self.kb.dec_variables[self.func.addr]
|
||||
region_simplifier_params = self.options_to_params(self.options_by_class["region_simplifier"])
|
||||
# The Rust flavor forces if-else simplification off regardless of user options.
|
||||
region_simplifier_params.pop("simplify_ifelse", None)
|
||||
|
|
@ -482,7 +556,7 @@ class Decompiler(Analysis):
|
|||
binop_operators=cache.binop_operators,
|
||||
goto_manager=s.goto_manager,
|
||||
graph=clinic.graph,
|
||||
variable_kb=self._variable_kb,
|
||||
kb=self.kb,
|
||||
)
|
||||
|
||||
# rewrite the sequence node to remove phi expressions
|
||||
|
|
@ -501,7 +575,6 @@ class Decompiler(Analysis):
|
|||
ail_graph=clinic.graph,
|
||||
flavor=self._flavor,
|
||||
func_args=clinic.arg_list,
|
||||
variable_kb=clinic.variable_kb,
|
||||
variable_map=clinic.variable_map,
|
||||
expr_comments=old_codegen.expr_comments if old_codegen is not None else None,
|
||||
stmt_comments=old_codegen.stmt_comments if old_codegen is not None else None,
|
||||
|
|
@ -517,6 +590,10 @@ class Decompiler(Analysis):
|
|||
# save a copy of the AIL graph that is optimized but not modified by region identification
|
||||
self.ail_graph = clinic.cc_graph
|
||||
self.cache.codegen = codegen
|
||||
if codegen is not None:
|
||||
# copy the cache's version and timestamp onto the codegen
|
||||
codegen.version = self.cache.version
|
||||
codegen.timestamp = self.cache.timestamp
|
||||
self.cache.clinic = self.clinic
|
||||
|
||||
# LLM refinement pass
|
||||
|
|
@ -591,7 +668,7 @@ class Decompiler(Analysis):
|
|||
blocks_by_addr=addr_to_blocks,
|
||||
blocks_by_addr_and_idx=addr_and_idx_to_blocks,
|
||||
graph=ail_graph,
|
||||
variable_kb=self._variable_kb,
|
||||
kb=self.kb,
|
||||
reaching_definitions=reaching_definitions,
|
||||
entry_node_addr=self.clinic.entry_node_addr,
|
||||
scratch=self._optimization_scratch,
|
||||
|
|
@ -656,7 +733,7 @@ class Decompiler(Analysis):
|
|||
blocks_by_addr=addr_to_blocks,
|
||||
blocks_by_addr_and_idx=addr_and_idx_to_blocks,
|
||||
graph=ail_graph,
|
||||
variable_kb=self._variable_kb,
|
||||
kb=self.kb,
|
||||
arg_vvars=arg_vvars,
|
||||
region_identifier=ri,
|
||||
reaching_definitions=reaching_definitions,
|
||||
|
|
@ -747,13 +824,13 @@ class Decompiler(Analysis):
|
|||
# nothing to reflow; but this should not happen
|
||||
return None
|
||||
|
||||
var_kb = self._variable_kb if self._variable_kb is not None else KnowledgeBase(self.project)
|
||||
var_kb = self.kb
|
||||
|
||||
if self.func.addr not in var_kb.variables:
|
||||
if self.func.addr not in var_kb.dec_variables:
|
||||
# for some reason variables for the current function don't really exist...
|
||||
groundtruth = {}
|
||||
else:
|
||||
var_manager = var_kb.variables[self.func.addr]
|
||||
var_manager = var_kb.dec_variables[self.func.addr]
|
||||
# ground-truth types
|
||||
groundtruth = {}
|
||||
for variable in var_manager.variables_with_manual_types:
|
||||
|
|
@ -816,7 +893,7 @@ class Decompiler(Analysis):
|
|||
and isinstance(codegen, CStructuredCodeGenerator)
|
||||
and codegen.cfunc is not None
|
||||
):
|
||||
var_manager = var_kb.variables[self.func.addr]
|
||||
var_manager = var_kb.dec_variables[self.func.addr]
|
||||
for i, arg in enumerate(codegen.cfunc.arg_list):
|
||||
if i >= len(self.func.prototype.args):
|
||||
break
|
||||
|
|
@ -885,12 +962,10 @@ class Decompiler(Analysis):
|
|||
:param ail_graph: The AIL graph to transform out of SSA form.
|
||||
:return: The translated AIL graph.
|
||||
"""
|
||||
variable_kb = self._variable_kb
|
||||
dephication = self.project.analyses.GraphDephication(
|
||||
self.func,
|
||||
ail_graph,
|
||||
rewrite=True,
|
||||
variable_kb=variable_kb,
|
||||
variable_map=self._variable_map,
|
||||
kb=self.kb,
|
||||
fail_fast=self._fail_fast,
|
||||
|
|
@ -898,12 +973,10 @@ class Decompiler(Analysis):
|
|||
return dephication.output
|
||||
|
||||
def transform_seqnode_from_ssa(self, seq_node: SequenceNode) -> SequenceNode:
|
||||
variable_kb = self._variable_kb
|
||||
dephication = self.project.analyses.SeqNodeDephication(
|
||||
self.func,
|
||||
seq_node,
|
||||
rewrite=True,
|
||||
variable_kb=variable_kb,
|
||||
variable_map=self._variable_map,
|
||||
kb=self.kb,
|
||||
fail_fast=self._fail_fast,
|
||||
|
|
@ -963,29 +1036,10 @@ class Decompiler(Analysis):
|
|||
if not code_text:
|
||||
return False
|
||||
|
||||
# collect unified variables
|
||||
varman = self._variable_kb.variables[self.func.addr]
|
||||
unified_vars = varman.get_unified_variables(sort=None)
|
||||
|
||||
# also collect argument variables
|
||||
arg_vars = []
|
||||
if (
|
||||
self.codegen
|
||||
and isinstance(self.codegen, CStructuredCodeGenerator)
|
||||
and self.codegen.cfunc
|
||||
and self.codegen.cfunc.arg_list
|
||||
):
|
||||
for cvar in self.codegen.cfunc.arg_list:
|
||||
v = cvar.unified_variable if cvar.unified_variable is not None else cvar.variable
|
||||
if v not in unified_vars:
|
||||
arg_vars.append(v)
|
||||
|
||||
all_vars = unified_vars + arg_vars
|
||||
if not all_vars:
|
||||
var_names = list_variable_names(self.codegen, self.kb, self.func.addr)
|
||||
if not var_names:
|
||||
return False
|
||||
|
||||
var_names = [v.name or str(v) for v in all_vars]
|
||||
|
||||
prompt = (
|
||||
"You are a reverse engineering assistant. Given the following decompiled C code, suggest better, "
|
||||
"more descriptive variable names. Only include variables that you want to rename. "
|
||||
|
|
@ -1000,27 +1054,27 @@ class Decompiler(Analysis):
|
|||
if not result:
|
||||
return False
|
||||
|
||||
# build name-to-variable lookup
|
||||
name_to_var = {}
|
||||
for v in all_vars:
|
||||
key = v.name or str(v)
|
||||
name_to_var[key] = v
|
||||
|
||||
changed = False
|
||||
for rename in result.renames:
|
||||
old_name = rename.old_name
|
||||
new_name = rename.new_name
|
||||
if not new_name:
|
||||
old_name, new_name = rename.old_name, rename.new_name
|
||||
if not new_name or old_name == new_name:
|
||||
continue
|
||||
var = name_to_var.get(old_name)
|
||||
if var is None:
|
||||
try:
|
||||
edit = rename_variable(
|
||||
self.project,
|
||||
self.func,
|
||||
old_name,
|
||||
new_name,
|
||||
kb=self.kb,
|
||||
flavor=self._flavor,
|
||||
rerender=False,
|
||||
)
|
||||
except DecompilationEditError as ex:
|
||||
l.debug("LLM rename %s -> %s rejected: %s", old_name, new_name, ex)
|
||||
continue
|
||||
if old_name == new_name:
|
||||
continue
|
||||
var.name = new_name
|
||||
var.renamed = True
|
||||
changed = True
|
||||
l.info("LLM renamed variable %s -> %s", old_name, new_name)
|
||||
if edit.changed:
|
||||
changed = True
|
||||
l.info("LLM renamed variable %s -> %s", old_name, new_name)
|
||||
|
||||
return changed
|
||||
|
||||
|
|
@ -1064,13 +1118,15 @@ class Decompiler(Analysis):
|
|||
if not new_name or new_name == current_name:
|
||||
return False
|
||||
|
||||
l.info("LLM renamed function %s -> %s", current_name, new_name)
|
||||
self.func.name = new_name
|
||||
self.func.is_default_name = False
|
||||
if self.codegen and isinstance(self.codegen, CStructuredCodeGenerator) and self.codegen.cfunc:
|
||||
self.codegen.cfunc.name = new_name
|
||||
try:
|
||||
edit = rename_function(self.project, self.func, new_name, kb=self.kb, flavor=self._flavor, rerender=False)
|
||||
except DecompilationEditError as ex:
|
||||
l.debug("LLM function rename %s -> %s rejected: %s", current_name, new_name, ex)
|
||||
return False
|
||||
|
||||
return True
|
||||
if edit.changed:
|
||||
l.info("LLM renamed function %s -> %s", current_name, new_name)
|
||||
return edit.changed
|
||||
|
||||
def llm_suggest_variable_types(
|
||||
self, llm_client=None, code_text: str | None = None, raise_exc: bool = False
|
||||
|
|
@ -1093,19 +1149,20 @@ class Decompiler(Analysis):
|
|||
if not code_text:
|
||||
return False
|
||||
|
||||
varman = self._variable_kb.variables[self.func.addr]
|
||||
unified_vars = varman.get_unified_variables(sort=None)
|
||||
varman = self.kb.dec_variables[self.func.addr]
|
||||
|
||||
if not unified_vars:
|
||||
return False
|
||||
|
||||
# build current type info
|
||||
var_type_info = {}
|
||||
for v in unified_vars:
|
||||
name = v.name or str(v)
|
||||
current_type = varman.get_variable_type(v)
|
||||
for name in list_variable_names(self.codegen, self.kb, self.func.addr):
|
||||
try:
|
||||
rv = resolve_variable(self.kb, self.func.addr, name, codegen=self.codegen, flavor=self._flavor)
|
||||
except DecompilationEditError:
|
||||
continue
|
||||
current_type = varman.get_variable_type(rv.variable)
|
||||
var_type_info[name] = str(current_type) if current_type else "unknown"
|
||||
|
||||
if not var_type_info:
|
||||
return False
|
||||
|
||||
prompt = (
|
||||
"You are a reverse engineering assistant. Given the following decompiled C code and the current "
|
||||
"variable types, suggest better C types for the variables. Only include variables whose types "
|
||||
|
|
@ -1120,33 +1177,33 @@ class Decompiler(Analysis):
|
|||
if not result:
|
||||
return False
|
||||
|
||||
# build name-to-variable lookup
|
||||
name_to_var = {}
|
||||
for v in unified_vars:
|
||||
key = v.name or str(v)
|
||||
name_to_var[key] = v
|
||||
|
||||
changed = False
|
||||
for type_change in result.type_changes:
|
||||
var_name = type_change.variable_name
|
||||
type_str = type_change.new_type
|
||||
var_name, type_str = type_change.variable_name, type_change.new_type
|
||||
if not type_str:
|
||||
continue
|
||||
var = name_to_var.get(var_name)
|
||||
if var is None:
|
||||
continue
|
||||
try:
|
||||
new_type = parse_type(type_str, arch=self.project.arch)
|
||||
except Exception: # pylint:disable=broad-exception-caught
|
||||
l.debug("LLM suggested unparseable type '%s' for %s", type_str, var_name)
|
||||
# reflow once at the end rather than per variable: Typehoon is expensive
|
||||
edit = set_variable_type(
|
||||
self.project,
|
||||
self.func,
|
||||
var_name,
|
||||
type_str,
|
||||
kb=self.kb,
|
||||
flavor=self._flavor,
|
||||
reflow=False,
|
||||
)
|
||||
except DecompilationEditError as ex:
|
||||
l.debug("LLM retype of %s to '%s' rejected: %s", var_name, type_str, ex)
|
||||
continue
|
||||
if edit.changed:
|
||||
changed = True
|
||||
l.info("LLM changed type of %s to %s", var_name, type_str)
|
||||
|
||||
varman.set_variable_type(var, new_type, mark_manual=True, all_unified=True)
|
||||
changed = True
|
||||
l.info("LLM changed type of %s to %s", var_name, type_str)
|
||||
|
||||
if changed and self.codegen:
|
||||
self.codegen.reload_variable_types()
|
||||
if changed:
|
||||
new_codegen = reflow_types(self.project, self.func, kb=self.kb, flavor=self._flavor, rerender=False)
|
||||
if new_codegen is not None:
|
||||
self.codegen = new_codegen
|
||||
|
||||
return changed
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Any
|
|||
from angr.analyses.analysis import Analysis
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from angr import KnowledgeBase
|
||||
from angr.analyses.decompiler.variable_map import VariableMap
|
||||
|
||||
l = logging.getLogger(name=__name__)
|
||||
|
|
@ -24,7 +23,6 @@ class DephicationBase(Analysis):
|
|||
func,
|
||||
vvar_to_vvar_mapping: dict[int, int] | None = None,
|
||||
rewrite: bool = False,
|
||||
variable_kb: KnowledgeBase | None = None,
|
||||
variable_map: VariableMap | None = None,
|
||||
):
|
||||
if isinstance(func, str):
|
||||
|
|
@ -32,7 +30,6 @@ class DephicationBase(Analysis):
|
|||
else:
|
||||
self._function = func
|
||||
|
||||
self.variable_kb = variable_kb
|
||||
self.vvar_to_vvar_mapping = vvar_to_vvar_mapping if vvar_to_vvar_mapping is not None else None
|
||||
self.variable_map = variable_map
|
||||
self.rewrite = rewrite
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ from .dephication_base import DephicationBase
|
|||
from .graph_rewriting import GraphRewritingAnalysis
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from angr import KnowledgeBase
|
||||
from angr.analyses.decompiler.variable_map import VariableMap
|
||||
|
||||
|
||||
|
|
@ -34,7 +33,6 @@ class GraphDephication(DephicationBase): # pylint:disable=abstract-method
|
|||
ail_graph,
|
||||
vvar_to_vvar_mapping: dict[int, int] | None = None,
|
||||
rewrite: bool = False,
|
||||
variable_kb: KnowledgeBase | None = None,
|
||||
variable_map: VariableMap | None = None,
|
||||
):
|
||||
"""
|
||||
|
|
@ -48,7 +46,6 @@ class GraphDephication(DephicationBase): # pylint:disable=abstract-method
|
|||
func,
|
||||
vvar_to_vvar_mapping=vvar_to_vvar_mapping,
|
||||
rewrite=rewrite,
|
||||
variable_kb=variable_kb,
|
||||
variable_map=variable_map,
|
||||
)
|
||||
|
||||
|
|
@ -76,7 +73,7 @@ class GraphDephication(DephicationBase): # pylint:disable=abstract-method
|
|||
self._function,
|
||||
self._graph,
|
||||
self.vvar_to_vvar_mapping,
|
||||
variable_kb=self.variable_kb,
|
||||
kb=self.kb,
|
||||
variable_map=self.variable_map,
|
||||
)
|
||||
return rewriter.out_graph
|
||||
|
|
|
|||
|
|
@ -26,13 +26,13 @@ class GraphRewritingAnalysis(ForwardAnalysis[None, NodeType, object, object, obj
|
|||
func,
|
||||
ail_graph,
|
||||
vvar_to_vvar: dict[int, int],
|
||||
variable_kb=None,
|
||||
kb=None,
|
||||
variable_map=None,
|
||||
):
|
||||
self.project = project
|
||||
self._function = func
|
||||
self._graph_visitor = FunctionGraphVisitor(self._function, ail_graph)
|
||||
self.variable_kb = variable_kb
|
||||
self._dvars_kb = kb
|
||||
self.variable_map = variable_map
|
||||
|
||||
ForwardAnalysis.__init__(
|
||||
|
|
@ -44,7 +44,7 @@ class GraphRewritingAnalysis(ForwardAnalysis[None, NodeType, object, object, obj
|
|||
self.project,
|
||||
self._vvar_to_vvar,
|
||||
func_addr=self._function.addr,
|
||||
variable_kb=self.variable_kb,
|
||||
kb=self._dvars_kb,
|
||||
variable_map=self.variable_map,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from angr.ailment.block import Block
|
|||
from angr.ailment.expression import Phi, VirtualVariable
|
||||
from angr.ailment.statement import Assignment, ConditionalJump, Jump, Label
|
||||
from angr.analyses.analysis import Analysis, register_analysis
|
||||
from angr.analyses.s_reaching_definitions import SRDAModel
|
||||
from angr.analyses.s_reaching_definitions import SRDAModel, SReachingDefinitions
|
||||
from angr.knowledge_plugins.functions import Function
|
||||
from angr.utils.ssa import is_phi_assignment
|
||||
|
||||
|
|
@ -60,9 +60,7 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
|
|||
|
||||
self.vvar_to_vvar_mapping = None
|
||||
self.copied_vvar_ids: set[int] = set()
|
||||
self._rd: SRDAModel = self.project.analyses.SReachingDefinitions(
|
||||
subject=self._function, func_graph=self._graph
|
||||
).model
|
||||
self._rd: SRDAModel = SReachingDefinitions(self.project, subject=self._function, func_graph=self._graph).model
|
||||
self._blocks: dict[tuple[int, int | None], Block] = {(block.addr, block.idx): block for block in self._graph}
|
||||
|
||||
self._analyze()
|
||||
|
|
@ -74,14 +72,6 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
|
|||
# collect phi assignments
|
||||
phi_to_srcvarid = self._collect_phi_assignments()
|
||||
|
||||
# initialize phi_congruence_class
|
||||
phi_congruence_class: dict[int, set[int]] = {}
|
||||
for phi_varid in phi_to_srcvarid:
|
||||
phi_congruence_class[phi_varid] = {phi_varid}
|
||||
for src_and_varids in phi_to_srcvarid.values():
|
||||
for _, varid in src_and_varids:
|
||||
phi_congruence_class[varid] = {varid}
|
||||
|
||||
# compute liveness
|
||||
liveness = self.project.analyses.SLiveness(
|
||||
self._function, func_graph=self._graph, entry=self._entry, arg_vvars=self._arg_vvars
|
||||
|
|
@ -91,13 +81,73 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
|
|||
live_outs = liveness.model.live_outs
|
||||
interference = liveness.interference_graph()
|
||||
|
||||
unresolved_neighbor_map = defaultdict(set)
|
||||
# A phi congruence class is the transitive closure over phi statements. This means two vvars that never appear
|
||||
# together in a single phi statement can still land in the same phi congruence class through a chain of phi
|
||||
# statements.
|
||||
#
|
||||
# The following code maintains phi congruence classes in a union-find structure.
|
||||
# Each congruence class records its members and the set of vvars interfering with any of its members.
|
||||
parent: dict[int, int] = {}
|
||||
members: dict[int, set[int]] = {}
|
||||
interferes_with: dict[int, set[int]] = {}
|
||||
|
||||
# check for interferences
|
||||
candidate_vvar_set_to_phiid: defaultdict[frozenset[int], set[int]] = defaultdict(set)
|
||||
for phi_id, src_and_varids in phi_to_srcvarid.items():
|
||||
def _make(v: int) -> None:
|
||||
if v not in parent:
|
||||
parent[v] = v
|
||||
members[v] = {v}
|
||||
nbrs = set(interference[v]) if interference.has_node(v) else set()
|
||||
nbrs.discard(v) # a self-loop carries no information for coalescing
|
||||
interferes_with[v] = nbrs
|
||||
|
||||
def _find(v: int) -> int:
|
||||
_make(v)
|
||||
root = v
|
||||
while parent[root] != root:
|
||||
root = parent[root]
|
||||
while parent[v] != root:
|
||||
parent[v], v = root, parent[v]
|
||||
return root
|
||||
|
||||
def _classes_interfere(v0: int, v1: int) -> bool:
|
||||
r0, r1 = _find(v0), _find(v1)
|
||||
if r0 == r1:
|
||||
return False
|
||||
if len(members[r0]) > len(members[r1]):
|
||||
r0, r1 = r1, r0
|
||||
return not interferes_with[r1].isdisjoint(members[r0])
|
||||
|
||||
def _union(v0: int, v1: int) -> None:
|
||||
r0, r1 = _find(v0), _find(v1)
|
||||
if r0 == r1:
|
||||
return
|
||||
if len(members[r0]) < len(members[r1]):
|
||||
r0, r1 = r1, r0
|
||||
parent[r1] = r0
|
||||
members[r0] |= members[r1]
|
||||
interferes_with[r0] |= interferes_with[r1]
|
||||
del members[r1]
|
||||
del interferes_with[r1]
|
||||
|
||||
def _note_interference(v0: int, v1: int) -> None:
|
||||
if v0 == v1:
|
||||
return
|
||||
interference.add_edge(v0, v1)
|
||||
interferes_with[_find(v0)].add(v1)
|
||||
interferes_with[_find(v1)].add(v0)
|
||||
|
||||
def _loc_key(loc: tuple[int, int | None]) -> tuple[int, int]:
|
||||
return loc[0], -1 if loc[1] is None else loc[1]
|
||||
|
||||
# process phi statements in a deterministic order
|
||||
phi_ids = sorted(
|
||||
phi_to_srcvarid,
|
||||
key=lambda vid: (_loc_key(self._vvar_defloc[vid][0]), self._vvar_defloc[vid][1], vid),
|
||||
)
|
||||
|
||||
for phi_id in phi_ids:
|
||||
src_and_varids = sorted(phi_to_srcvarid[phi_id], key=lambda t: (_loc_key(t[0]), t[1]))
|
||||
candidate_vvar_set: set[int] = set()
|
||||
src_and_varids = list(src_and_varids)
|
||||
unresolved_neighbor_map: defaultdict[int, set[int]] = defaultdict(set)
|
||||
|
||||
for i in range(-1, len(src_and_varids)):
|
||||
for j in range(i + 1, len(src_and_varids)):
|
||||
|
|
@ -111,9 +161,18 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
|
|||
if var1 == var2:
|
||||
continue
|
||||
|
||||
if interference.has_edge(var1, var2):
|
||||
intersection_1 = phi_congruence_class[var1].intersection(live_outs[src1])
|
||||
intersection_2 = phi_congruence_class[var2].intersection(live_outs[src2])
|
||||
if _classes_interfere(var1, var2):
|
||||
# the intersection considers both liveouts and the vvars used in the last statement of the
|
||||
# block if it is a jump or a conditional jump because we cannot insert a vvar copy statement
|
||||
# after the jump. this is a special case that is not covered in Sreedhar et al.'s paper. It is
|
||||
# documented in "Revisiting Out-of-SSA Translation for Correctness, Code Quality, and
|
||||
# Efficiency" (Section II.A) by Boissinot et. al.
|
||||
intersection_1 = members[_find(var1)].intersection(
|
||||
live_outs[src2] | liveness.model.block_end_vvars.get(src2, set())
|
||||
)
|
||||
intersection_2 = members[_find(var2)].intersection(
|
||||
live_outs[src1] | liveness.model.block_end_vvars.get(src1, set())
|
||||
)
|
||||
if intersection_1 and not intersection_2:
|
||||
# case 1
|
||||
candidate_vvar_set.add(var1)
|
||||
|
|
@ -131,7 +190,7 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
|
|||
|
||||
# process unresolved_neighbor_map in a decreasing order of the number of neighbors
|
||||
while unresolved_neighbor_map:
|
||||
varid, neighbors = max(unresolved_neighbor_map.items(), key=lambda x: len(x[1]))
|
||||
varid, neighbors = max(unresolved_neighbor_map.items(), key=lambda x: (len(x[1]), x[0]))
|
||||
del unresolved_neighbor_map[varid]
|
||||
|
||||
candidate_vvar_set.add(varid)
|
||||
|
|
@ -142,19 +201,16 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
|
|||
if not unresolved_neighbor_map[neighbor]:
|
||||
del unresolved_neighbor_map[neighbor]
|
||||
|
||||
if candidate_vvar_set:
|
||||
candidate_vvar_set_to_phiid[frozenset(candidate_vvar_set)].add(phi_id)
|
||||
|
||||
for vvar_set, phi_ids in candidate_vvar_set_to_phiid.items():
|
||||
# insert copies of variables as needed
|
||||
for varid in vvar_set:
|
||||
insertion_type, new_vvar_ids = self._insert_vvar_copy(varid, phi_ids)
|
||||
for varid in sorted(candidate_vvar_set):
|
||||
insertion_type, new_vvar_ids = self._insert_vvar_copy(varid, {phi_id})
|
||||
|
||||
if insertion_type == 0:
|
||||
for src, old_vvar_id, new_vvar_id in new_vvar_ids:
|
||||
for src, old_vvar_id, new_vvar_id in sorted(
|
||||
new_vvar_ids, key=lambda t: (_loc_key(t[0]), t[1], t[2])
|
||||
):
|
||||
self.copied_vvar_ids.add(new_vvar_id)
|
||||
|
||||
phi_congruence_class[new_vvar_id] = {new_vvar_id}
|
||||
_make(new_vvar_id)
|
||||
live_outs[src].add(new_vvar_id)
|
||||
|
||||
src_block = self._blocks[(src[0], src[1])]
|
||||
|
|
@ -167,47 +223,44 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
|
|||
|
||||
# update interference graph
|
||||
for vvar_id in live_outs[src]:
|
||||
interference.add_edge(new_vvar_id, vvar_id)
|
||||
_note_interference(new_vvar_id, vvar_id)
|
||||
|
||||
else: # insertion_type == 1, i.e. the set has only one element
|
||||
for phi_block_loc, old_phi_varid, new_phi_varid in new_vvar_ids:
|
||||
else: # insertion_type == 1, i.e. the copy replaces the phi destination
|
||||
for phi_block_loc, old_phi_varid, new_phi_varid in sorted(
|
||||
new_vvar_ids, key=lambda t: (_loc_key(t[0]), t[1], t[2])
|
||||
):
|
||||
self.copied_vvar_ids.add(new_phi_varid)
|
||||
|
||||
phi_congruence_class[new_phi_varid] = {new_phi_varid}
|
||||
_make(new_phi_varid)
|
||||
|
||||
live_ins[phi_block_loc].discard(old_phi_varid)
|
||||
live_ins[phi_block_loc].add(new_phi_varid)
|
||||
|
||||
# update interference graph
|
||||
for vvar_id in live_ins[phi_block_loc]:
|
||||
interference.add_edge(new_phi_varid, vvar_id)
|
||||
_note_interference(new_phi_varid, vvar_id)
|
||||
|
||||
# update phi_congruence_class
|
||||
for phi_id in phi_to_srcvarid:
|
||||
# merge the congruence classes of the phi destination and its (possibly rewritten) sources
|
||||
(phidef_block_addr, phidef_block_idx), phidef_stmt_idx = self._vvar_defloc[phi_id]
|
||||
phi_block = self._blocks[(phidef_block_addr, phidef_block_idx)]
|
||||
# phi_stmt is the newly created phi statement with variables replaced
|
||||
phi_stmt = phi_block.statements[phidef_stmt_idx]
|
||||
phi_src_vvar_ids = {src_vvar.varid for _, src_vvar in phi_stmt.src.src_and_vvars if src_vvar is not None}
|
||||
new_class = phi_congruence_class[phi_stmt.dst.varid]
|
||||
for src_vvar_id in phi_src_vvar_ids:
|
||||
new_class |= phi_congruence_class[src_vvar_id]
|
||||
phi_congruence_class[src_vvar_id] = new_class
|
||||
for _, src_vvar in phi_stmt.src.src_and_vvars:
|
||||
if src_vvar is not None:
|
||||
_union(phi_stmt.dst.varid, src_vvar.varid)
|
||||
|
||||
# append statements that were recorded for prepending
|
||||
for block, stmts in self._stmts_to_prepend.items():
|
||||
for stmt in stmts:
|
||||
self._prepend_stmt(block, stmt)
|
||||
|
||||
# remove congruence classes with only one element
|
||||
for phi_varid in list(phi_congruence_class):
|
||||
if len(phi_congruence_class[phi_varid]) == 1:
|
||||
del phi_congruence_class[phi_varid]
|
||||
|
||||
mapping: dict[int, int] = {}
|
||||
for phi_varid, congruence_class in phi_congruence_class.items():
|
||||
for varid in congruence_class:
|
||||
mapping[varid] = phi_varid
|
||||
for class_members in members.values():
|
||||
if len(class_members) <= 1:
|
||||
# congruence classes with only one element require no remapping
|
||||
continue
|
||||
rep = min(class_members)
|
||||
for varid in class_members:
|
||||
mapping[varid] = rep
|
||||
|
||||
return mapping
|
||||
|
||||
|
|
@ -237,7 +290,9 @@ class GraphDephicationVVarMapping(Analysis): # pylint:disable=abstract-method
|
|||
if src not in stmt_appended_locs:
|
||||
# we have not yet appended a statement to this block
|
||||
the_block = self._blocks[src]
|
||||
ins_addr = the_block.addr + the_block.original_size - 1
|
||||
ins_addr = (
|
||||
the_block.addr + (the_block.original_size if the_block.original_size is not None else 1) - 1
|
||||
)
|
||||
new_category = phi_stmt.dst.category
|
||||
new_oident = phi_stmt.dst.oident
|
||||
new_vvar = VirtualVariable(
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
|
|||
project,
|
||||
vvar_to_vvar: dict[int, int],
|
||||
func_addr: int | None = None,
|
||||
variable_kb: KnowledgeBase | None = None,
|
||||
kb: KnowledgeBase | None = None,
|
||||
variable_map: VariableMap | None = None,
|
||||
):
|
||||
super().__init__(project)
|
||||
|
|
@ -64,7 +64,7 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
|
|||
self.vvar_to_vvar = vvar_to_vvar
|
||||
self.out_block = None
|
||||
self.func_addr = func_addr
|
||||
self.variable_kb = variable_kb
|
||||
self._dvars_kb = kb
|
||||
self.variable_map = variable_map
|
||||
|
||||
self._stmt_handlers["IncompleteSwitchCaseHeadStatement"] = self._handle_stmt_IncompleteSwitchCaseHeadStatement
|
||||
|
|
@ -121,13 +121,13 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
|
|||
return ()
|
||||
if (
|
||||
self.func_addr is not None
|
||||
and self.variable_kb is not None
|
||||
and self.func_addr in self.variable_kb.variables
|
||||
and self._dvars_kb is not None
|
||||
and self.func_addr in self._dvars_kb.dec_variables
|
||||
and self.variable_map is not None
|
||||
):
|
||||
dst_var = self.variable_map.variable(dst)
|
||||
src_var = self.variable_map.variable(src)
|
||||
var_manager = self.variable_kb.variables[self.func_addr]
|
||||
var_manager = self._dvars_kb.dec_variables[self.func_addr]
|
||||
if (
|
||||
dst_var is not None
|
||||
and src_var is not None
|
||||
|
|
@ -136,6 +136,7 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
|
|||
# skip it
|
||||
return ()
|
||||
|
||||
if new_dst is not None or new_src is not None:
|
||||
return Assignment(stmt.idx, dst, src, **stmt.tags)
|
||||
return None
|
||||
|
||||
|
|
@ -236,8 +237,9 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
|
|||
return None
|
||||
|
||||
def _handle_stmt_DirtyStatement(self, stmt: DirtyStatement) -> DirtyStatement | None:
|
||||
dirty = self._expr(stmt.dirty)
|
||||
if dirty is None or dirty is stmt.dirty:
|
||||
dirty_in = stmt.dirty
|
||||
dirty = self._expr(dirty_in)
|
||||
if dirty is None or dirty == dirty_in:
|
||||
return None
|
||||
assert isinstance(dirty, DirtyExpression)
|
||||
return DirtyStatement(stmt.idx, dirty, **stmt.tags)
|
||||
|
|
@ -394,19 +396,24 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
|
|||
return None
|
||||
|
||||
def _handle_expr_Extract(self, expr):
|
||||
base = self._expr(expr.base) or expr.base
|
||||
offset = self._expr(expr.offset) or expr.offset
|
||||
base_in = expr.base
|
||||
offset_in = expr.offset
|
||||
base = self._expr(base_in) or base_in
|
||||
offset = self._expr(offset_in) or offset_in
|
||||
|
||||
if base is not expr.base or offset is not expr.offset:
|
||||
if base != base_in or offset != offset_in:
|
||||
return Extract(expr.idx, expr.bits, base, offset, expr.endness, **expr.tags)
|
||||
return None
|
||||
|
||||
def _handle_expr_Insert(self, expr):
|
||||
base = self._expr(expr.base) or expr.base
|
||||
offset = self._expr(expr.offset) or expr.offset
|
||||
value = self._expr(expr.value) or expr.value
|
||||
base_in = expr.base
|
||||
offset_in = expr.offset
|
||||
value_in = expr.value
|
||||
base = self._expr(base_in) or base_in
|
||||
offset = self._expr(offset_in) or offset_in
|
||||
value = self._expr(value_in) or value_in
|
||||
|
||||
if base is not expr.base or offset is not expr.offset or value is not expr.value:
|
||||
if base != base_in or offset != offset_in or value != value_in:
|
||||
return Insert(expr.idx, base, offset, value, expr.endness, **expr.tags)
|
||||
return None
|
||||
|
||||
|
|
@ -549,6 +556,7 @@ class SimEngineDephiRewriting(SimEngineNostmtAIL[None, Expression | None, Statem
|
|||
_handle_binop_CmpLTV = _unreachable
|
||||
_handle_binop_MinV = _unreachable
|
||||
_handle_binop_MaxV = _unreachable
|
||||
_handle_binop_HAddV = _unreachable
|
||||
_handle_binop_QAddV = _unreachable
|
||||
_handle_binop_QSubV = _unreachable
|
||||
_handle_binop_QNarrowBinV = _unreachable
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ class SeqNodeRewriter(SequenceWalker):
|
|||
seq_node: SequenceNode,
|
||||
vvar_to_vvar: dict[int, int],
|
||||
project: angr.Project,
|
||||
variable_kb: KnowledgeBase | None = None,
|
||||
func_addr: int | None = None,
|
||||
kb: KnowledgeBase | None = None,
|
||||
variable_map: VariableMap | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
|
|
@ -81,7 +81,7 @@ class SeqNodeRewriter(SequenceWalker):
|
|||
self.vvar_to_vvar = vvar_to_vvar
|
||||
self.variable_map = variable_map
|
||||
self.engine = SimEngineDephiRewriting(
|
||||
project, self.vvar_to_vvar, func_addr=func_addr, variable_kb=variable_kb, variable_map=self.variable_map
|
||||
project, self.vvar_to_vvar, func_addr=func_addr, kb=kb, variable_map=self.variable_map
|
||||
)
|
||||
|
||||
self.output = self.walk(seq_node)
|
||||
|
|
@ -134,14 +134,12 @@ class SeqNodeDephication(DephicationBase):
|
|||
seq_node,
|
||||
vvar_to_vvar_mapping: dict[int, int] | None = None,
|
||||
rewrite: bool = False,
|
||||
variable_kb: KnowledgeBase | None = None,
|
||||
variable_map: VariableMap | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
func,
|
||||
vvar_to_vvar_mapping=vvar_to_vvar_mapping,
|
||||
rewrite=rewrite,
|
||||
variable_kb=variable_kb,
|
||||
variable_map=variable_map,
|
||||
)
|
||||
|
||||
|
|
@ -160,7 +158,7 @@ class SeqNodeDephication(DephicationBase):
|
|||
self.vvar_to_vvar_mapping,
|
||||
self.project,
|
||||
func_addr=self._function.addr,
|
||||
variable_kb=self.variable_kb,
|
||||
kb=self.kb,
|
||||
variable_map=self.variable_map,
|
||||
)
|
||||
return rewriter.output
|
||||
|
|
|
|||
89
angr/analyses/decompiler/edits/__init__.py
Normal file
89
angr/analyses/decompiler/edits/__init__.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""
|
||||
Knowledge-base edits for decompilation output: renaming, retyping, and commenting.
|
||||
|
||||
This layer is deliberately free of any UI or transport dependency so both the headless MCP server
|
||||
and angr-management drive the same code. It is *not* thread-safe: locking belongs to whoever owns
|
||||
the knowledge base's lifetime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .cache import (
|
||||
DEFAULT_FLAVOR,
|
||||
get_cache,
|
||||
invalidate,
|
||||
require_cache,
|
||||
restore_user_edits,
|
||||
snapshot_user_edits,
|
||||
)
|
||||
from .errors import (
|
||||
AmbiguousFunctionError,
|
||||
DecompilationEditError,
|
||||
FunctionNotFoundError,
|
||||
InvalidNameError,
|
||||
NameCollisionError,
|
||||
NotDecompiledError,
|
||||
TypeParseError,
|
||||
UnsupportedEditError,
|
||||
VariableNotFoundError,
|
||||
)
|
||||
from .hooks import EditHooks, NullEditHooks
|
||||
from .ops import (
|
||||
global_variable_at,
|
||||
reflow_types,
|
||||
rename_function,
|
||||
rename_global,
|
||||
rename_variable,
|
||||
set_comment,
|
||||
set_function_prototype,
|
||||
set_global_type,
|
||||
set_variable_type,
|
||||
)
|
||||
from .resolve import (
|
||||
ResolvedVariable,
|
||||
concrete_variables,
|
||||
list_variable_names,
|
||||
parse_address,
|
||||
resolve_function,
|
||||
resolve_variable,
|
||||
validate_name,
|
||||
)
|
||||
from .results import EditResult, Refresh
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_FLAVOR",
|
||||
"AmbiguousFunctionError",
|
||||
"DecompilationEditError",
|
||||
"EditHooks",
|
||||
"EditResult",
|
||||
"FunctionNotFoundError",
|
||||
"InvalidNameError",
|
||||
"NameCollisionError",
|
||||
"NotDecompiledError",
|
||||
"NullEditHooks",
|
||||
"Refresh",
|
||||
"ResolvedVariable",
|
||||
"TypeParseError",
|
||||
"UnsupportedEditError",
|
||||
"VariableNotFoundError",
|
||||
"concrete_variables",
|
||||
"get_cache",
|
||||
"global_variable_at",
|
||||
"invalidate",
|
||||
"list_variable_names",
|
||||
"parse_address",
|
||||
"reflow_types",
|
||||
"rename_function",
|
||||
"rename_global",
|
||||
"rename_variable",
|
||||
"require_cache",
|
||||
"resolve_function",
|
||||
"resolve_variable",
|
||||
"restore_user_edits",
|
||||
"set_comment",
|
||||
"set_function_prototype",
|
||||
"set_global_type",
|
||||
"set_variable_type",
|
||||
"snapshot_user_edits",
|
||||
"validate_name",
|
||||
]
|
||||
126
angr/analyses/decompiler/edits/cache.py
Normal file
126
angr/analyses/decompiler/edits/cache.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""
|
||||
Decompilation-cache access for the edit layer.
|
||||
|
||||
Both ``kb.decompilations`` and ``kb.dec_variables`` spill to LMDB under memory pressure and hand
|
||||
back a freshly deserialized -- that is, *different* -- Python object on reload. Never hold a
|
||||
``DecompilationCache`` or a ``VariableManagerInternal`` across anything that can decompile; always
|
||||
re-fetch by key at the point of mutation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from .errors import NotDecompiledError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
from angr.analyses.decompiler.decompilation_cache import DecompilationCache
|
||||
from angr.knowledge_base import KnowledgeBase
|
||||
from angr.sim_type import SimType
|
||||
|
||||
DEFAULT_FLAVOR = "pseudocode"
|
||||
|
||||
|
||||
def get_cache(kb: KnowledgeBase, func_addr: int, flavor: str = DEFAULT_FLAVOR) -> DecompilationCache | None:
|
||||
"""Return the cached decompilation for a function, or None if there is none."""
|
||||
return kb.decompilations.get((func_addr, flavor), None)
|
||||
|
||||
|
||||
def require_cache(kb: KnowledgeBase, func_addr: int, flavor: str = DEFAULT_FLAVOR) -> DecompilationCache:
|
||||
"""Return the cached decompilation, raising NotDecompiledError if it is missing or empty."""
|
||||
cache = get_cache(kb, func_addr, flavor)
|
||||
if cache is None or cache.codegen is None:
|
||||
raise NotDecompiledError(
|
||||
f"Function {func_addr:#x} has not been decompiled yet (flavor {flavor!r}). Decompile it first."
|
||||
)
|
||||
return cache
|
||||
|
||||
|
||||
def invalidate(
|
||||
kb: KnowledgeBase,
|
||||
func_addr: int,
|
||||
*,
|
||||
flavors: Iterable[str] | None = None,
|
||||
drop_variables: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Drop cached decompilations for a function.
|
||||
|
||||
:param flavors: Flavors to drop; None drops every flavor currently cached. A prototype
|
||||
change must drop all of them, or a non-pseudocode flavor silently
|
||||
retains the old signature.
|
||||
:param drop_variables: Also drop ``kb.dec_variables[func_addr]``. Required for new argument
|
||||
names to take effect, but it discards every rename and manual type for
|
||||
the function -- see :func:`snapshot_user_edits`.
|
||||
"""
|
||||
if flavors is None:
|
||||
flavors = list(kb.decompilations.available_flavors(func_addr))
|
||||
for flavor in flavors:
|
||||
kb.decompilations.discard((func_addr, flavor))
|
||||
|
||||
if drop_variables and kb.dec_variables.has_function_manager(func_addr):
|
||||
del kb.dec_variables[func_addr]
|
||||
|
||||
|
||||
def snapshot_user_edits(kb: KnowledgeBase, func_addr: int) -> dict[str, tuple[str | None, SimType | None]]:
|
||||
"""
|
||||
Capture user renames and manual types for a function, keyed by ``SimVariable.ident``.
|
||||
|
||||
Used to survive the ``dec_variables`` drop that a prototype change requires.
|
||||
"""
|
||||
if not kb.dec_variables.has_function_manager(func_addr):
|
||||
return {}
|
||||
|
||||
varman = kb.dec_variables[func_addr]
|
||||
snapshot: dict[str, tuple[str | None, SimType | None]] = {}
|
||||
for var in varman.get_unified_variables(sort=None):
|
||||
if not var.ident:
|
||||
continue
|
||||
name = var.name if var.renamed else None
|
||||
ty = varman.get_variable_type(var) if var in varman.variables_with_manual_types else None
|
||||
if name is not None or ty is not None:
|
||||
snapshot[var.ident] = (name, ty)
|
||||
return snapshot
|
||||
|
||||
|
||||
def restore_user_edits(
|
||||
kb: KnowledgeBase, func_addr: int, snapshot: dict[str, tuple[str | None, SimType | None]]
|
||||
) -> tuple[int, list[str]]:
|
||||
"""
|
||||
Re-apply a :func:`snapshot_user_edits` result after re-decompilation.
|
||||
|
||||
Best-effort: idents can change across a re-decompile, so the unmatched ones are returned rather
|
||||
than silently dropped.
|
||||
"""
|
||||
if not snapshot or not kb.dec_variables.has_function_manager(func_addr):
|
||||
return 0, sorted(snapshot)
|
||||
|
||||
varman = kb.dec_variables[func_addr]
|
||||
by_ident = {var.ident: var for var in varman.get_unified_variables(sort=None) if var.ident}
|
||||
|
||||
restored = 0
|
||||
missing: list[str] = []
|
||||
for ident, (name, ty) in snapshot.items():
|
||||
var = by_ident.get(ident)
|
||||
if var is None:
|
||||
missing.append(ident)
|
||||
continue
|
||||
if name is not None:
|
||||
var.name = name
|
||||
var.renamed = True
|
||||
var.clear_hash()
|
||||
if ty is not None:
|
||||
varman.set_variable_type(var, ty, all_unified=True, mark_manual=True)
|
||||
restored += 1
|
||||
|
||||
return restored, sorted(missing)
|
||||
|
||||
|
||||
def function_summary(kb: KnowledgeBase, func_addr: int) -> dict[str, Any]:
|
||||
"""Small helper for edit results: what is currently cached for a function."""
|
||||
return {
|
||||
"flavors": sorted(kb.decompilations.available_flavors(func_addr)),
|
||||
"has_variables": kb.dec_variables.has_function_manager(func_addr),
|
||||
}
|
||||
56
angr/analyses/decompiler/edits/errors.py
Normal file
56
angr/analyses/decompiler/edits/errors.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from angr.errors import AngrError
|
||||
|
||||
|
||||
class DecompilationEditError(AngrError):
|
||||
"""Base class for every failure raised by the decompilation edit layer."""
|
||||
|
||||
|
||||
class FunctionNotFoundError(DecompilationEditError):
|
||||
"""No function matches the given address or name."""
|
||||
|
||||
|
||||
class AmbiguousFunctionError(DecompilationEditError):
|
||||
"""More than one function matches the given name. Renames make this reachable."""
|
||||
|
||||
def __init__(self, message: str, addresses: list[int] | None = None):
|
||||
super().__init__(message)
|
||||
self.addresses: list[int] = addresses if addresses is not None else []
|
||||
|
||||
|
||||
class VariableNotFoundError(DecompilationEditError):
|
||||
"""
|
||||
No variable in the decompilation matches the given display name.
|
||||
|
||||
``candidates`` carries the names that *are* available, so a caller that failed can correct
|
||||
itself without a second round trip.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, candidates: list[str] | None = None):
|
||||
super().__init__(message)
|
||||
self.candidates: list[str] = candidates if candidates is not None else []
|
||||
|
||||
|
||||
class NotDecompiledError(DecompilationEditError):
|
||||
"""The function has no cached decompilation, so there is nothing to edit."""
|
||||
|
||||
|
||||
class InvalidNameError(DecompilationEditError):
|
||||
"""The requested name is not a usable identifier."""
|
||||
|
||||
|
||||
class NameCollisionError(DecompilationEditError):
|
||||
"""The requested name is already bound to a different function, variable, or label."""
|
||||
|
||||
def __init__(self, message: str, existing: int | str | None = None):
|
||||
super().__init__(message)
|
||||
self.existing = existing
|
||||
|
||||
|
||||
class TypeParseError(DecompilationEditError):
|
||||
"""A C type declaration or function signature could not be parsed."""
|
||||
|
||||
|
||||
class UnsupportedEditError(DecompilationEditError):
|
||||
"""The requested edit is not supported for this kind of target."""
|
||||
93
angr/analyses/decompiler/edits/hooks.py
Normal file
93
angr/analyses/decompiler/edits/hooks.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from angr.knowledge_plugins.functions import Function
|
||||
from angr.sim_type import SimType, SimTypeFunction
|
||||
from angr.sim_variable import SimVariable
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class EditHooks(Protocol):
|
||||
"""
|
||||
Notifications fired immediately *before* each mutation, while the old value is still readable
|
||||
from the knowledge base.
|
||||
|
||||
The method set mirrors angr-management's plugin hooks one-for-one so its adapter is a pure
|
||||
forwarder. Firing before the mutation matches what the GUI's own edit dialogs do, and is what
|
||||
lets a handler snapshot pre-edit state.
|
||||
"""
|
||||
|
||||
def before_function_renamed(self, func: Function, old_name: str, new_name: str) -> None: ...
|
||||
|
||||
def before_stack_var_renamed(self, func: Function, offset: int, old_name: str, new_name: str) -> None: ...
|
||||
|
||||
def before_func_arg_renamed(self, func: Function, arg_index: int, old_name: str, new_name: str) -> None: ...
|
||||
|
||||
def before_global_var_renamed(self, addr: int, old_name: str, new_name: str) -> None: ...
|
||||
|
||||
def before_stack_var_retyped(
|
||||
self, func: Function, offset: int, old_type: SimType | None, new_type: SimType
|
||||
) -> None: ...
|
||||
|
||||
def before_func_arg_retyped(
|
||||
self, func: Function, arg_index: int, old_type: SimType | None, new_type: SimType
|
||||
) -> None: ...
|
||||
|
||||
def before_global_var_retyped(self, addr: int, old_type: SimType | None, new_type: SimType) -> None: ...
|
||||
|
||||
def before_other_var_retyped(self, var: SimVariable, old_type: SimType | None, new_type: SimType) -> None: ...
|
||||
|
||||
def before_function_retyped(
|
||||
self, func: Function, old_proto: SimTypeFunction | None, new_proto: SimTypeFunction
|
||||
) -> None: ...
|
||||
|
||||
def before_comment_changed(self, addr: int, old: str, new: str, created: bool, decomp: bool) -> None: ...
|
||||
|
||||
|
||||
class NullEditHooks:
|
||||
"""A concrete no-op implementation. Subclass it so an adapter only overrides what it needs."""
|
||||
|
||||
def before_function_renamed(self, func: Function, old_name: str, new_name: str) -> None:
|
||||
pass
|
||||
|
||||
def before_stack_var_renamed(self, func: Function, offset: int, old_name: str, new_name: str) -> None:
|
||||
pass
|
||||
|
||||
def before_func_arg_renamed(self, func: Function, arg_index: int, old_name: str, new_name: str) -> None:
|
||||
pass
|
||||
|
||||
def before_global_var_renamed(self, addr: int, old_name: str, new_name: str) -> None:
|
||||
pass
|
||||
|
||||
def before_stack_var_retyped(
|
||||
self, func: Function, offset: int, old_type: SimType | None, new_type: SimType
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def before_func_arg_retyped(
|
||||
self, func: Function, arg_index: int, old_type: SimType | None, new_type: SimType
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def before_global_var_retyped(self, addr: int, old_type: SimType | None, new_type: SimType) -> None:
|
||||
pass
|
||||
|
||||
def before_other_var_retyped(self, var: SimVariable, old_type: SimType | None, new_type: SimType) -> None:
|
||||
pass
|
||||
|
||||
def before_function_retyped(
|
||||
self, func: Function, old_proto: SimTypeFunction | None, new_proto: SimTypeFunction
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def before_comment_changed(self, addr: int, old: str, new: str, created: bool, decomp: bool) -> None:
|
||||
pass
|
||||
|
||||
|
||||
NULL_HOOKS = NullEditHooks()
|
||||
|
||||
|
||||
def coerce_hooks(hooks: EditHooks | None) -> EditHooks:
|
||||
return NULL_HOOKS if hooks is None else hooks
|
||||
638
angr/analyses/decompiler/edits/ops.py
Normal file
638
angr/analyses/decompiler/edits/ops.py
Normal file
|
|
@ -0,0 +1,638 @@
|
|||
"""
|
||||
The mutating operations of the edit layer.
|
||||
|
||||
Every operation re-fetches the decompilation cache by key at the point of mutation rather than
|
||||
holding a reference: see the note in :mod:`.cache`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from angr.knowledge_plugins.functions.function import PrototypeSource
|
||||
from angr.sim_type import parse_signature, parse_type
|
||||
from angr.sim_variable import SimMemoryVariable, SimStackVariable
|
||||
|
||||
from .cache import DEFAULT_FLAVOR, get_cache, invalidate, require_cache, restore_user_edits, snapshot_user_edits
|
||||
from .errors import NameCollisionError, TypeParseError, UnsupportedEditError
|
||||
from .hooks import coerce_hooks
|
||||
from .resolve import concrete_variables, list_variable_names, resolve_variable, validate_name
|
||||
from .results import EditResult, Refresh
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from angr.knowledge_base import KnowledgeBase
|
||||
from angr.knowledge_plugins.functions import Function
|
||||
from angr.project import Project
|
||||
from angr.sim_type import SimType, SimTypeFunction
|
||||
|
||||
from .hooks import EditHooks
|
||||
|
||||
l = logging.getLogger(name=__name__)
|
||||
|
||||
|
||||
def _require_free_function_name(kb: KnowledgeBase, name: str, own_addr: int) -> None:
|
||||
for other in kb.functions.get_by_name(name):
|
||||
if other is not None and other.addr != own_addr:
|
||||
raise NameCollisionError(
|
||||
f"A function named {name!r} already exists at {other.addr:#x}.", existing=other.addr
|
||||
)
|
||||
existing = kb.labels.lookup(name, None)
|
||||
if existing is not None and existing != own_addr:
|
||||
raise NameCollisionError(f"The label {name!r} is already bound to {existing:#x}.", existing=existing)
|
||||
|
||||
|
||||
def _set_arg_name(codegen, arg_index: int, new_name: str) -> None:
|
||||
"""
|
||||
Rewrite an argument's name in the rendered signature.
|
||||
|
||||
``cfunc.functy`` *is* ``func.prototype``, so this single write also updates the stored
|
||||
prototype -- do not write both.
|
||||
"""
|
||||
cfunc = getattr(codegen, "cfunc", None)
|
||||
if cfunc is None or cfunc.functy is None or not cfunc.functy.arg_names:
|
||||
return
|
||||
arg_names = list(cfunc.functy.arg_names)
|
||||
if 0 <= arg_index < len(arg_names):
|
||||
arg_names[arg_index] = new_name
|
||||
cfunc.functy.arg_names = tuple(arg_names)
|
||||
|
||||
|
||||
def rename_function(
|
||||
project: Project,
|
||||
func: Function,
|
||||
new_name: str,
|
||||
*,
|
||||
kb: KnowledgeBase | None = None,
|
||||
hooks: EditHooks | None = None,
|
||||
flavor: str = DEFAULT_FLAVOR,
|
||||
allow_overwrite: bool = True,
|
||||
strict_names: bool = True,
|
||||
rerender: bool = True,
|
||||
) -> EditResult:
|
||||
"""
|
||||
Rename a function.
|
||||
|
||||
Nothing is invalidated: other functions' cached ASTs reference the same Function object, so only
|
||||
their rendered text goes stale. That is reported through ``Refresh.text_stale_all`` for the
|
||||
caller to act on lazily.
|
||||
"""
|
||||
kb = project.kb if kb is None else kb
|
||||
hooks = coerce_hooks(hooks)
|
||||
validate_name(new_name, strict=strict_names)
|
||||
|
||||
old_name = func.name
|
||||
if old_name == new_name:
|
||||
return EditResult(changed=False, kind="function_name", func_addr=func.addr, old=old_name, new=new_name)
|
||||
|
||||
if not allow_overwrite:
|
||||
_require_free_function_name(kb, new_name, func.addr)
|
||||
|
||||
hooks.before_function_renamed(func, old_name, new_name)
|
||||
|
||||
kb.functions.get_by_addr(func.addr).name = new_name
|
||||
# the name setter does not clear this, and leaving it set makes the default-naming machinery
|
||||
# treat the function as still auto-named
|
||||
func.is_default_name = False
|
||||
|
||||
cache = get_cache(kb, func.addr, flavor)
|
||||
if cache is not None and cache.codegen is not None and getattr(cache.codegen, "cfunc", None) is not None:
|
||||
cache.codegen.cfunc.name = new_name
|
||||
cache.codegen.cfunc.demangled_name = new_name
|
||||
if rerender:
|
||||
cache.codegen.regenerate_text()
|
||||
|
||||
return EditResult(
|
||||
changed=True,
|
||||
kind="function_name",
|
||||
func_addr=func.addr,
|
||||
old=old_name,
|
||||
new=new_name,
|
||||
refresh=Refresh(
|
||||
text_stale=frozenset({func.addr}),
|
||||
text_stale_all=True,
|
||||
function_list_dirty=True,
|
||||
disassembly_dirty=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def rename_variable(
|
||||
project: Project,
|
||||
func: Function,
|
||||
variable_name: str,
|
||||
new_name: str,
|
||||
*,
|
||||
kb: KnowledgeBase | None = None,
|
||||
hooks: EditHooks | None = None,
|
||||
flavor: str = DEFAULT_FLAVOR,
|
||||
allow_overwrite: bool = True,
|
||||
strict_names: bool = True,
|
||||
rerender: bool = True,
|
||||
codegen=None,
|
||||
) -> EditResult:
|
||||
"""
|
||||
Rename a local, an argument, or a global as it appears in a function's decompilation.
|
||||
|
||||
Sets ``renamed`` on the variable, without which a later re-decompilation overwrites the name.
|
||||
"""
|
||||
kb = project.kb if kb is None else kb
|
||||
hooks = coerce_hooks(hooks)
|
||||
validate_name(new_name, strict=strict_names)
|
||||
|
||||
cache = require_cache(kb, func.addr, flavor)
|
||||
if codegen is None:
|
||||
codegen = cache.codegen
|
||||
|
||||
rv = resolve_variable(kb, func.addr, variable_name, codegen=codegen, flavor=flavor)
|
||||
old_name = rv.name
|
||||
|
||||
if old_name == new_name:
|
||||
return EditResult(
|
||||
changed=False,
|
||||
kind="variable_name",
|
||||
func_addr=func.addr,
|
||||
old=old_name,
|
||||
new=new_name,
|
||||
detail=rv.detail(),
|
||||
)
|
||||
|
||||
if not allow_overwrite and new_name in list_variable_names(codegen, kb, func.addr):
|
||||
raise NameCollisionError(f"A variable named {new_name!r} already exists in {func.name}.", existing=new_name)
|
||||
|
||||
# dispatch order matches the GUI's: a stack-backed argument fires the stack hook, not the
|
||||
# argument hook
|
||||
if rv.kind == "global":
|
||||
addr = rv.global_addr
|
||||
if addr is not None and addr in kb.functions:
|
||||
raise UnsupportedEditError(
|
||||
f"{variable_name!r} is the entry point of a function at {addr:#x}; "
|
||||
"rename it with rename_function instead."
|
||||
)
|
||||
hooks.before_global_var_renamed(addr, old_name, new_name)
|
||||
target = rv.variable
|
||||
elif rv.stack_offset is not None:
|
||||
hooks.before_stack_var_renamed(func, rv.stack_offset, old_name, new_name)
|
||||
target = rv.unified
|
||||
elif rv.kind == "argument":
|
||||
hooks.before_func_arg_renamed(func, rv.arg_index, old_name, new_name)
|
||||
target = rv.unified
|
||||
else:
|
||||
target = rv.unified
|
||||
|
||||
target.name = new_name
|
||||
target.renamed = True
|
||||
target.clear_hash()
|
||||
|
||||
if rv.kind == "global" and rv.global_addr is not None:
|
||||
kb.labels[rv.global_addr] = new_name
|
||||
if rv.kind == "argument" and rv.arg_index is not None:
|
||||
_set_arg_name(codegen, rv.arg_index, new_name)
|
||||
|
||||
if rerender:
|
||||
codegen.regenerate_text()
|
||||
|
||||
return EditResult(
|
||||
changed=True,
|
||||
kind="variable_name",
|
||||
func_addr=func.addr,
|
||||
old=old_name,
|
||||
new=new_name,
|
||||
refresh=Refresh(
|
||||
text_stale=frozenset({func.addr}),
|
||||
disassembly_dirty=rv.kind == "global",
|
||||
),
|
||||
detail=rv.detail(),
|
||||
)
|
||||
|
||||
|
||||
def _parse_type(c_type: str | SimType, arch) -> SimType:
|
||||
if not isinstance(c_type, str):
|
||||
return c_type.with_arch(arch)
|
||||
try:
|
||||
return parse_type(c_type).with_arch(arch)
|
||||
except Exception as ex: # pylint:disable=broad-exception-caught
|
||||
raise TypeParseError(f"Could not parse C type {c_type!r}: {ex}") from ex
|
||||
|
||||
|
||||
def _parse_prototype(prototype: str | SimTypeFunction, arch) -> SimTypeFunction:
|
||||
if not isinstance(prototype, str):
|
||||
return prototype.with_arch(arch)
|
||||
try:
|
||||
return parse_signature(prototype).with_arch(arch)
|
||||
except Exception as ex: # pylint:disable=broad-exception-caught
|
||||
raise TypeParseError(f"Could not parse C prototype {prototype!r}: {ex}") from ex
|
||||
|
||||
|
||||
def reflow_types(
|
||||
project: Project,
|
||||
func: Function,
|
||||
*,
|
||||
kb: KnowledgeBase | None = None,
|
||||
flavor: str = DEFAULT_FLAVOR,
|
||||
rerender: bool = True,
|
||||
):
|
||||
"""
|
||||
Re-run type inference over the cached constraints and refresh the rendered code.
|
||||
|
||||
Separate from :func:`set_variable_type` so a batch can retype many variables and reflow once;
|
||||
per-variable reflow would re-run Typehoon N times.
|
||||
|
||||
This is not a re-decompilation: the AST is untouched, so a retype that should change how an
|
||||
access renders (a struct field, an array index) needs a full re-decompilation instead.
|
||||
"""
|
||||
kb = project.kb if kb is None else kb
|
||||
|
||||
dec = project.analyses.Decompiler(func, decompile=False, use_cache=True, flavor=flavor)
|
||||
cache = require_cache(kb, func.addr, flavor)
|
||||
new_codegen = dec.reflow_variable_types(cache)
|
||||
if new_codegen is None:
|
||||
return None
|
||||
|
||||
cache.codegen = new_codegen
|
||||
if rerender:
|
||||
# reflow_variable_types ends at reload_variable_types(), which refreshes CVariable types but
|
||||
# does not re-render; without this the text stays stale
|
||||
new_codegen.regenerate_text()
|
||||
return new_codegen
|
||||
|
||||
|
||||
def _set_argument_type(
|
||||
project: Project,
|
||||
func: Function,
|
||||
rv,
|
||||
new_type: SimType,
|
||||
*,
|
||||
hooks: EditHooks,
|
||||
) -> EditResult:
|
||||
"""Retyping an argument means rewriting the prototype; the variable's own type is not enough."""
|
||||
proto = func.prototype
|
||||
if proto is None or rv.arg_index is None or rv.arg_index >= len(proto.args):
|
||||
raise UnsupportedEditError(
|
||||
f"Cannot retype argument {rv.name!r}: {func.name} has no prototype covering argument "
|
||||
f"index {rv.arg_index}. Set the whole prototype instead."
|
||||
)
|
||||
|
||||
old_type = proto.args[rv.arg_index]
|
||||
hooks.before_func_arg_retyped(func, rv.arg_index, old_type, new_type)
|
||||
|
||||
new_proto = proto.copy()
|
||||
args = list(new_proto.args)
|
||||
args[rv.arg_index] = new_type
|
||||
new_proto.args = tuple(args)
|
||||
func.prototype = new_proto.with_arch(project.arch)
|
||||
func.prototype_source = PrototypeSource.USER
|
||||
func.ran_cca = True
|
||||
|
||||
return EditResult(
|
||||
changed=True,
|
||||
kind="variable_type",
|
||||
func_addr=func.addr,
|
||||
old=str(old_type),
|
||||
new=str(new_type),
|
||||
refresh=Refresh(redecompile=frozenset({func.addr})),
|
||||
detail=rv.detail(),
|
||||
)
|
||||
|
||||
|
||||
def set_variable_type(
|
||||
project: Project,
|
||||
func: Function,
|
||||
variable_name: str,
|
||||
c_type: str | SimType,
|
||||
*,
|
||||
kb: KnowledgeBase | None = None,
|
||||
hooks: EditHooks | None = None,
|
||||
flavor: str = DEFAULT_FLAVOR,
|
||||
reflow: bool = True,
|
||||
rerender: bool = True,
|
||||
allow_prototype_change: bool = True,
|
||||
codegen=None,
|
||||
) -> EditResult:
|
||||
"""
|
||||
Change the type of a local, an argument, or a global.
|
||||
|
||||
Arguments are retyped by rewriting the function prototype, which requires a re-decompilation --
|
||||
the returned Refresh says so. Pass ``allow_prototype_change=False`` to refuse instead.
|
||||
"""
|
||||
kb = project.kb if kb is None else kb
|
||||
hooks = coerce_hooks(hooks)
|
||||
new_type = _parse_type(c_type, project.arch)
|
||||
|
||||
cache = require_cache(kb, func.addr, flavor)
|
||||
if codegen is None:
|
||||
codegen = cache.codegen
|
||||
|
||||
rv = resolve_variable(kb, func.addr, variable_name, codegen=codegen, flavor=flavor)
|
||||
|
||||
if rv.kind == "argument":
|
||||
if not allow_prototype_change:
|
||||
raise UnsupportedEditError(
|
||||
f"{variable_name!r} is an argument of {func.name}; change it by setting the whole prototype."
|
||||
)
|
||||
return _set_argument_type(project, func, rv, new_type, hooks=hooks)
|
||||
|
||||
if rv.kind == "global":
|
||||
varman = kb.dec_variables["global"]
|
||||
old_type = varman.get_variable_type(rv.variable)
|
||||
hooks.before_global_var_retyped(rv.global_addr, old_type, new_type)
|
||||
varman.set_variable_type(rv.variable, new_type, all_unified=False, mark_manual=True)
|
||||
else:
|
||||
varman = kb.dec_variables[func.addr]
|
||||
old_type = varman.get_variable_type(rv.variable)
|
||||
if rv.stack_offset is not None:
|
||||
hooks.before_stack_var_retyped(func, rv.stack_offset, old_type, new_type)
|
||||
else:
|
||||
hooks.before_other_var_retyped(rv.variable, old_type, new_type)
|
||||
# mark_manual is what makes the type survive re-inference: reflow reads
|
||||
# variables_with_manual_types as its ground truth
|
||||
for var in concrete_variables(varman, rv):
|
||||
varman.set_variable_type(var, new_type, all_unified=True, mark_manual=True)
|
||||
|
||||
if reflow:
|
||||
reflow_types(project, func, kb=kb, flavor=flavor, rerender=rerender)
|
||||
|
||||
return EditResult(
|
||||
changed=True,
|
||||
kind="variable_type",
|
||||
func_addr=func.addr,
|
||||
old=None if old_type is None else str(old_type),
|
||||
new=str(new_type),
|
||||
refresh=Refresh(text_stale=frozenset({func.addr})),
|
||||
detail=rv.detail(),
|
||||
)
|
||||
|
||||
|
||||
def set_function_prototype(
|
||||
project: Project,
|
||||
func: Function,
|
||||
prototype: str | SimTypeFunction,
|
||||
*,
|
||||
kb: KnowledgeBase | None = None,
|
||||
hooks: EditHooks | None = None,
|
||||
flavor: str = DEFAULT_FLAVOR,
|
||||
invalidate_cache: bool = True,
|
||||
preserve_user_edits: bool = True,
|
||||
redecompile: bool = False,
|
||||
) -> EditResult:
|
||||
"""
|
||||
Set a function's prototype.
|
||||
|
||||
The function name inside the signature is ignored; use :func:`rename_function` to rename.
|
||||
|
||||
Dropping kb.dec_variables is what makes new argument names take effect, but it also discards
|
||||
every rename and manual type for the function. Those are snapshotted and, when this function
|
||||
re-decompiles, restored. Otherwise the snapshot is returned in ``detail["user_edits"]`` so a
|
||||
caller that decompiles asynchronously can restore it once its own job finishes.
|
||||
"""
|
||||
kb = project.kb if kb is None else kb
|
||||
hooks = coerce_hooks(hooks)
|
||||
new_proto = _parse_prototype(prototype, project.arch)
|
||||
|
||||
old_proto = func.prototype
|
||||
hooks.before_function_retyped(func, old_proto, new_proto)
|
||||
|
||||
snapshot = snapshot_user_edits(kb, func.addr) if preserve_user_edits else {}
|
||||
|
||||
func.prototype = new_proto
|
||||
func.prototype_source = PrototypeSource.USER
|
||||
# keep CompleteCallingConventions from overwriting a user-supplied prototype
|
||||
func.ran_cca = True
|
||||
|
||||
if invalidate_cache:
|
||||
invalidate(kb, func.addr, flavors=None, drop_variables=True)
|
||||
|
||||
detail: dict = {"user_edits": snapshot}
|
||||
code = None
|
||||
if redecompile:
|
||||
dec = project.analyses.Decompiler(func, flavor=flavor)
|
||||
if snapshot:
|
||||
restored, missing = restore_user_edits(kb, func.addr, snapshot)
|
||||
detail.update({"restored_user_edits": restored, "unrestored_user_edits": missing})
|
||||
detail["user_edits"] = {}
|
||||
if restored and dec.codegen is not None:
|
||||
dec.codegen.regenerate_text()
|
||||
code = dec.codegen.text if dec.codegen is not None else None
|
||||
detail["code"] = code
|
||||
|
||||
return EditResult(
|
||||
changed=True,
|
||||
kind="prototype",
|
||||
func_addr=func.addr,
|
||||
old=None if old_proto is None else str(old_proto),
|
||||
new=str(new_proto),
|
||||
refresh=Refresh(redecompile=frozenset({func.addr}), function_list_dirty=True),
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
|
||||
_ORPHAN_MARKER = "// Orphaned comments"
|
||||
|
||||
|
||||
def _snap_comment_addr(codegen, addr: int) -> int:
|
||||
"""
|
||||
Snap to the nearest address the codegen tracks, at or below ``addr``.
|
||||
|
||||
stmt_comments is keyed by ins_addr, so an address the codegen never emits cannot match. This
|
||||
only maps an arbitrary in-function address onto a tracked one; whether the result actually
|
||||
renders inline additionally depends on it being the *last* tracked address on its line, which
|
||||
is not derivable from the position maps -- map_addr_to_pos records each address's first
|
||||
position, not its last. :func:`_rendered_inline` reports the real outcome afterwards.
|
||||
"""
|
||||
insmap = getattr(codegen, "map_addr_to_pos", None)
|
||||
if insmap is None:
|
||||
return addr
|
||||
rendered = [ins_addr for ins_addr, _ in insmap.items()]
|
||||
if not rendered or addr in set(rendered):
|
||||
return addr
|
||||
below = [a for a in rendered if a < addr]
|
||||
return max(below) if below else addr
|
||||
|
||||
|
||||
def _rendered_inline(codegen, comment: str) -> bool:
|
||||
"""Whether a comment rendered next to a statement rather than in the orphaned block."""
|
||||
text = getattr(codegen, "text", None) or ""
|
||||
marker = text.find(_ORPHAN_MARKER)
|
||||
return marker == -1 or comment not in text[marker:]
|
||||
|
||||
|
||||
def set_comment(
|
||||
project: Project,
|
||||
addr: int,
|
||||
comment: str | None,
|
||||
*,
|
||||
kb: KnowledgeBase | None = None,
|
||||
hooks: EditHooks | None = None,
|
||||
flavor: str = DEFAULT_FLAVOR,
|
||||
mirror_to_pseudocode: bool = True,
|
||||
snap: bool = True,
|
||||
rerender: bool = True,
|
||||
) -> EditResult:
|
||||
"""
|
||||
Set the comment at an address, or clear it with an empty string or None.
|
||||
|
||||
The comment goes into kb.comments, which the disassembly renders and which the decompiler reads
|
||||
for the function header. Per-statement pseudocode comments live in codegen.stmt_comments
|
||||
instead, so both are written -- except at the function entry, where kb.comments is already what
|
||||
the header renders and mirroring would show the comment twice.
|
||||
"""
|
||||
kb = project.kb if kb is None else kb
|
||||
hooks = coerce_hooks(hooks)
|
||||
text = comment or ""
|
||||
|
||||
old = kb.comments.get(addr, "")
|
||||
existed = addr in kb.comments
|
||||
|
||||
hooks.before_comment_changed(addr, old, text, not existed, False)
|
||||
if text:
|
||||
kb.comments[addr] = text
|
||||
elif existed:
|
||||
del kb.comments[addr]
|
||||
|
||||
in_pseudocode = False
|
||||
inline: bool | None = None
|
||||
snapped_from = None
|
||||
|
||||
func = kb.functions.floor_func(addr) if mirror_to_pseudocode else None
|
||||
if func is not None and not (func.addr <= addr < func.addr + max(func.size, 1)):
|
||||
func = None
|
||||
|
||||
if func is not None:
|
||||
cache = get_cache(kb, func.addr, flavor)
|
||||
codegen = cache.codegen if cache is not None else None
|
||||
if codegen is not None:
|
||||
if addr == func.addr:
|
||||
# rendered as the header comment straight from kb.comments; mirroring it into
|
||||
# stmt_comments would show it twice
|
||||
in_pseudocode = bool(text)
|
||||
if rerender:
|
||||
codegen.regenerate_text()
|
||||
else:
|
||||
target = _snap_comment_addr(codegen, addr) if snap else addr
|
||||
if target != addr:
|
||||
snapped_from = addr
|
||||
cdict = codegen.stmt_comments
|
||||
prev = cdict.get(target, "")
|
||||
hooks.before_comment_changed(target, prev, text, target not in cdict, True)
|
||||
if text:
|
||||
cdict[target] = text
|
||||
in_pseudocode = True
|
||||
elif target in cdict:
|
||||
del cdict[target]
|
||||
if rerender:
|
||||
codegen.regenerate_text()
|
||||
if in_pseudocode:
|
||||
inline = _rendered_inline(codegen, text)
|
||||
|
||||
return EditResult(
|
||||
changed=old != text,
|
||||
kind="comment",
|
||||
func_addr=None if func is None else func.addr,
|
||||
old=old,
|
||||
new=text,
|
||||
refresh=Refresh(
|
||||
text_stale=frozenset() if func is None else frozenset({func.addr}),
|
||||
disassembly_dirty=True,
|
||||
),
|
||||
detail={
|
||||
"address": hex(addr),
|
||||
"shown_in_pseudocode": in_pseudocode,
|
||||
# None when not re-rendered here, so the caller cannot yet know
|
||||
"rendered_inline": inline,
|
||||
"snapped_from": None if snapped_from is None else hex(snapped_from),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def global_variable_at(kb: KnowledgeBase, addr: int):
|
||||
"""The global SimVariable recorded at an address, if variable recovery produced one."""
|
||||
varman = kb.dec_variables["global"]
|
||||
for var in varman.get_variables(sort=None):
|
||||
if isinstance(var, SimMemoryVariable) and not isinstance(var, SimStackVariable) and var.addr == addr:
|
||||
return var
|
||||
return None
|
||||
|
||||
|
||||
def _reject_function_entry(kb: KnowledgeBase, addr: int, what: str) -> None:
|
||||
"""kb.labels[addr] = name renames the function when addr is a function entry."""
|
||||
if addr in kb.functions:
|
||||
raise UnsupportedEditError(
|
||||
f"{addr:#x} is the entry point of a function; {what} it with the function-level operation instead."
|
||||
)
|
||||
|
||||
|
||||
def rename_global(
|
||||
project: Project,
|
||||
addr: int,
|
||||
new_name: str,
|
||||
*,
|
||||
kb: KnowledgeBase | None = None,
|
||||
hooks: EditHooks | None = None,
|
||||
allow_overwrite: bool = True,
|
||||
strict_names: bool = True,
|
||||
) -> EditResult:
|
||||
"""Rename a global by address, without needing a function whose decompilation shows it."""
|
||||
kb = project.kb if kb is None else kb
|
||||
hooks = coerce_hooks(hooks)
|
||||
validate_name(new_name, strict=strict_names)
|
||||
_reject_function_entry(kb, addr, "rename")
|
||||
|
||||
var = global_variable_at(kb, addr)
|
||||
old_name = var.name if var is not None and var.name else kb.labels.get(addr, "")
|
||||
if old_name == new_name:
|
||||
return EditResult(changed=False, kind="global_name", old=old_name, new=new_name)
|
||||
|
||||
if not allow_overwrite:
|
||||
existing = kb.labels.lookup(new_name, None)
|
||||
if existing is not None and existing != addr:
|
||||
raise NameCollisionError(f"The label {new_name!r} is already bound to {existing:#x}.", existing=existing)
|
||||
|
||||
hooks.before_global_var_renamed(addr, old_name, new_name)
|
||||
kb.labels[addr] = new_name
|
||||
if var is not None:
|
||||
var.name = new_name
|
||||
var.renamed = True
|
||||
var.clear_hash()
|
||||
|
||||
return EditResult(
|
||||
changed=True,
|
||||
kind="global_name",
|
||||
old=old_name,
|
||||
new=new_name,
|
||||
refresh=Refresh(text_stale_all=True, disassembly_dirty=True),
|
||||
detail={"global_address": hex(addr)},
|
||||
)
|
||||
|
||||
|
||||
def set_global_type(
|
||||
project: Project,
|
||||
addr: int,
|
||||
c_type: str | SimType,
|
||||
*,
|
||||
kb: KnowledgeBase | None = None,
|
||||
hooks: EditHooks | None = None,
|
||||
) -> EditResult:
|
||||
"""Set the type of a global by address."""
|
||||
kb = project.kb if kb is None else kb
|
||||
hooks = coerce_hooks(hooks)
|
||||
new_type = _parse_type(c_type, project.arch)
|
||||
|
||||
var = global_variable_at(kb, addr)
|
||||
if var is None:
|
||||
raise UnsupportedEditError(
|
||||
f"No global variable is recorded at {addr:#x}. Decompile a function that references it first."
|
||||
)
|
||||
|
||||
varman = kb.dec_variables["global"]
|
||||
old_type = varman.get_variable_type(var)
|
||||
hooks.before_global_var_retyped(addr, old_type, new_type)
|
||||
varman.set_variable_type(var, new_type, all_unified=False, mark_manual=True)
|
||||
|
||||
return EditResult(
|
||||
changed=True,
|
||||
kind="global_type",
|
||||
old=None if old_type is None else str(old_type),
|
||||
new=str(new_type),
|
||||
refresh=Refresh(text_stale_all=True),
|
||||
detail={"global_address": hex(addr)},
|
||||
)
|
||||
342
angr/analyses/decompiler/edits/resolve.py
Normal file
342
angr/analyses/decompiler/edits/resolve.py
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
"""
|
||||
Resolution helpers shared by every edit operation: address-or-name to Function, and pseudocode
|
||||
display name to the underlying SimVariable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from angr.sim_variable import SimMemoryVariable, SimStackVariable
|
||||
|
||||
from .cache import DEFAULT_FLAVOR, require_cache
|
||||
from .errors import (
|
||||
AmbiguousFunctionError,
|
||||
FunctionNotFoundError,
|
||||
InvalidNameError,
|
||||
VariableNotFoundError,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from angr.analyses.decompiler.structured_codegen.c import CVariable
|
||||
from angr.knowledge_base import KnowledgeBase
|
||||
from angr.knowledge_plugins.functions import Function
|
||||
from angr.sim_variable import SimVariable
|
||||
|
||||
_C_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||
|
||||
VariableKind = Literal["argument", "local", "global"]
|
||||
|
||||
|
||||
def parse_address(value: str | int) -> int:
|
||||
"""Parse an address given as an int or a string. Accepts 0x-prefixed hex and decimal."""
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
try:
|
||||
return int(value, 0)
|
||||
except (TypeError, ValueError) as ex:
|
||||
raise InvalidNameError(f'Invalid address {value!r}. Pass a hex string such as "0x401000".') from ex
|
||||
|
||||
|
||||
def validate_name(name: str, *, strict: bool = True) -> None:
|
||||
"""
|
||||
Reject names that cannot be used as identifiers.
|
||||
|
||||
``strict`` requires a C identifier. Without it only whitespace-free names are required, which
|
||||
still admits things like ``int;`` that render as uncompilable C -- so strict is the default.
|
||||
"""
|
||||
if not name:
|
||||
raise InvalidNameError("Name must not be empty.")
|
||||
if strict:
|
||||
if _C_IDENTIFIER_RE.match(name) is None:
|
||||
raise InvalidNameError(
|
||||
f"Invalid name {name!r}: must be a C identifier (letters, digits and underscores, "
|
||||
"not starting with a digit)."
|
||||
)
|
||||
elif re.search(r"\s", name) is not None:
|
||||
raise InvalidNameError(f"Invalid name {name!r}: names must not contain whitespace.")
|
||||
|
||||
|
||||
def resolve_function(
|
||||
kb: KnowledgeBase,
|
||||
*,
|
||||
address: str | int | None = None,
|
||||
name: str | None = None,
|
||||
containing: bool = True,
|
||||
) -> Function:
|
||||
"""
|
||||
Find a function by address or by name.
|
||||
|
||||
:param containing: If True, an address inside a function resolves to that function rather
|
||||
than requiring the exact entry address.
|
||||
"""
|
||||
if address is None and name is None:
|
||||
raise FunctionNotFoundError("Specify either an address or a name.")
|
||||
|
||||
if address is not None:
|
||||
addr = parse_address(address)
|
||||
func = kb.functions.get(addr)
|
||||
if func is None and containing:
|
||||
func = kb.functions.floor_func(addr)
|
||||
if func is not None and not (func.addr <= addr < func.addr + max(func.size, 1)):
|
||||
func = None
|
||||
if func is None:
|
||||
raise FunctionNotFoundError(f"No function found at address {addr:#x}.")
|
||||
return func
|
||||
|
||||
matches = [f for f in kb.functions.get_by_name(name) if f is not None]
|
||||
if not matches:
|
||||
raise FunctionNotFoundError(f"No function named {name!r}.")
|
||||
if len(matches) > 1:
|
||||
addrs = sorted(f.addr for f in matches)
|
||||
raise AmbiguousFunctionError(
|
||||
f"{len(matches)} functions are named {name!r}: {', '.join(hex(a) for a in addrs)}. "
|
||||
"Specify an address instead.",
|
||||
addresses=addrs,
|
||||
)
|
||||
return matches[0]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResolvedVariable:
|
||||
"""
|
||||
A pseudocode display name resolved to the objects an edit needs.
|
||||
|
||||
``variable`` is the concrete/SSA variable that ``set_variable_type`` expects; ``unified`` is the
|
||||
unified variable that a rename mutates (None for globals, which have no unified form).
|
||||
"""
|
||||
|
||||
kind: VariableKind
|
||||
variable: SimVariable
|
||||
unified: SimVariable | None
|
||||
cvar: CVariable | None = None
|
||||
arg_index: int | None = None
|
||||
stack_offset: int | None = None
|
||||
global_addr: int | None = None
|
||||
ambiguous: bool = False
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
target = self.variable if self.unified is None else self.unified
|
||||
return target.name
|
||||
|
||||
@property
|
||||
def storage(self) -> str:
|
||||
if self.stack_offset is not None:
|
||||
return "stack"
|
||||
if isinstance(self.variable, SimMemoryVariable) and not isinstance(self.variable, SimStackVariable):
|
||||
return "memory"
|
||||
return "register"
|
||||
|
||||
def detail(self) -> dict:
|
||||
return {
|
||||
"storage": self.storage,
|
||||
"is_argument": self.kind == "argument",
|
||||
"stack_offset": self.stack_offset,
|
||||
"arg_index": self.arg_index,
|
||||
"global_address": None if self.global_addr is None else hex(self.global_addr),
|
||||
}
|
||||
|
||||
|
||||
def _stack_offset(var: SimVariable) -> int | None:
|
||||
return var.offset if isinstance(var, SimStackVariable) else None
|
||||
|
||||
|
||||
def _iter_candidates(kb: KnowledgeBase, func_addr: int, codegen) -> list[ResolvedVariable]:
|
||||
"""
|
||||
Collect every addressable variable, in precedence order: arguments, then locals, then globals.
|
||||
|
||||
Locals come from the codegen (which carries the concrete variable) unioned with the variable
|
||||
manager's unified variables, so unified variables not currently referenced in the body are still
|
||||
addressable. Globals come from cexterns first, then any non-stack memory variable in use, then a
|
||||
walk of the position map for inline references that cexterns filters out.
|
||||
"""
|
||||
out: list[ResolvedVariable] = []
|
||||
seen: set[int] = set()
|
||||
|
||||
cfunc = getattr(codegen, "cfunc", None)
|
||||
|
||||
if cfunc is not None and cfunc.arg_list:
|
||||
for idx, cvar in enumerate(cfunc.arg_list):
|
||||
var = getattr(cvar, "variable", None)
|
||||
if var is None:
|
||||
continue
|
||||
unified = getattr(cvar, "unified_variable", None) or var
|
||||
out.append(
|
||||
ResolvedVariable(
|
||||
kind="argument",
|
||||
variable=var,
|
||||
unified=unified,
|
||||
cvar=cvar,
|
||||
arg_index=idx,
|
||||
stack_offset=_stack_offset(var),
|
||||
)
|
||||
)
|
||||
seen.add(id(unified))
|
||||
|
||||
if cfunc is not None and cfunc.variables_in_use:
|
||||
for var, cvar in cfunc.variables_in_use.items():
|
||||
unified = getattr(cvar, "unified_variable", None)
|
||||
if unified is None or id(unified) in seen:
|
||||
continue
|
||||
out.append(
|
||||
ResolvedVariable(
|
||||
kind="local",
|
||||
variable=var,
|
||||
unified=unified,
|
||||
cvar=cvar,
|
||||
stack_offset=_stack_offset(var),
|
||||
)
|
||||
)
|
||||
seen.add(id(unified))
|
||||
|
||||
if kb.dec_variables.has_function_manager(func_addr):
|
||||
varman = kb.dec_variables[func_addr]
|
||||
concrete_by_unified: dict[int, SimVariable] = {}
|
||||
for var in varman.get_variables(sort=None):
|
||||
unified = varman.unified_variable(var)
|
||||
if unified is not None:
|
||||
concrete_by_unified.setdefault(id(unified), var)
|
||||
for unified in varman.get_unified_variables(sort=None):
|
||||
if id(unified) in seen:
|
||||
continue
|
||||
# prefer a concrete SSA variable: set_variable_type(all_unified=True) keys off the
|
||||
# SSA-to-unified map, so passing the unified variable would not propagate
|
||||
concrete = concrete_by_unified.get(id(unified), unified)
|
||||
out.append(
|
||||
ResolvedVariable(
|
||||
kind="local",
|
||||
variable=concrete,
|
||||
unified=unified,
|
||||
stack_offset=_stack_offset(concrete),
|
||||
)
|
||||
)
|
||||
seen.add(id(unified))
|
||||
|
||||
for cvar in _iter_global_cvars(codegen, cfunc):
|
||||
var = cvar.variable
|
||||
if var is None or id(var) in seen:
|
||||
continue
|
||||
out.append(
|
||||
ResolvedVariable(
|
||||
kind="global",
|
||||
variable=var,
|
||||
unified=None,
|
||||
cvar=cvar,
|
||||
global_addr=getattr(var, "addr", None),
|
||||
)
|
||||
)
|
||||
seen.add(id(var))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def concrete_variables(varman, resolved: ResolvedVariable) -> list[SimVariable]:
|
||||
"""
|
||||
Every SSA variable sharing the resolved variable's unified form.
|
||||
|
||||
Retyping applies to all of them. Computed here rather than relying on
|
||||
``set_variable_type(all_unified=True)``, which silently does nothing when the variable it is
|
||||
handed is not a key in the SSA-to-unified map.
|
||||
"""
|
||||
if resolved.unified is None:
|
||||
return [resolved.variable]
|
||||
|
||||
out = [var for var in varman.get_variables(sort=None) if varman.unified_variable(var) is resolved.unified]
|
||||
if not any(var is resolved.variable for var in out):
|
||||
out.append(resolved.variable)
|
||||
return out
|
||||
|
||||
|
||||
def _iter_global_cvars(codegen, cfunc):
|
||||
from angr.analyses.decompiler.structured_codegen.c import CVariable # pylint:disable=import-outside-toplevel
|
||||
|
||||
if codegen.cexterns:
|
||||
yield from codegen.cexterns
|
||||
|
||||
if cfunc is not None and cfunc.variables_in_use:
|
||||
for var, cvar in cfunc.variables_in_use.items():
|
||||
if (
|
||||
getattr(cvar, "unified_variable", None) is None
|
||||
and isinstance(var, SimMemoryVariable)
|
||||
and not isinstance(var, SimStackVariable)
|
||||
):
|
||||
yield cvar
|
||||
|
||||
# inline references (strings, function pointers) that cexterns filters out
|
||||
if codegen.map_pos_to_node is not None:
|
||||
for item in codegen.map_pos_to_node.values():
|
||||
obj = getattr(item, "obj", None)
|
||||
if isinstance(obj, CVariable) and obj.unified_variable is None and obj.variable is not None:
|
||||
yield obj
|
||||
|
||||
|
||||
def list_variable_names(codegen, kb: KnowledgeBase | None = None, func_addr: int | None = None) -> list[str]:
|
||||
"""Every display name addressable in this decompilation, for error messages."""
|
||||
if kb is None or func_addr is None:
|
||||
cfunc = getattr(codegen, "cfunc", None)
|
||||
names = set()
|
||||
if cfunc is not None:
|
||||
for cvar in cfunc.arg_list or []:
|
||||
if cvar.name:
|
||||
names.add(cvar.name)
|
||||
for cvar in (cfunc.variables_in_use or {}).values():
|
||||
if cvar.name:
|
||||
names.add(cvar.name)
|
||||
return sorted(names)
|
||||
|
||||
return sorted({rv.name for rv in _iter_candidates(kb, func_addr, codegen) if rv.name})
|
||||
|
||||
|
||||
def resolve_variable(
|
||||
kb: KnowledgeBase,
|
||||
func_addr: int,
|
||||
display_name: str,
|
||||
*,
|
||||
codegen=None,
|
||||
flavor: str = DEFAULT_FLAVOR,
|
||||
) -> ResolvedVariable:
|
||||
"""
|
||||
Resolve a name as it appears in the pseudocode to the underlying variable.
|
||||
|
||||
Precedence is argument > local > global. A name matching more than one variable within the same
|
||||
bucket resolves deterministically and sets ``ambiguous``, rather than failing -- a batch edit
|
||||
should be able to report the collision and continue.
|
||||
"""
|
||||
if codegen is None:
|
||||
codegen = require_cache(kb, func_addr, flavor).codegen
|
||||
|
||||
candidates = _iter_candidates(kb, func_addr, codegen)
|
||||
matches = [rv for rv in candidates if rv.name == display_name]
|
||||
|
||||
if not matches:
|
||||
raise VariableNotFoundError(
|
||||
f"No variable named {display_name!r} in the decompilation of function {func_addr:#x}.",
|
||||
candidates=sorted({rv.name for rv in candidates if rv.name}),
|
||||
)
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
|
||||
order = {"argument": 0, "local": 1, "global": 2}
|
||||
matches.sort(
|
||||
key=lambda rv: (
|
||||
order[rv.kind],
|
||||
rv.arg_index if rv.arg_index is not None else 0,
|
||||
rv.variable.ident or "",
|
||||
rv.global_addr if rv.global_addr is not None else 0,
|
||||
)
|
||||
)
|
||||
best = matches[0]
|
||||
return ResolvedVariable(
|
||||
kind=best.kind,
|
||||
variable=best.variable,
|
||||
unified=best.unified,
|
||||
cvar=best.cvar,
|
||||
arg_index=best.arg_index,
|
||||
stack_offset=best.stack_offset,
|
||||
global_addr=best.global_addr,
|
||||
ambiguous=True,
|
||||
)
|
||||
46
angr/analyses/decompiler/edits/results.py
Normal file
46
angr/analyses/decompiler/edits/results.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Refresh:
|
||||
"""
|
||||
What a caller has to redo after an edit.
|
||||
|
||||
Lets a UI pick between re-rendering text, rebuilding the codegen AST, and a full
|
||||
re-decompilation instead of guessing. ``text_stale_all`` means every cached decompilation's
|
||||
rendered text is stale (a function rename changes call sites everywhere), but the caller should
|
||||
re-render lazily: eagerly re-rendering the whole cache would thrash its LRU.
|
||||
"""
|
||||
|
||||
text_stale: frozenset[int] = frozenset()
|
||||
text_stale_all: bool = False
|
||||
reanalyze: frozenset[int] = frozenset()
|
||||
redecompile: frozenset[int] = frozenset()
|
||||
function_list_dirty: bool = False
|
||||
disassembly_dirty: bool = False
|
||||
|
||||
def merge(self, other: Refresh) -> Refresh:
|
||||
return Refresh(
|
||||
text_stale=self.text_stale | other.text_stale,
|
||||
text_stale_all=self.text_stale_all or other.text_stale_all,
|
||||
reanalyze=self.reanalyze | other.reanalyze,
|
||||
redecompile=self.redecompile | other.redecompile,
|
||||
function_list_dirty=self.function_list_dirty or other.function_list_dirty,
|
||||
disassembly_dirty=self.disassembly_dirty or other.disassembly_dirty,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EditResult:
|
||||
"""The outcome of a single edit. ``changed`` is False when the edit was a no-op."""
|
||||
|
||||
changed: bool
|
||||
kind: str
|
||||
func_addr: int | None = None
|
||||
old: Any = None
|
||||
new: Any = None
|
||||
refresh: Refresh = field(default_factory=Refresh)
|
||||
detail: dict[str, Any] = field(default_factory=dict)
|
||||
|
|
@ -122,7 +122,9 @@ class EffectiveSizeExtractor(AILBlockWalker[None, None, None]):
|
|||
super()._handle_expr(expr_idx, expr, stmt_idx, stmt, block)
|
||||
|
||||
def _handle_Insert(self, expr_idx: int, expr, stmt_idx: int, stmt: Statement | None, block: Block | None):
|
||||
# self._handle_expr(0, expr.base, stmt_idx, stmt, block)
|
||||
# the base of an Insert is consumed at full width: every byte outside the inserted range is preserved
|
||||
# into the result, so narrowing the base (and zero-extending it back) would destroy those bytes
|
||||
self._handle_expr(0, expr.base, stmt_idx, stmt, block)
|
||||
if isinstance(expr.base, VirtualVariable):
|
||||
self.vvars_used_as_insert_base.add(expr.base.varid)
|
||||
self._handle_expr(1, expr.offset, stmt_idx, stmt, block)
|
||||
|
|
@ -270,11 +272,14 @@ class ExpressionNarrower(AILBlockRewriter):
|
|||
return super().walk(block)
|
||||
|
||||
def _handle_Assignment(self, stmt_idx: int, stmt: Assignment, block: Block | None) -> Assignment:
|
||||
if isinstance(stmt.src, Phi):
|
||||
src_in = stmt.src
|
||||
dst_in = stmt.dst
|
||||
|
||||
if isinstance(src_in, Phi):
|
||||
changed = False
|
||||
|
||||
src_and_vvars = []
|
||||
for src, vvar in stmt.src.src_and_vvars:
|
||||
for src, vvar in src_in.src_and_vvars:
|
||||
if vvar is None:
|
||||
src_and_vvars.append((src, None))
|
||||
continue
|
||||
|
|
@ -296,39 +301,39 @@ class ExpressionNarrower(AILBlockRewriter):
|
|||
|
||||
src_and_vvars.append((src, new_var))
|
||||
|
||||
new_src = Phi(stmt.src.idx, stmt.src.bits, src_and_vvars, **stmt.src.tags)
|
||||
new_src = Phi(src_in.idx, src_in.bits, src_and_vvars, **src_in.tags)
|
||||
|
||||
else:
|
||||
new_src = self._handle_expr(1, stmt.src, stmt_idx, stmt, block)
|
||||
changed = new_src is not stmt.src
|
||||
new_src = self._handle_expr(1, src_in, stmt_idx, stmt, block)
|
||||
changed = new_src != src_in
|
||||
|
||||
if isinstance(stmt.dst, VirtualVariable) and stmt.dst.varid in self.new_vvar_sizes:
|
||||
if isinstance(dst_in, VirtualVariable) and dst_in.varid in self.new_vvar_sizes:
|
||||
changed = True
|
||||
new_dst = VirtualVariable(
|
||||
stmt.dst.idx,
|
||||
stmt.dst.varid,
|
||||
self.new_vvar_sizes[stmt.dst.varid] * self.project.arch.byte_width,
|
||||
category=stmt.dst.category,
|
||||
oident=stmt.dst.oident,
|
||||
**stmt.dst.tags,
|
||||
dst_in.idx,
|
||||
dst_in.varid,
|
||||
self.new_vvar_sizes[dst_in.varid] * self.project.arch.byte_width,
|
||||
category=dst_in.category,
|
||||
oident=dst_in.oident,
|
||||
**dst_in.tags,
|
||||
)
|
||||
|
||||
self.replacement_core_vvars[new_dst.varid].append(new_dst)
|
||||
|
||||
if isinstance(new_src, Phi):
|
||||
new_src.bits = self.new_vvar_sizes[stmt.dst.varid] * self.project.arch.byte_width
|
||||
new_src.bits = self.new_vvar_sizes[dst_in.varid] * self.project.arch.byte_width
|
||||
else:
|
||||
new_src = Convert(
|
||||
self.manager.next_atom(),
|
||||
stmt.src.bits,
|
||||
self.new_vvar_sizes[stmt.dst.varid] * self.project.arch.byte_width,
|
||||
src_in.bits,
|
||||
self.new_vvar_sizes[dst_in.varid] * self.project.arch.byte_width,
|
||||
False,
|
||||
new_src,
|
||||
**new_src.tags,
|
||||
)
|
||||
else:
|
||||
new_dst = self._handle_expr(0, stmt.dst, stmt_idx, stmt, block)
|
||||
changed |= new_dst is not stmt.dst
|
||||
new_dst = self._handle_expr(0, dst_in, stmt_idx, stmt, block)
|
||||
changed |= new_dst != dst_in
|
||||
|
||||
if changed:
|
||||
self.narrowed_any = True
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import networkx
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.ailment.block import Block
|
||||
|
||||
from .utils import find_block_by_addr
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# pylint:disable=unused-argument
|
||||
from __future__ import annotations
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
|
||||
from .sequence_walker import SequenceWalker
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
|
||||
from collections import defaultdict
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
|
||||
from .sequence_walker import SequenceWalker
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ from __future__ import annotations
|
|||
|
||||
from .decompilation_note import DecompilationNote, DecompilationNoteLevel
|
||||
|
||||
# importing the module registers the subclass for DecompilationNote.from_json dispatch
|
||||
from .deobfuscated_strings import DeobfuscatedString, DeobfuscatedStringsNote
|
||||
|
||||
__all__ = (
|
||||
"DecompilationNote",
|
||||
"DecompilationNoteLevel",
|
||||
"DeobfuscatedString",
|
||||
"DeobfuscatedStringsNote",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
l = logging.getLogger(name=__name__)
|
||||
|
||||
|
||||
class DecompilationNoteLevel(Enum):
|
||||
"""
|
||||
|
|
@ -28,6 +32,8 @@ class DecompilationNote:
|
|||
DecompilationNoteLevel.INFO, DecompilationNoteLevel.WARNING, and DecompilationNoteLevel.CRITICAL.
|
||||
"""
|
||||
|
||||
_subclasses: dict[str, type[DecompilationNote]] = {}
|
||||
|
||||
__slots__ = (
|
||||
"content",
|
||||
"key",
|
||||
|
|
@ -41,8 +47,51 @@ class DecompilationNote:
|
|||
self.content = content
|
||||
self.level = level
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
DecompilationNote._subclasses[cls.__name__] = cls
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DecompilationNote: {self.name}>"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}: {self.content}"
|
||||
|
||||
#
|
||||
# JSON serialization
|
||||
#
|
||||
|
||||
def to_jsonable(self) -> dict[str, Any]:
|
||||
try:
|
||||
content = json.loads(json.dumps(self.content))
|
||||
except (TypeError, ValueError):
|
||||
l.warning("Failed to serialize content of decompilation note %s to JSON", self.key)
|
||||
content = None
|
||||
return {
|
||||
"class": type(self).__name__,
|
||||
"key": self.key,
|
||||
"name": self.name,
|
||||
"content": content,
|
||||
"level": int(self.level.value),
|
||||
}
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps(self.to_jsonable())
|
||||
|
||||
@classmethod
|
||||
def from_jsonable(cls, d: dict[str, Any]) -> DecompilationNote:
|
||||
klass = cls._subclasses.get(d.get("class", ""), DecompilationNote)
|
||||
return klass._from_jsonable_impl(d) # pylint:disable=protected-access
|
||||
|
||||
@classmethod
|
||||
def _from_jsonable_impl(cls, d: dict[str, Any]) -> DecompilationNote:
|
||||
return cls(
|
||||
key=d["key"],
|
||||
name=d["name"],
|
||||
content=d.get("content"),
|
||||
level=DecompilationNoteLevel(d.get("level", DecompilationNoteLevel.INFO.value)),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, s: str) -> DecompilationNote:
|
||||
return cls.from_jsonable(json.loads(s))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from typing import Any
|
||||
|
||||
from .decompilation_note import DecompilationNote
|
||||
|
||||
|
||||
|
|
@ -54,3 +57,26 @@ class DeobfuscatedStringsNote(DecompilationNote):
|
|||
lines.append(f" Type {deobf_str.type} @ {deobf_str.ref_addr:#x}: {deobf_str.value!r}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
#
|
||||
# JSON serialization
|
||||
#
|
||||
|
||||
def to_jsonable(self) -> dict[str, Any]:
|
||||
d = super().to_jsonable()
|
||||
d["strings"] = [
|
||||
{
|
||||
"ref_addr": s.ref_addr,
|
||||
"type": s.type,
|
||||
"value": base64.b64encode(s.value).decode("ascii"),
|
||||
}
|
||||
for _, s in sorted(self.strings.items())
|
||||
]
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def _from_jsonable_impl(cls, d: dict[str, Any]) -> DeobfuscatedStringsNote:
|
||||
note = cls(key=d["key"], name=d["name"])
|
||||
for entry in d.get("strings", []):
|
||||
note.add_string(entry["type"], base64.b64decode(entry["value"]), ref_addr=entry["ref_addr"])
|
||||
return note
|
||||
|
|
|
|||
30
angr/analyses/decompiler/optimization_pass_registry.py
Normal file
30
angr/analyses/decompiler/optimization_pass_registry.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""
|
||||
Stable name resolution for decompiler optimization passes and peephole optimizations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def _known_passes() -> dict[str, type]:
|
||||
# Recomputed on each call to pick up classes added via ``register_optimization_pass`` after this module is imported.
|
||||
|
||||
from .optimization_passes import ALL_OPTIMIZATION_PASSES # pylint:disable=import-outside-toplevel
|
||||
from .peephole_optimizations import ALL_PEEPHOLE_OPTS # pylint:disable=import-outside-toplevel
|
||||
|
||||
return {cls.__qualname__: cls for cls in (*ALL_OPTIMIZATION_PASSES, *ALL_PEEPHOLE_OPTS)}
|
||||
|
||||
|
||||
def pass_to_name(cls: type) -> str:
|
||||
"""Return a stable string identifier (the class name) for an optimization pass or peephole class."""
|
||||
return cls.__qualname__
|
||||
|
||||
|
||||
def name_to_pass(name: str) -> type | None:
|
||||
"""
|
||||
Resolve a class name back to its registered pass class. Returns None for names that are not registered at the time
|
||||
this method is called. Peephole passes might be defined by analyses or plugins that have not yet been imported.
|
||||
"""
|
||||
return _known_passes().get(name)
|
||||
|
||||
|
||||
__all__ = ("name_to_pass", "pass_to_name")
|
||||
|
|
@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from archinfo import Arch
|
||||
|
||||
import angr.analyses.decompiler as decompiler
|
||||
from angr.analyses import decompiler
|
||||
|
||||
from .base_ptr_save_simplifier import BasePointerSaveSimplifier
|
||||
from .call_stmt_rewriter import CallStatementRewriter
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.analyses.decompiler.stack_item import StackItem, StackItemType
|
||||
|
||||
from .optimization_pass import OptimizationPass, OptimizationPassStage
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from angr.ailment import AILBlockRewriter, Block, Expression
|
|||
from angr.ailment.expression import BinaryOp, Const, VirtualVariable
|
||||
from angr.ailment.statement import Assignment, ConditionalJump, Statement
|
||||
from angr.analyses.decompiler.utils import first_nonlabel_nonphi_statement
|
||||
from angr.analyses.s_reaching_definitions import SReachingDefinitions
|
||||
from angr.utils.graph import dominates
|
||||
from angr.utils.timing import timethis
|
||||
|
||||
|
|
@ -140,7 +141,7 @@ class ConditionConstantPropagation(OptimizationPass):
|
|||
entry_node_addr, entry_node_idx = self.entry_node_addr
|
||||
entry_node = self._get_block(entry_node_addr, idx=entry_node_idx)
|
||||
idoms = networkx.algorithms.immediate_dominators(self._graph, entry_node)
|
||||
rda: SRDAModel = self.project.analyses.SReachingDefinitions(self._func, func_graph=self._graph).model
|
||||
rda: SRDAModel = SReachingDefinitions(self.project, self._func, func_graph=self._graph).model
|
||||
|
||||
for src, cconds in cconds_by_src.items():
|
||||
head_block = self._get_block(src[0], idx=src[1])
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ class AILMergeGraph:
|
|||
self.original_ends = start_blocks
|
||||
|
||||
# moved here
|
||||
for _, pair in merge_to_end_pair.items():
|
||||
for pair in merge_to_end_pair.values():
|
||||
for block in pair:
|
||||
other_block = pair[0] if pair[1] is block else pair[1]
|
||||
while True:
|
||||
|
|
@ -390,7 +390,7 @@ class AILMergeGraph:
|
|||
#
|
||||
|
||||
def _find_block_pair_in_originals(self, block: Block):
|
||||
for _, originals in self.merge_blocks_to_originals.items():
|
||||
for originals in self.merge_blocks_to_originals.values():
|
||||
# need at least 2 for a pair
|
||||
if len(originals) < 2:
|
||||
continue
|
||||
|
|
@ -454,7 +454,7 @@ class AILMergeGraph:
|
|||
return None
|
||||
|
||||
def _find_split_block_by_original(self, block: Block) -> AILBlockSplit | None:
|
||||
for _, split_blocks in self.original_split_blocks.items():
|
||||
for split_blocks in self.original_split_blocks.values():
|
||||
for split_block in split_blocks:
|
||||
if split_block.original == block:
|
||||
return split_block
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from itertools import combinations
|
|||
|
||||
import networkx as nx
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.ailment.block import Block
|
||||
from angr.ailment.expression import Const, Convert, Expression, Register, VirtualVariable
|
||||
from angr.ailment.statement import Assignment, ConditionalJump, Jump, Label, Return
|
||||
|
|
@ -337,7 +337,7 @@ class DuplicationReverter(StructuringOptimizationPass):
|
|||
for node in graph.nodes:
|
||||
nodes_by_addr[node.addr].append(node)
|
||||
|
||||
for _, nodes in nodes_by_addr.items():
|
||||
for nodes in nodes_by_addr.values():
|
||||
if len(nodes) == 1:
|
||||
continue
|
||||
|
||||
|
|
@ -681,9 +681,9 @@ class DuplicationReverter(StructuringOptimizationPass):
|
|||
# conditions yet. This means the graph is still missing the divergence of the two graphs.
|
||||
try:
|
||||
graph_lcs = longest_ail_graph_subseq(blocks, graph)
|
||||
except SAILRSemanticError as e:
|
||||
except SAILRSemanticError:
|
||||
self.candidate_blacklist.add(tuple(blocks))
|
||||
raise e
|
||||
raise
|
||||
|
||||
ail_merge_graph = AILMergeGraph(original_graph=graph)
|
||||
# some blocks in originals may update during this time (if-statements can change)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import logging
|
|||
|
||||
import networkx as nx
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.ailment import Const
|
||||
from angr.ailment.block import Block
|
||||
from angr.ailment.statement import ConditionalJump, Jump, Statement
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.engines.light import SimEngineLightAIL
|
||||
|
||||
_l = logging.getLogger(name=__name__)
|
||||
|
|
@ -377,6 +377,7 @@ class SimplifierAILEngine(
|
|||
return ailment.expression.UnaryOp(expr.idx, expr.op, operand, **expr.tags)
|
||||
return expr
|
||||
|
||||
_handle_unop_Abs = _handle_unop_Default
|
||||
_handle_unop_Not = _handle_unop_Default
|
||||
_handle_unop_Neg = _handle_unop_Default
|
||||
_handle_unop_BitwiseNeg = _handle_unop_Default
|
||||
|
|
@ -509,6 +510,8 @@ class SimplifierAILEngine(
|
|||
|
||||
_handle_binop_MaxV = _handle_binop_Default
|
||||
|
||||
_handle_binop_HAddV = _handle_binop_Default
|
||||
|
||||
_handle_binop_QAddV = _handle_binop_Default
|
||||
|
||||
_handle_binop_QSubV = _handle_binop_Default
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
|
@ -90,7 +91,7 @@ class ExpressionReplacer(AILBlockRewriter):
|
|||
|
||||
class OpDescriptor:
|
||||
"""
|
||||
Describes a specific operator.
|
||||
Describes a specific operator. Serializes to JSON.
|
||||
"""
|
||||
|
||||
def __init__(self, block_addr: int, stmt_idx: int, ins_addr: int, op: str):
|
||||
|
|
@ -111,6 +112,29 @@ class OpDescriptor:
|
|||
and self.op == other.op
|
||||
)
|
||||
|
||||
#
|
||||
# JSON serialization
|
||||
#
|
||||
|
||||
def to_jsonable(self) -> dict[str, Any]:
|
||||
return {
|
||||
"block_addr": self.block_addr,
|
||||
"stmt_idx": self.stmt_idx,
|
||||
"ins_addr": self.ins_addr,
|
||||
"op": self.op,
|
||||
}
|
||||
|
||||
def to_json(self) -> str:
|
||||
return json.dumps(self.to_jsonable())
|
||||
|
||||
@classmethod
|
||||
def from_jsonable(cls, d: dict[str, Any]) -> OpDescriptor:
|
||||
return cls(d["block_addr"], d["stmt_idx"], d["ins_addr"], d["op"])
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, s: str) -> OpDescriptor:
|
||||
return cls.from_jsonable(json.loads(s))
|
||||
|
||||
|
||||
class ExprOpSwapper(SequenceOptimizationPass):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class InlinedMemsetSimplifier(OptimizationPass):
|
|||
replaced_indices: set[int] = set()
|
||||
replacements: dict[int, SideEffectStatement] = {}
|
||||
|
||||
for _kind, lst in info_by_kind.items():
|
||||
for lst in info_by_kind.values():
|
||||
if len(lst) <= 1:
|
||||
continue
|
||||
candidates = self._find_memset_candidates(lst)
|
||||
|
|
|
|||
|
|
@ -96,9 +96,11 @@ class InlinedStrcpySimplifier(OptimizationPass):
|
|||
elif (
|
||||
isinstance(stmt.src, Insert)
|
||||
and isinstance(stmt.src.base, (Const, VirtualVariable))
|
||||
and (not isinstance(stmt.src.base, Const) or stmt.src.base.is_int)
|
||||
and isinstance(stmt.src.value, Const)
|
||||
and stmt.src.value.is_int
|
||||
and isinstance(stmt.src.offset, Const)
|
||||
and stmt.src.offset.is_int
|
||||
):
|
||||
inlined_strcpy_candidate = True
|
||||
src = stmt.src.value
|
||||
|
|
@ -253,7 +255,7 @@ class InlinedStrcpySimplifier(OptimizationPass):
|
|||
delta = self._get_delta(addr_last, addr_curr)
|
||||
if delta is not None and delta == len(s_last):
|
||||
new_str = s_last + s_curr
|
||||
elif isinstance(stmt, Store) and isinstance(stmt.data, Const):
|
||||
elif isinstance(stmt, Store) and isinstance(stmt.data, Const) and stmt.data.is_int:
|
||||
addr_curr = stmt.addr
|
||||
delta = self._get_delta(addr_last, addr_curr)
|
||||
if delta is not None and delta == len(s_last):
|
||||
|
|
@ -322,7 +324,7 @@ class InlinedStrcpySimplifier(OptimizationPass):
|
|||
and stmt.dst.was_stack
|
||||
and isinstance(stmt.dst.stack_offset, int)
|
||||
):
|
||||
if isinstance(stmt.src, Const):
|
||||
if isinstance(stmt.src, Const) and stmt.src.is_int:
|
||||
r[stmt.dst.stack_offset] = idx, ail_const_to_be(stmt.src, self.project.arch.memory_endness)
|
||||
if (
|
||||
isinstance(stmt.src, Insert)
|
||||
|
|
@ -334,17 +336,20 @@ class InlinedStrcpySimplifier(OptimizationPass):
|
|||
and stmt.src.base.stack_offset == stmt.dst.stack_offset
|
||||
)
|
||||
)
|
||||
and (not isinstance(stmt.src.base, Const) or stmt.src.base.is_int)
|
||||
and isinstance(stmt.src.offset, Const)
|
||||
and stmt.src.offset.is_int
|
||||
and isinstance(stmt.src.value, Const)
|
||||
and stmt.src.value.is_int
|
||||
):
|
||||
r[stmt.dst.stack_offset + stmt.src.offset.value] = (
|
||||
r[stmt.dst.stack_offset + stmt.src.offset.value_int] = (
|
||||
idx,
|
||||
ail_const_to_be(stmt.src.value, self.project.arch.memory_endness),
|
||||
)
|
||||
else:
|
||||
r[stmt.dst.stack_offset] = idx, None
|
||||
elif isinstance(stmt, Store) and isinstance(stmt.addr, StackBaseOffset):
|
||||
if isinstance(stmt.data, Const):
|
||||
if isinstance(stmt.data, Const) and stmt.data.is_int:
|
||||
r[stmt.addr.offset] = idx, ail_const_to_be(stmt.data, self.project.arch.memory_endness)
|
||||
else:
|
||||
r[stmt.addr.offset] = idx, None
|
||||
|
|
@ -420,12 +425,12 @@ class InlinedStrcpySimplifier(OptimizationPass):
|
|||
):
|
||||
return StackBaseOffset(-1, addr.bits, 0), addr.operand.stack_offset
|
||||
if isinstance(addr, BinaryOp):
|
||||
if addr.op == "Add" and isinstance(addr.operands[1], Const):
|
||||
if addr.op == "Add" and isinstance(addr.operands[1], Const) and addr.operands[1].is_int:
|
||||
base_0, offset_0 = InlinedStrcpySimplifier._parse_addr(addr.operands[0])
|
||||
return base_0, offset_0 + addr.operands[1].value
|
||||
if addr.op == "Sub" and isinstance(addr.operands[1], Const):
|
||||
return base_0, offset_0 + addr.operands[1].value_int
|
||||
if addr.op == "Sub" and isinstance(addr.operands[1], Const) and addr.operands[1].is_int:
|
||||
base_0, offset_0 = InlinedStrcpySimplifier._parse_addr(addr.operands[0])
|
||||
return base_0, offset_0 - addr.operands[1].value
|
||||
return base_0, offset_0 - addr.operands[1].value_int
|
||||
return addr, 0
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import archinfo
|
|||
import claripy
|
||||
from archinfo import Endness
|
||||
|
||||
from angr.ailment import AILBlockViewer
|
||||
from angr.ailment.expression import (
|
||||
BinaryOp,
|
||||
Const,
|
||||
|
|
@ -21,7 +22,7 @@ from angr.ailment.expression import (
|
|||
UnaryOp,
|
||||
VirtualVariable,
|
||||
)
|
||||
from angr.ailment.statement import ConditionalJump, Jump, Store
|
||||
from angr.ailment.statement import Assignment, ConditionalJump, Jump, Store
|
||||
from angr.code_location import CodeLocation
|
||||
from angr.engines.light import SimEngineNostmtAIL
|
||||
from angr.errors import SimMemoryMissingError
|
||||
|
|
@ -347,6 +348,9 @@ class InlinedStringTransformationAILEngine(
|
|||
return ~v
|
||||
return None
|
||||
|
||||
def _handle_unop_Abs(self, expr: UnaryOp):
|
||||
self._expr(expr.operand)
|
||||
|
||||
def _handle_unop_Default(self, expr: UnaryOp):
|
||||
return None
|
||||
|
||||
|
|
@ -502,6 +506,7 @@ class InlinedStringTransformationAILEngine(
|
|||
_handle_binop_SubV = _handle_binop_Default
|
||||
_handle_binop_MinV = _handle_binop_Default
|
||||
_handle_binop_MaxV = _handle_binop_Default
|
||||
_handle_binop_HAddV = _handle_binop_Default
|
||||
_handle_binop_QAddV = _handle_binop_Default
|
||||
_handle_binop_QSubV = _handle_binop_Default
|
||||
_handle_binop_QNarrowBinV = _handle_binop_Default
|
||||
|
|
@ -509,6 +514,71 @@ class InlinedStringTransformationAILEngine(
|
|||
_handle_binop_Set = _handle_binop_Default
|
||||
|
||||
|
||||
class _StackReadNotification(Exception):
|
||||
"""Abort the walk on the first potential stack read."""
|
||||
|
||||
|
||||
class _HasStackReadWalker(AILBlockViewer):
|
||||
"""
|
||||
Raises ``_StackReadNotification`` on the first expression that InlinedStringTransformationAILEngine could turn
|
||||
into a "load" stack-access record: a Load, or a virtual variable that lives on the stack.
|
||||
"""
|
||||
|
||||
def _handle_Load(self, expr_idx, expr, stmt_idx, stmt, block): # pylint:disable=unused-argument
|
||||
raise _StackReadNotification
|
||||
|
||||
def _handle_VirtualVariable(self, expr_idx, expr, stmt_idx, stmt, block): # pylint:disable=unused-argument
|
||||
if expr.was_stack:
|
||||
raise _StackReadNotification
|
||||
|
||||
|
||||
_HAS_STACK_READ_WALKER = _HasStackReadWalker()
|
||||
|
||||
|
||||
def _addr_may_be_stack(addr: Expression) -> bool:
|
||||
"""
|
||||
Syntactic over-approximation of the addresses ``InlinedStringTransformationAILEngine._process_address`` can
|
||||
resolve. Anything outside this shape never produces a stack-access record.
|
||||
"""
|
||||
if isinstance(addr, (Const, StackBaseOffset)):
|
||||
return True
|
||||
if isinstance(addr, UnaryOp) and addr.op == "Reference":
|
||||
return True
|
||||
return (
|
||||
isinstance(addr, BinaryOp)
|
||||
and addr.op in {"Add", "Sub"}
|
||||
and isinstance(addr.operands[0], (StackBaseOffset, UnaryOp, Const))
|
||||
)
|
||||
|
||||
|
||||
def _reads_stack(expr: Expression) -> bool:
|
||||
try:
|
||||
_HAS_STACK_READ_WALKER.walk_expression(expr)
|
||||
except _StackReadNotification:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _may_transform_stack_bytes(block) -> bool:
|
||||
"""
|
||||
A descriptor is only ever built when some statement records a "load" and a "store" stack access at the *same*
|
||||
code location, i.e. a store to the stack whose value is derived from a stack read. Checking that syntactically is
|
||||
far cheaper than symbolically executing the loop, and no statement outside this shape can produce that pair.
|
||||
"""
|
||||
for stmt in block.statements:
|
||||
if isinstance(stmt, Store):
|
||||
if _addr_may_be_stack(stmt.addr) and _reads_stack(stmt.data):
|
||||
return True
|
||||
elif (
|
||||
isinstance(stmt, Assignment)
|
||||
and isinstance(stmt.dst, VirtualVariable)
|
||||
and stmt.dst.was_stack
|
||||
and _reads_stack(stmt.src)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class InlineStringTransformationDescriptor:
|
||||
"""
|
||||
Describes an instance of inline string transformation.
|
||||
|
|
@ -630,6 +700,10 @@ class InlinedStringTransformationSimplifier(OptimizationPass):
|
|||
for loop_node in self_loops:
|
||||
pred = next(iter(nn for nn in self._graph.predecessors(loop_node) if nn is not loop_node))
|
||||
succ = next(iter(nn for nn in self._graph.successors(loop_node) if nn is not loop_node))
|
||||
if not _may_transform_stack_bytes(loop_node) and not _may_transform_stack_bytes(pred):
|
||||
# no statement here can produce the load-then-store-at-the-same-code-location pair a descriptor
|
||||
# needs; skip the (expensive) symbolic execution entirely
|
||||
continue
|
||||
engine = InlinedStringTransformationAILEngine(
|
||||
self.project, {pred.addr: pred, loop_node.addr: loop_node}, pred.addr, succ.addr, 1024
|
||||
)
|
||||
|
|
|
|||
|
|
@ -176,8 +176,11 @@ class InlinedWcscpySimplifier(OptimizationPass):
|
|||
if isinstance(stmt, SideEffectStatement) and self.is_inlined_wcsncpy(stmt):
|
||||
assert stmt.expr.args is not None and len(stmt.expr.args) >= 3
|
||||
base, off = self._parse_addr(stmt.expr.args[0])
|
||||
store_size = stmt.expr.args[2].value * 2 if isinstance(stmt.expr.args[2], Const) else None
|
||||
if off is not None and store_size is not None:
|
||||
count = stmt.expr.args[2]
|
||||
if not isinstance(count, Const) or not count.is_int:
|
||||
return None
|
||||
store_size = count.value_int * 2
|
||||
if off is not None:
|
||||
candidates.append((i, base, off, store_size, stmt))
|
||||
elif isinstance(stmt, Store) and isinstance(stmt.data, Const):
|
||||
base, off = self._parse_addr(stmt.addr)
|
||||
|
|
@ -262,8 +265,10 @@ class InlinedWcscpySimplifier(OptimizationPass):
|
|||
merged_stmt = merged[0]
|
||||
new_base, new_off = self._parse_addr(merged_stmt.expr.args[0])
|
||||
new_sz = (
|
||||
merged_stmt.expr.args[2].value * 2
|
||||
if len(merged_stmt.expr.args) >= 3 and isinstance(merged_stmt.expr.args[2], Const)
|
||||
merged_stmt.expr.args[2].value_int * 2
|
||||
if len(merged_stmt.expr.args) >= 3
|
||||
and isinstance(merged_stmt.expr.args[2], Const)
|
||||
and merged_stmt.expr.args[2].is_int
|
||||
else sz0 + sz1
|
||||
)
|
||||
new_item = idx0, new_base, new_off, new_sz, merged_stmt
|
||||
|
|
@ -442,6 +447,7 @@ class InlinedWcscpySimplifier(OptimizationPass):
|
|||
and starting_stmt.addr.op == "Add"
|
||||
and isinstance(starting_stmt.addr.operands[0], VirtualVariable)
|
||||
and isinstance(starting_stmt.addr.operands[1], Const)
|
||||
and starting_stmt.addr.operands[1].is_int
|
||||
):
|
||||
expected_store_varid = starting_stmt.addr.operands[0].varid
|
||||
else:
|
||||
|
|
@ -464,7 +470,9 @@ class InlinedWcscpySimplifier(OptimizationPass):
|
|||
):
|
||||
offset = stmt.dst.stack_offset
|
||||
value = (
|
||||
ail_const_to_be(stmt.src, self.project.arch.memory_endness) if isinstance(stmt.src, Const) else None
|
||||
ail_const_to_be(stmt.src, self.project.arch.memory_endness)
|
||||
if isinstance(stmt.src, Const) and stmt.src.is_int
|
||||
else None
|
||||
)
|
||||
elif expected_type == "store" and isinstance(stmt, Store):
|
||||
if isinstance(stmt.addr, VirtualVariable) and stmt.addr.varid == expected_store_varid:
|
||||
|
|
@ -474,14 +482,15 @@ class InlinedWcscpySimplifier(OptimizationPass):
|
|||
and stmt.addr.op == "Add"
|
||||
and isinstance(stmt.addr.operands[0], VirtualVariable)
|
||||
and isinstance(stmt.addr.operands[1], Const)
|
||||
and stmt.addr.operands[1].is_int
|
||||
and stmt.addr.operands[0].varid == expected_store_varid
|
||||
):
|
||||
offset = stmt.addr.operands[1].value
|
||||
offset = stmt.addr.operands[1].value_int
|
||||
else:
|
||||
offset = None
|
||||
value = (
|
||||
ail_const_to_be(stmt.data, self.project.arch.memory_endness)
|
||||
if isinstance(stmt.data, Const)
|
||||
if isinstance(stmt.data, Const) and stmt.data.is_int
|
||||
else None
|
||||
)
|
||||
else:
|
||||
|
|
@ -508,16 +517,19 @@ class InlinedWcscpySimplifier(OptimizationPass):
|
|||
def even_offsets_are_zero(lst):
|
||||
if len(lst) >= 2 and lst[-1] == 0 and lst[-2] == 0:
|
||||
lst = lst[:-2]
|
||||
return all((ch == 0 if i % 2 == 0 else ch != 0) for i, ch in enumerate(lst))
|
||||
return all(isinstance(ch, int) and (ch == 0 if i % 2 == 0 else ch != 0) for i, ch in enumerate(lst))
|
||||
|
||||
@staticmethod
|
||||
def odd_offsets_are_zero(lst):
|
||||
if len(lst) >= 2 and lst[-1] == 0 and lst[-2] == 0:
|
||||
lst = lst[:-2]
|
||||
return all((ch == 0 if i % 2 == 1 else ch != 0) for i, ch in enumerate(lst))
|
||||
return all(isinstance(ch, int) and (ch == 0 if i % 2 == 1 else ch != 0) for i, ch in enumerate(lst))
|
||||
|
||||
@staticmethod
|
||||
def is_integer_likely_a_wide_string(v, size, endness, min_length=4):
|
||||
if not isinstance(v, int) or not isinstance(size, int):
|
||||
return False, None
|
||||
|
||||
chars = []
|
||||
if endness == Endness.LE:
|
||||
while v != 0:
|
||||
|
|
@ -579,12 +591,12 @@ class InlinedWcscpySimplifier(OptimizationPass):
|
|||
):
|
||||
return StackBaseOffset(-1, 64, 0), addr.operand.stack_offset
|
||||
if isinstance(addr, BinaryOp):
|
||||
if addr.op == "Add" and isinstance(addr.operands[1], Const) and isinstance(addr.operands[1].value, int):
|
||||
if addr.op == "Add" and isinstance(addr.operands[1], Const) and addr.operands[1].is_int:
|
||||
base_0, offset_0 = InlinedWcscpySimplifier._parse_addr(addr.operands[0])
|
||||
return base_0, offset_0 + addr.operands[1].value
|
||||
if addr.op == "Sub" and isinstance(addr.operands[1], Const) and isinstance(addr.operands[1].value, int):
|
||||
return base_0, offset_0 + addr.operands[1].value_int
|
||||
if addr.op == "Sub" and isinstance(addr.operands[1], Const) and addr.operands[1].is_int:
|
||||
base_0, offset_0 = InlinedWcscpySimplifier._parse_addr(addr.operands[0])
|
||||
return base_0, offset_0 - addr.operands[1].value
|
||||
return base_0, offset_0 - addr.operands[1].value_int
|
||||
return addr, 0
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -230,8 +230,9 @@ class LoweredSwitchSimplifier(StructuringOptimizationPass):
|
|||
graph_copy = networkx.DiGraph(self._graph)
|
||||
self.out_graph = graph_copy
|
||||
node_to_heads = defaultdict(set)
|
||||
modified = False
|
||||
|
||||
for _, caselists in variablehash_to_cases.items():
|
||||
for caselists in variablehash_to_cases.values():
|
||||
for cases, redundant_nodes in caselists:
|
||||
real_cases = [case for case in cases if case.value != "default"]
|
||||
max_continuous_cases = self._count_max_continuous_cases(real_cases)
|
||||
|
|
@ -346,6 +347,7 @@ class LoweredSwitchSimplifier(StructuringOptimizationPass):
|
|||
new_head.statements[-1] = switch_stmt
|
||||
# update the block
|
||||
self._update_block(original_head, new_head)
|
||||
modified = True
|
||||
|
||||
# sanity check that no switch head points to either itself
|
||||
# or to any if-head that was merged into the new switch head; this
|
||||
|
|
@ -412,6 +414,10 @@ class LoweredSwitchSimplifier(StructuringOptimizationPass):
|
|||
else:
|
||||
graph_copy.add_edge(node_copy, succ)
|
||||
|
||||
if not modified:
|
||||
# the graph is not modified
|
||||
self.out_graph = None
|
||||
return False
|
||||
return True
|
||||
|
||||
def _find_cascading_switch_variable_comparisons(self):
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import weakref
|
||||
from collections import namedtuple
|
||||
from collections.abc import Generator
|
||||
from enum import Enum
|
||||
|
|
@ -13,6 +14,7 @@ import angr
|
|||
from angr import ailment
|
||||
from angr.ailment.manager import Manager
|
||||
from angr.analyses.decompiler.ailgraph_walker import AILGraphWalker
|
||||
from angr.analyses.decompiler.block_simplifier import BlockSimplifier, PeepholeOptimizationBundle
|
||||
from angr.analyses.decompiler.condition_processor import ConditionProcessor
|
||||
from angr.analyses.decompiler.counters import ControlFlowStructureCounter
|
||||
from angr.analyses.decompiler.goto_manager import Goto, GotoManager
|
||||
|
|
@ -20,6 +22,7 @@ from angr.analyses.decompiler.structuring import RecursiveStructurer, SAILRStruc
|
|||
from angr.analyses.decompiler.utils import add_labels, is_empty_node, remove_edges_in_ailgraph
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from angr.analyses.decompiler.region_identifier import RegionIdentifier, RegionOverlay
|
||||
from angr.analyses.decompiler.stack_item import StackItem
|
||||
from angr.knowledge_plugins.functions import Function
|
||||
from angr.project import Project
|
||||
|
|
@ -125,6 +128,9 @@ class OptimizationPass(BaseOptimizationPass):
|
|||
|
||||
_graph: networkx.DiGraph
|
||||
|
||||
# self._scratch[_STRUCTURABILITY_CACHE_KEY] = weakref(graph), params for applying the structurability result
|
||||
_STRUCTURABILITY_CACHE_KEY = "structurability_cache"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func,
|
||||
|
|
@ -133,7 +139,7 @@ class OptimizationPass(BaseOptimizationPass):
|
|||
graph,
|
||||
blocks_by_addr=None,
|
||||
blocks_by_addr_and_idx=None,
|
||||
variable_kb=None,
|
||||
kb=None,
|
||||
region_identifier=None,
|
||||
reaching_definitions=None,
|
||||
vvar_id_start: int = 0,
|
||||
|
|
@ -155,7 +161,7 @@ class OptimizationPass(BaseOptimizationPass):
|
|||
self._blocks_by_addr: dict[int, set[ailment.Block]] = blocks_by_addr or {}
|
||||
self._blocks_by_addr_and_idx: dict[tuple[int, int | None], ailment.Block] = blocks_by_addr_and_idx or {}
|
||||
self._graph = graph
|
||||
self._variable_kb = variable_kb
|
||||
self._dvars_kb = kb
|
||||
self._ri = region_identifier
|
||||
self._rd = reaching_definitions
|
||||
self._scratch = scratch if scratch is not None else {}
|
||||
|
|
@ -178,6 +184,24 @@ class OptimizationPass(BaseOptimizationPass):
|
|||
self.out_graph: networkx.DiGraph | None = None
|
||||
self.stack_items: dict[int, StackItem] = {}
|
||||
|
||||
def analyze(self):
|
||||
super().analyze()
|
||||
self._invalidate_structurability_cache_if_modified()
|
||||
|
||||
def _invalidate_structurability_cache_if_modified(self) -> None:
|
||||
"""
|
||||
Drop the shared structurability cache if this pass produced an output graph.
|
||||
|
||||
``out_graph`` is the single channel through which a pass reports a graph change, and it covers both flavors:
|
||||
a freshly built graph object (which would miss the identity-keyed cache anyway) and an in-place mutation of
|
||||
the graph the pass was handed (which would *wrongly hit* it). Invalidating on any non-None ``out_graph``
|
||||
keeps the cache sound without having to fingerprint AIL contents -- note that ``Block.__hash__`` is memoized
|
||||
by the Rust backend and does not track in-place statement edits, so content fingerprints based on it would be
|
||||
unsound.
|
||||
"""
|
||||
if self.out_graph is not None:
|
||||
self._scratch.pop(self._STRUCTURABILITY_CACHE_KEY, None)
|
||||
|
||||
@property
|
||||
def blocks_by_addr(self) -> dict[int, set[ailment.Block]]:
|
||||
return self._blocks_by_addr
|
||||
|
|
@ -342,8 +366,9 @@ class OptimizationPass(BaseOptimizationPass):
|
|||
ail_block,
|
||||
cache=cache,
|
||||
)
|
||||
key = ail_block.addr, ail_block.idx
|
||||
blocks_by_addr_and_idx[key] = simplified
|
||||
if simplified is not None:
|
||||
key = ail_block.addr, ail_block.idx
|
||||
blocks_by_addr_and_idx[key] = simplified
|
||||
|
||||
# update blocks_map to allow node_addr to node lookup
|
||||
def _replace_node_handler(node):
|
||||
|
|
@ -356,6 +381,21 @@ class OptimizationPass(BaseOptimizationPass):
|
|||
|
||||
return ail_graph
|
||||
|
||||
def _get_peephole_bundle(self) -> PeepholeOptimizationBundle:
|
||||
bundle: PeepholeOptimizationBundle | None = self._scratch.get("peephole_bundle")
|
||||
if bundle is None or not bundle.matches(
|
||||
self.project, self.manager, self._func.addr, None, None, self._peephole_optimizations
|
||||
):
|
||||
bundle = PeepholeOptimizationBundle(
|
||||
self.project,
|
||||
self.kb,
|
||||
self.manager,
|
||||
func_addr=self._func.addr,
|
||||
peephole_optimizations=self._peephole_optimizations,
|
||||
)
|
||||
self._scratch["peephole_bundle"] = bundle
|
||||
return bundle
|
||||
|
||||
def _simplify_block(self, ail_block, cache=None):
|
||||
"""
|
||||
Simplify a single AIL block.
|
||||
|
|
@ -374,13 +414,14 @@ class OptimizationPass(BaseOptimizationPass):
|
|||
cached_rd = cache_item.rd
|
||||
cached_prop = cache_item.prop
|
||||
|
||||
simp = self.project.analyses.AILBlockSimplifier(
|
||||
simp = BlockSimplifier(
|
||||
self.project,
|
||||
ail_block,
|
||||
self.manager,
|
||||
self._func.addr,
|
||||
peephole_optimizations=self._peephole_optimizations,
|
||||
cached_reaching_definitions=cached_rd,
|
||||
cached_propagator=cached_prop,
|
||||
peephole_bundle=self._get_peephole_bundle(),
|
||||
)
|
||||
# update the cache
|
||||
if cache is not None:
|
||||
|
|
@ -500,6 +541,12 @@ class StructuringOptimizationPass(OptimizationPass):
|
|||
raise NotImplementedError
|
||||
|
||||
def analyze(self):
|
||||
try:
|
||||
self._analyze_and_verify()
|
||||
finally:
|
||||
self._invalidate_structurability_cache_if_modified()
|
||||
|
||||
def _analyze_and_verify(self):
|
||||
"""
|
||||
Wrapper for _analyze() that verifies the graph is structurable before and after the optimization.
|
||||
"""
|
||||
|
|
@ -600,18 +647,55 @@ class StructuringOptimizationPass(OptimizationPass):
|
|||
if not had_any_changes:
|
||||
self.out_graph = None
|
||||
|
||||
def _graph_is_structurable(self, graph, readd_labels=False, initial=False) -> bool:
|
||||
def _graph_is_structurable(self, graph, readd_labels: bool = False, initial: bool = False) -> bool:
|
||||
"""
|
||||
Checks weather the input graph is structurable under the Phoenix schema-matching structuring algorithm.
|
||||
As a side effect, this will also update the region identifier and goto manager of this optimization pass.
|
||||
Consequently, a true return guarantees up-to-date goto information in the goto manager.
|
||||
Consequently, a True return guarantees up-to-date goto information in the goto manager.
|
||||
|
||||
We cache the structurability probe result in self._scratch. An optimization pass invalidates the cached
|
||||
structurability result if it updates the graph.
|
||||
"""
|
||||
# Only the probe of an unmodified input graph is cacheable
|
||||
cacheable = initial and not readd_labels
|
||||
if self._edges_to_remove:
|
||||
# remove_edges_in_ailgraph() below mutates the graph in place, so anything cached for it describes the
|
||||
# graph as it was before those edges were dropped.
|
||||
self._scratch.pop(self._STRUCTURABILITY_CACHE_KEY, None)
|
||||
cacheable = False
|
||||
|
||||
if cacheable:
|
||||
# check cache
|
||||
entry = self._scratch.get(self._STRUCTURABILITY_CACHE_KEY)
|
||||
if entry is not None and entry[0]() is graph:
|
||||
structurable, ri, goto_manager, region = entry[1]
|
||||
self._apply_structurability_result(structurable, ri, goto_manager, region, initial=initial)
|
||||
return structurable
|
||||
|
||||
# the old route
|
||||
structurable, ri, goto_manager, region = self._compute_structurability(graph, readd_labels)
|
||||
if cacheable:
|
||||
# cache the result
|
||||
self._scratch[self._STRUCTURABILITY_CACHE_KEY] = (
|
||||
weakref.ref(graph),
|
||||
(structurable, ri, goto_manager, region),
|
||||
)
|
||||
self._apply_structurability_result(structurable, ri, goto_manager, region, initial=initial)
|
||||
return structurable
|
||||
|
||||
def _compute_structurability(
|
||||
self, graph, readd_labels: bool
|
||||
) -> tuple[bool, RegionIdentifier | None, GotoManager | None, RegionOverlay | None]:
|
||||
"""
|
||||
Run region identification, structuring, and region simplification on ``graph`` to determine whether it is
|
||||
structurable or not.
|
||||
"""
|
||||
if readd_labels:
|
||||
graph = add_labels(graph, self.manager)
|
||||
|
||||
remove_edges_in_ailgraph(graph, self._edges_to_remove)
|
||||
|
||||
self._ri = self.project.analyses[angr.analyses.decompiler.RegionIdentifier].prep(kb=self.kb)(
|
||||
ri = self.project.analyses[angr.analyses.decompiler.RegionIdentifier].prep(kb=self.kb)(
|
||||
self._func,
|
||||
graph=graph,
|
||||
ail_manager=self.manager,
|
||||
|
|
@ -622,15 +706,15 @@ class StructuringOptimizationPass(OptimizationPass):
|
|||
expose_loop_head_backedges=True,
|
||||
entry_node_addr=self.entry_node_addr,
|
||||
)
|
||||
if self._ri is None:
|
||||
return False
|
||||
if ri is None:
|
||||
return False, None, None, None
|
||||
|
||||
# we should try-catch structuring here because we can often pass completely invalid graphs
|
||||
# that break the assumptions of the structuring algorithm
|
||||
try:
|
||||
rs = self.project.analyses[RecursiveStructurer].prep(kb=self.kb)(
|
||||
self._ri.region,
|
||||
cond_proc=self._ri.cond_proc,
|
||||
ri.region,
|
||||
cond_proc=ri.cond_proc,
|
||||
ail_manager=self.manager,
|
||||
func=self._func,
|
||||
structurer_cls=SAILRStructurer,
|
||||
|
|
@ -641,17 +725,29 @@ class StructuringOptimizationPass(OptimizationPass):
|
|||
rs = None
|
||||
|
||||
if not rs or not rs.result or is_empty_node(rs.result) or rs.result_incomplete:
|
||||
return False
|
||||
return False, ri, None, None
|
||||
|
||||
rs = self.project.analyses.RegionSimplifier(
|
||||
self._func, rs.result, self.manager, arg_vvars=self._arg_vvars, kb=self.kb
|
||||
)
|
||||
if not rs or rs.goto_manager is None or rs.result is None:
|
||||
return False
|
||||
return False, ri, None, None
|
||||
|
||||
self._analyze_simplified_region(rs.result, initial=initial)
|
||||
self._goto_manager = rs.goto_manager
|
||||
return True
|
||||
return True, ri, rs.goto_manager, rs.result
|
||||
|
||||
def _apply_structurability_result(
|
||||
self,
|
||||
structurable: bool,
|
||||
ri: RegionIdentifier | None,
|
||||
goto_manager: GotoManager | None,
|
||||
region: RegionOverlay | None,
|
||||
initial: bool = False,
|
||||
) -> None:
|
||||
self._ri = ri
|
||||
if structurable:
|
||||
assert ri is not None and goto_manager is not None and region is not None
|
||||
self._analyze_simplified_region(region, initial=initial)
|
||||
self._goto_manager = goto_manager
|
||||
|
||||
# pylint:disable=no-self-use
|
||||
def _analyze_simplified_region(self, region, initial=False):
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from angr import ailment
|
||||
from angr.analyses.decompiler.peephole_optimizations import (
|
||||
EXPR_OPTS,
|
||||
PeepholeOptimizationExprBase,
|
||||
)
|
||||
from angr.analyses.decompiler.block_simplifier import BlockSimplifier, PeepholeOptimizationBundle
|
||||
from angr.analyses.decompiler.sequence_walker import SequenceWalker
|
||||
from angr.analyses.decompiler.utils import (
|
||||
peephole_optimize_expr,
|
||||
|
|
@ -40,11 +37,15 @@ class PostStructuringPeepholeOptimizationPass(SequenceOptimizationPass):
|
|||
def __init__(self, *args, peephole_optimizations=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._peephole_optimizations = peephole_optimizations
|
||||
self._expr_peephole_opts = [
|
||||
cls(self.project, self.kb, ail_manager=self.manager, func_addr=self._func.addr)
|
||||
for cls in (self._peephole_optimizations or EXPR_OPTS)
|
||||
if issubclass(cls, PeepholeOptimizationExprBase)
|
||||
]
|
||||
# one bundle for all BlockSimplifier invocations of this pass
|
||||
self._peephole_bundle = PeepholeOptimizationBundle(
|
||||
self.project,
|
||||
self.kb,
|
||||
self.manager,
|
||||
func_addr=self._func.addr,
|
||||
peephole_optimizations=self._peephole_optimizations,
|
||||
)
|
||||
self._expr_peephole_opts = self._peephole_bundle.expr_opts
|
||||
self.analyze()
|
||||
|
||||
def _check(self):
|
||||
|
|
@ -65,12 +66,13 @@ class PostStructuringPeepholeOptimizationPass(SequenceOptimizationPass):
|
|||
old_block, new_block = None, block
|
||||
while old_block != new_block:
|
||||
old_block = new_block
|
||||
# Note: AILBlockSimplifier updates expressions in place
|
||||
simp = self.project.analyses.AILBlockSimplifier(
|
||||
# Note: BlockSimplifier updates expressions in place
|
||||
simp = BlockSimplifier(
|
||||
self.project,
|
||||
new_block,
|
||||
func_addr=self._func.addr,
|
||||
peephole_optimizations=self._peephole_optimizations,
|
||||
ail_manager=self.manager,
|
||||
peephole_bundle=self._peephole_bundle,
|
||||
)
|
||||
assert simp.result_block is not None
|
||||
new_block = simp.result_block
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from itertools import chain
|
|||
|
||||
import archinfo
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.analyses.decompiler.stack_item import StackItem, StackItemType
|
||||
from angr.calling_conventions import SimRegArg
|
||||
from angr.code_location import CodeLocation
|
||||
|
|
|
|||
|
|
@ -2,15 +2,20 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from angr.ailment.expression import VirtualVariable
|
||||
from angr.ailment.statement import Assignment
|
||||
from angr.analyses.decompiler.stack_item import StackItem, StackItemType
|
||||
from angr.code_location import CodeLocation, ExternalCodeLocation
|
||||
from angr.analyses.s_reaching_definitions import SReachingDefinitions
|
||||
from angr.utils.ail import is_phi_assignment
|
||||
|
||||
from .optimization_pass import OptimizationPass, OptimizationPassStage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from angr.analyses.s_reaching_definitions import SRDAModel
|
||||
from angr.code_location import AILCodeLocation
|
||||
|
||||
_l = logging.getLogger(name=__name__)
|
||||
|
||||
|
||||
|
|
@ -36,7 +41,8 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
|
|||
self.analyze()
|
||||
|
||||
def _check(self):
|
||||
self._srda = self.project.analyses.SReachingDefinitions(
|
||||
self._srda = SReachingDefinitions(
|
||||
self.project,
|
||||
subject=self._func,
|
||||
func_graph=self._graph,
|
||||
func_args={vvar for vvar, _ in arg_vvars.values()} if (arg_vvars := self._arg_vvars) is not None else set(),
|
||||
|
|
@ -60,17 +66,16 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
|
|||
if cache is None:
|
||||
return
|
||||
|
||||
info: list[tuple[int, CodeLocation, int, CodeLocation, int]] = cache["info"]
|
||||
info: list[tuple[list[AILCodeLocation], int]] = cache["info"]
|
||||
updated_blocks = {}
|
||||
|
||||
for _regvar, regvar_loc, _stackvar, stackvar_loc, _ in info:
|
||||
# remove storing statements
|
||||
old_block = self._get_block(regvar_loc.block_addr, idx=regvar_loc.block_idx)
|
||||
assert regvar_loc.stmt_idx is not None
|
||||
self._modify_statement(old_block, regvar_loc.stmt_idx, updated_blocks)
|
||||
old_block = self._get_block(stackvar_loc.block_addr, idx=stackvar_loc.block_idx)
|
||||
assert stackvar_loc.stmt_idx is not None
|
||||
self._modify_statement(old_block, stackvar_loc.stmt_idx, updated_blocks)
|
||||
for locs, _ in info:
|
||||
# remove all statements involved in this save (the store plus its matching restore, or the store plus the
|
||||
# dead phi statements it feeds)
|
||||
for loc in locs:
|
||||
old_block = self._get_block(loc.block_addr, idx=loc.block_idx)
|
||||
assert old_block is not None and loc.stmt_idx is not None
|
||||
self._modify_statement(old_block, loc.stmt_idx, updated_blocks)
|
||||
|
||||
for old_block, new_block in updated_blocks.items():
|
||||
# remove all statements that are None
|
||||
|
|
@ -80,20 +85,23 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
|
|||
|
||||
if updated_blocks:
|
||||
# update stack_items
|
||||
for _, _, _, _, stack_offset in info:
|
||||
for _, stack_offset in info:
|
||||
self.stack_items[stack_offset] = StackItem(
|
||||
stack_offset, self.project.arch.bytes, "regs", StackItemType.SAVED_REGS
|
||||
)
|
||||
|
||||
def _find_reg_store_and_restore_locations(self) -> list[tuple[int, CodeLocation, int, CodeLocation, int]]:
|
||||
results = []
|
||||
def _find_reg_store_and_restore_locations(self) -> list[tuple[list[AILCodeLocation], int]]:
|
||||
results: list[tuple[list[AILCodeLocation], int]] = []
|
||||
|
||||
assert self._srda is not None
|
||||
srda_model = self._srda.model
|
||||
srda_model: SRDAModel = self._srda.model
|
||||
# find all registers that are defined externally and used exactly once
|
||||
saved_vvars: set[tuple[int, CodeLocation]] = set()
|
||||
saved_vvars: set[tuple[int, AILCodeLocation]] = set()
|
||||
for vvar_id, loc in srda_model.all_vvar_definitions.items():
|
||||
if isinstance(loc, ExternalCodeLocation):
|
||||
# SReachingDefinitions records externally-defined (function live-in) vvars via
|
||||
# AILCodeLocation.make_extern(). These are AILCodeLocation instances (not ExternalCodeLocation), so the
|
||||
# extern check must go through AILCodeLocation.is_extern.
|
||||
if loc.is_extern:
|
||||
uses = srda_model.all_vvar_uses.get(vvar_id, [])
|
||||
if len(uses) == 1:
|
||||
vvar, used_loc = next(iter(uses))
|
||||
|
|
@ -105,8 +113,13 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
|
|||
|
||||
# for each candidate, we check to ensure:
|
||||
# - it is stored onto the stack (into a stack virtual variable)
|
||||
# - the stack virtual variable is only used once and restores the value to the same register
|
||||
# - the restore location is in the dominance frontier of the store location
|
||||
# - either
|
||||
# (a) the stack virtual variable is used exactly once (ignoring phi uses) to restore the value to the same
|
||||
# register, and the restore location is in the dominance frontier of the store location; or
|
||||
# (b) the stack virtual variable has no non-phi uses and only feeds phi nodes whose results are dead. This
|
||||
# happens with shrink-wrapped prologues (e.g. MSVC drivers) where the callee-saved register is
|
||||
# conditionally spilled and the matching restore has already been removed as a dead assignment; what
|
||||
# remains is a dead store whose stack vvar merges with an undefined value at a loop/branch join.
|
||||
for vvar_id, used_loc in saved_vvars:
|
||||
def_block = self._get_block(used_loc.block_addr, idx=used_loc.block_idx)
|
||||
assert def_block is not None and used_loc.stmt_idx is not None
|
||||
|
|
@ -122,40 +135,59 @@ class RegisterSaveAreaSimplifierAdvanced(OptimizationPass):
|
|||
continue
|
||||
stack_vvar = stmt.dst
|
||||
all_stack_vvar_uses = srda_model.all_vvar_uses.get(stack_vvar.varid, [])
|
||||
# eliminate the use location if it's a phi statement
|
||||
# partition the uses into phi uses and non-phi uses
|
||||
stack_vvar_uses = set()
|
||||
phi_use_locs: list[AILCodeLocation] = []
|
||||
for vvar_, loc_ in all_stack_vvar_uses:
|
||||
use_block = self._get_block(loc_.block_addr, idx=loc_.block_idx)
|
||||
if use_block is None or loc_.stmt_idx is None:
|
||||
continue
|
||||
use_stmt = use_block.statements[loc_.stmt_idx]
|
||||
if is_phi_assignment(use_stmt):
|
||||
phi_use_locs.append(loc_)
|
||||
continue
|
||||
stack_vvar_uses.add((vvar_, loc_))
|
||||
if len(stack_vvar_uses) != 1:
|
||||
continue
|
||||
_, stack_vvar_use_loc = next(iter(stack_vvar_uses))
|
||||
restore_block = self._get_block(stack_vvar_use_loc.block_addr, idx=stack_vvar_use_loc.block_idx)
|
||||
assert restore_block is not None
|
||||
restore_stmt = restore_block.statements[stack_vvar_use_loc.stmt_idx]
|
||||
|
||||
if not (
|
||||
isinstance(restore_stmt, Assignment)
|
||||
and isinstance(restore_stmt.src, VirtualVariable)
|
||||
and restore_stmt.src.varid == stack_vvar.varid
|
||||
and isinstance(restore_stmt.dst, VirtualVariable)
|
||||
and restore_stmt.dst.was_reg
|
||||
and restore_stmt.dst.reg_offset == stmt.src.reg_offset
|
||||
):
|
||||
continue
|
||||
# this is the dumb version of the dominance frontier check
|
||||
if self._within_dominance_frontier(def_block, restore_block, True, True):
|
||||
results.append(
|
||||
(stmt.src.varid, used_loc, stack_vvar.varid, stack_vvar_use_loc, stack_vvar.stack_offset)
|
||||
)
|
||||
if len(stack_vvar_uses) == 1:
|
||||
# case (a): a genuine store/restore pair
|
||||
_, stack_vvar_use_loc = next(iter(stack_vvar_uses))
|
||||
restore_block = self._get_block(stack_vvar_use_loc.block_addr, idx=stack_vvar_use_loc.block_idx)
|
||||
assert restore_block is not None
|
||||
restore_stmt = restore_block.statements[stack_vvar_use_loc.stmt_idx]
|
||||
|
||||
if not (
|
||||
isinstance(restore_stmt, Assignment)
|
||||
and isinstance(restore_stmt.src, VirtualVariable)
|
||||
and restore_stmt.src.varid == stack_vvar.varid
|
||||
and isinstance(restore_stmt.dst, VirtualVariable)
|
||||
and restore_stmt.dst.was_reg
|
||||
and restore_stmt.dst.reg_offset == stmt.src.reg_offset
|
||||
):
|
||||
continue
|
||||
# this is the dumb version of the dominance frontier check
|
||||
if self._within_dominance_frontier(def_block, restore_block, True, True):
|
||||
results.append(([used_loc, stack_vvar_use_loc], stack_vvar.stack_offset))
|
||||
elif not stack_vvar_uses and phi_use_locs and self._phi_uses_are_dead(srda_model, phi_use_locs):
|
||||
# case (b): a dead spill whose stack vvar only feeds dead phi nodes
|
||||
results.append(([used_loc, *phi_use_locs], stack_vvar.stack_offset))
|
||||
|
||||
return results
|
||||
|
||||
def _phi_uses_are_dead(self, srda_model, phi_use_locs: list[AILCodeLocation]) -> bool:
|
||||
"""Return True iff every phi statement at ``phi_use_locs`` defines a virtual variable that has no uses. Such a
|
||||
phi is dead and can be removed together with the store that feeds it."""
|
||||
|
||||
for loc in phi_use_locs:
|
||||
block = self._get_block(loc.block_addr, idx=loc.block_idx)
|
||||
if block is None or loc.stmt_idx is None:
|
||||
return False
|
||||
phi_stmt = block.statements[loc.stmt_idx]
|
||||
if not (isinstance(phi_stmt, Assignment) and isinstance(phi_stmt.dst, VirtualVariable)):
|
||||
return False
|
||||
if srda_model.all_vvar_uses.get(phi_stmt.dst.varid, []):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _within_dominance_frontier(self, dom_node, node, use_preds: bool, use_succs: bool) -> bool:
|
||||
if use_succs:
|
||||
# scan forward
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.calling_conventions import DEFAULT_CC, SimRegArg, default_cc
|
||||
|
||||
from .optimization_pass import OptimizationPass, OptimizationPassStage
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from typing import Any
|
|||
|
||||
import networkx
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.ailment import AILBlockRewriter, Block
|
||||
from angr.ailment.expression import Const, Phi, VirtualVariable
|
||||
from angr.ailment.statement import Assignment, ConditionalJump, Jump, Label, Return, SideEffectStatement
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.analyses.decompiler.stack_item import StackItem, StackItemType
|
||||
from angr.utils.bits import s2u
|
||||
|
||||
|
|
@ -78,9 +78,7 @@ class StackCanarySimplifier(OptimizationPass):
|
|||
pred_addr_to_endpoint_addrs[pred.addr].add(node_addr)
|
||||
|
||||
found_endpoints = False
|
||||
for pred_addr in pred_addr_to_endpoint_addrs:
|
||||
endpoint_addrs = pred_addr_to_endpoint_addrs[pred_addr]
|
||||
|
||||
for endpoint_addrs in pred_addr_to_endpoint_addrs.values():
|
||||
if len(endpoint_addrs) != 2:
|
||||
# we expect there to be only two nodes: one for canary-check-success, and the other for
|
||||
# canary-check-failure. if not, we check the next predecessor
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
|
||||
from .optimization_pass import OptimizationPass, OptimizationPassStage
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,18 @@ if TYPE_CHECKING:
|
|||
class PeepholeOptimizationStmtBase:
|
||||
"""
|
||||
The base class for all peephole optimizations that are applied on AIL statements.
|
||||
|
||||
``fixpoint_reached`` is an output parameter; it tells the caller whether this optimizer should run on this
|
||||
statement again (e.g., when statements prior to the current statement are optimized and changed). The caller sets
|
||||
``fixpoint_reached`` to True before every :meth:`optimize` call; the optimizer optionally sets it to False if it
|
||||
wants to be invoked again on the same statement.
|
||||
|
||||
So, ``fixpoint_reached`` should be set to (or kept) True if (a) the optimizer has optimized the statement and does
|
||||
not expect to optimize it ever again, or (b) the optimizer cannot ever optimize the statement.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"fixpoint_reached",
|
||||
"func_addr",
|
||||
"kb",
|
||||
"manager",
|
||||
|
|
@ -31,6 +40,7 @@ class PeepholeOptimizationStmtBase:
|
|||
func_addr: int | None
|
||||
preserve_vvar_ids: set[int]
|
||||
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]]
|
||||
fixpoint_reached: bool
|
||||
|
||||
NAME = "Peephole Optimization - Statement"
|
||||
DESCRIPTION = "Peephole Optimization - Statement"
|
||||
|
|
@ -51,6 +61,7 @@ class PeepholeOptimizationStmtBase:
|
|||
self.func_addr = func_addr
|
||||
self.preserve_vvar_ids = set() if preserve_vvar_ids is None else preserve_vvar_ids
|
||||
self.type_hints = [] if type_hints is None else type_hints
|
||||
self.fixpoint_reached = False
|
||||
|
||||
def optimize(self, stmt, stmt_idx: int | None = None, block=None, **kwargs):
|
||||
raise NotImplementedError("_optimize() is not implemented.")
|
||||
|
|
@ -59,9 +70,13 @@ class PeepholeOptimizationStmtBase:
|
|||
class PeepholeOptimizationMultiStmtBase:
|
||||
"""
|
||||
The base class for all peephole optimizations that are applied on multiple AIL statements at once.
|
||||
|
||||
``fixpoint_reached`` exists for uniformity but is unused. Multi-statement optimizers always run regardless of
|
||||
whether the statements have reached fixed points or not.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"fixpoint_reached",
|
||||
"func_addr",
|
||||
"kb",
|
||||
"manager",
|
||||
|
|
@ -74,6 +89,7 @@ class PeepholeOptimizationMultiStmtBase:
|
|||
func_addr: int | None
|
||||
preserve_vvar_ids: set[int]
|
||||
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]]
|
||||
fixpoint_reached: bool
|
||||
|
||||
NAME = "Peephole Optimization - Multi-statement"
|
||||
DESCRIPTION = "Peephole Optimization - Multi-statement"
|
||||
|
|
@ -94,6 +110,7 @@ class PeepholeOptimizationMultiStmtBase:
|
|||
self.func_addr = func_addr
|
||||
self.preserve_vvar_ids = set() if preserve_vvar_ids is None else preserve_vvar_ids
|
||||
self.type_hints = [] if type_hints is None else type_hints
|
||||
self.fixpoint_reached = False
|
||||
|
||||
def optimize(self, stmts: list[Statement], stmt_idx: int | None = None, block=None, **kwargs):
|
||||
raise NotImplementedError("_optimize() is not implemented.")
|
||||
|
|
@ -101,10 +118,12 @@ class PeepholeOptimizationMultiStmtBase:
|
|||
|
||||
class PeepholeOptimizationExprBase:
|
||||
"""
|
||||
The base class for all peephole optimizations that are applied on AIL expressions.
|
||||
The base class for all peephole optimizations that are applied on AIL expressions. Please refer to
|
||||
:class:`PeepholeOptimizationStmtBase` for the ``fixpoint_reached`` contract.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"fixpoint_reached",
|
||||
"func_addr",
|
||||
"kb",
|
||||
"manager",
|
||||
|
|
@ -117,6 +136,7 @@ class PeepholeOptimizationExprBase:
|
|||
func_addr: int | None
|
||||
preserve_vvar_ids: set[int]
|
||||
type_hints: list[tuple[atoms.VirtualVariable | atoms.MemoryLocation, str]]
|
||||
fixpoint_reached: bool
|
||||
|
||||
NAME = "Peephole Optimization - Expression"
|
||||
DESCRIPTION = "Peephole Optimization - Expression"
|
||||
|
|
@ -137,6 +157,7 @@ class PeepholeOptimizationExprBase:
|
|||
self.func_addr = func_addr
|
||||
self.preserve_vvar_ids = set() if preserve_vvar_ids is None else preserve_vvar_ids
|
||||
self.type_hints = [] if type_hints is None else type_hints
|
||||
self.fixpoint_reached = False
|
||||
|
||||
def optimize(self, expr, *, stmt_idx: int | None = None, block=None, **kwargs) -> Expression | None:
|
||||
raise NotImplementedError("_optimize() is not implemented.")
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class SimplifyBitwiseInserts(PeepholeOptimizationExprBase):
|
|||
# I don't know if 0 is right here
|
||||
pb2l.append((0, potential_base.operand, other_base))
|
||||
|
||||
for (pb1o, pb1), (pb2o, pb2, pb2x) in itertools.product(pb1l, pb2l): # noqa: B007
|
||||
for (pb1o, pb1), (pb2o, pb2, pb2x) in itertools.product(pb1l, pb2l):
|
||||
if pb1o == pb2o and pb1.bits == pb2.bits and pb1.likes(pb2):
|
||||
break
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class Bswap(PeepholeOptimizationExprBase):
|
|||
(">>", 8, 0xFF00),
|
||||
}:
|
||||
core_expr = next(iter(cores))
|
||||
return Call(expr.idx, "__buildin_bswap32", args=[core_expr], bits=expr.bits, **expr.tags)
|
||||
return Call(expr.idx, "__builtin_bswap32", args=[core_expr], bits=expr.bits, **expr.tags)
|
||||
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from angr.ailment import Const
|
||||
from angr.ailment.expression import BinaryOp, Call, Expression, Load, Tmp
|
||||
from angr.ailment.expression import ITE, BinaryOp, Call, Expression, Load, Tmp
|
||||
from angr.ailment.statement import CAS, Assignment, ConditionalJump, Statement
|
||||
|
||||
from .base import PeepholeOptimizationMultiStmtBase
|
||||
|
|
@ -27,6 +27,16 @@ _INTRINSICS_NAMES = {
|
|||
}
|
||||
|
||||
|
||||
def cas_intrinsic_name(mnemonic: str, os_name: str | None) -> str:
|
||||
"""
|
||||
Resolve the intrinsic name for a lock-prefixed instruction on the given OS, falling back to Linux naming.
|
||||
"""
|
||||
if mnemonic not in _INTRINSICS_NAMES:
|
||||
return mnemonic
|
||||
names = _INTRINSICS_NAMES[mnemonic]
|
||||
return names[os_name] if os_name in names else names["Linux"]
|
||||
|
||||
|
||||
class CASIntrinsics(PeepholeOptimizationMultiStmtBase):
|
||||
"""
|
||||
Rewrite lock-prefixed instructions (or rather, their VEX/AIL forms) into intrinsic calls.
|
||||
|
|
@ -135,7 +145,9 @@ class CASIntrinsics(PeepholeOptimizationMultiStmtBase):
|
|||
stmt = Assignment(cas_stmt.idx, assignment_dst, call_expr, **cas_stmt.tags) # type: ignore
|
||||
return [stmt]
|
||||
|
||||
if next_stmt.tags["ins_addr"] <= cas_stmt.tags["ins_addr"]:
|
||||
if next_stmt.tags["ins_addr"] <= cas_stmt.tags["ins_addr"] and not self._is_cas_writeback_ite(
|
||||
cas_stmt, next_stmt
|
||||
):
|
||||
# avoid matching against statements prematurely
|
||||
return None
|
||||
|
||||
|
|
@ -159,17 +171,31 @@ class CASIntrinsics(PeepholeOptimizationMultiStmtBase):
|
|||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _is_cas_writeback_ite(cas_stmt: CAS, stmt: Statement) -> bool:
|
||||
"""
|
||||
Detect the ITE assignment that models "cmpxchg writes the memory value back into the accumulator". It belongs
|
||||
to the same instruction as the CAS, so the same-instruction guard above would otherwise keep this shape from
|
||||
ever being rewritten. Case 1 cannot apply to it either, because that requires the next statement to be a
|
||||
CasCmpNE conditional jump.
|
||||
|
||||
CAS(addr, expd_lo=X, data_lo=D, old_lo=OLD)
|
||||
vvar = (X == OLD) ? X : OLD
|
||||
"""
|
||||
if cas_stmt.old_lo is None or not isinstance(stmt, Assignment) or not isinstance(stmt.src, ITE):
|
||||
return False
|
||||
ite = stmt.src
|
||||
if not (isinstance(ite.cond, BinaryOp) and ite.cond.op == "CmpEQ"):
|
||||
return False
|
||||
expd, old = cas_stmt.expd_lo, cas_stmt.old_lo
|
||||
cond_op0, cond_op1 = ite.cond.operands
|
||||
return ((cond_op0.likes(expd) and cond_op1.likes(old)) or (cond_op0.likes(old) and cond_op1.likes(expd))) and (
|
||||
(ite.iftrue.likes(expd) and ite.iffalse.likes(old)) or (ite.iftrue.likes(old) and ite.iffalse.likes(expd))
|
||||
)
|
||||
|
||||
def _get_instrincs_name(self, mnemonic: str) -> str:
|
||||
if mnemonic in _INTRINSICS_NAMES:
|
||||
os = (
|
||||
self.project.simos.name
|
||||
if self.project is not None and self.project.simos is not None and self.project.simos.name is not None
|
||||
else "Linux"
|
||||
)
|
||||
if os not in _INTRINSICS_NAMES[mnemonic]:
|
||||
os = "Linux"
|
||||
return _INTRINSICS_NAMES[mnemonic][os]
|
||||
return mnemonic
|
||||
os_name = self.project.simos.name if self.project is not None and self.project.simos is not None else None
|
||||
return cas_intrinsic_name(mnemonic, os_name)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_tmp_expr(expr: Expression, block) -> Expression:
|
||||
|
|
|
|||
|
|
@ -14,14 +14,15 @@ class CoalesceSameCascadingIfs(PeepholeOptimizationStmtBase):
|
|||
|
||||
def optimize(self, stmt: ConditionalJump, stmt_idx: int | None = None, block=None, **kwargs):
|
||||
cond = stmt.condition
|
||||
true_target_in = stmt.true_target
|
||||
|
||||
# if (cond) {ITE(cond, true_branch, false_branch)} else {} ==> if (cond) {true_branch} else {}
|
||||
if isinstance(stmt.true_target, ITE) and cond == stmt.true_target.cond:
|
||||
new_true_target = stmt.true_target.iftrue
|
||||
if isinstance(true_target_in, ITE) and cond == true_target_in.cond:
|
||||
new_true_target = true_target_in.iftrue
|
||||
else:
|
||||
new_true_target = stmt.true_target
|
||||
new_true_target = true_target_in
|
||||
|
||||
if cond is not stmt.condition or new_true_target is not stmt.true_target:
|
||||
if new_true_target != true_target_in:
|
||||
# it's updated
|
||||
return ConditionalJump(
|
||||
stmt.idx, cond, new_true_target, stmt.false_target, false_target_idx=stmt.false_target_idx, **stmt.tags
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@ class ConcatSimplifier(PeepholeOptimizationExprBase):
|
|||
low,
|
||||
**expr.tags,
|
||||
)
|
||||
# we may start matching once the block changes
|
||||
self.fixpoint_reached = False
|
||||
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from math import gcd
|
||||
|
||||
from angr.ailment import ExpressionKind
|
||||
from angr.ailment.expression import BinaryOp, Const, Convert, StackBaseOffset, UnaryOp
|
||||
from angr.utils.bits import sign_extend
|
||||
|
||||
|
|
@ -29,6 +30,10 @@ class EagerEvaluation(PeepholeOptimizationExprBase):
|
|||
|
||||
@staticmethod
|
||||
def _optimize_binaryop(expr: BinaryOp):
|
||||
# The identities below assume integer arithmetic and are not generally valid under IEEE 754.
|
||||
if expr.floating_point:
|
||||
return None
|
||||
|
||||
op0, op1 = expr.operands
|
||||
if expr.op == "Add":
|
||||
if (
|
||||
|
|
@ -212,7 +217,7 @@ class EagerEvaluation(PeepholeOptimizationExprBase):
|
|||
# constant multiplication
|
||||
mask = (1 << expr.bits) - 1
|
||||
return Const(expr.idx, (op0.value * op1.value) & mask, expr.bits, **expr.tags)
|
||||
if {type(op0), type(op1)} == {BinaryOp, Const}:
|
||||
if {op0.pykind, op1.pykind} == {ExpressionKind.BinaryOp, ExpressionKind.Const}:
|
||||
op0, op1 = expr.operands
|
||||
const_, x0 = (op0, op1) if isinstance(op0, Const) else (op1, op0)
|
||||
if x0.op == "Mul" and (isinstance(x0.operands[0], Const) or isinstance(x0.operands[1], Const)):
|
||||
|
|
@ -227,17 +232,20 @@ class EagerEvaluation(PeepholeOptimizationExprBase):
|
|||
elif (
|
||||
expr.op == "Div"
|
||||
and isinstance(op1, Const)
|
||||
and op1.is_int
|
||||
and isinstance(op0, BinaryOp)
|
||||
and op0.op == "Mul"
|
||||
and not op0.floating_point
|
||||
and isinstance(op0.operands[1], Const)
|
||||
and op0.operands[1].is_int
|
||||
):
|
||||
expr0, const_0 = expr.operands
|
||||
const_1 = expr0.operands[1]
|
||||
if const_0.value != 0 and const_1.value != 0:
|
||||
gcd_ = gcd(const_0.value, const_1.value)
|
||||
if const_0.value_int != 0 and const_1.value_int != 0:
|
||||
gcd_ = gcd(const_0.value_int, const_1.value_int)
|
||||
if gcd_ != 1:
|
||||
new_const_1 = Const(const_1.idx, const_1.value // gcd_, const_1.bits, **const_1.tags)
|
||||
new_const_0 = Const(const_0.idx, const_0.value // gcd_, const_0.bits, **const_0.tags)
|
||||
new_const_1 = Const(const_1.idx, const_1.value_int // gcd_, const_1.bits, **const_1.tags)
|
||||
new_const_0 = Const(const_0.idx, const_0.value_int // gcd_, const_0.bits, **const_0.tags)
|
||||
mul = BinaryOp(
|
||||
expr0.idx,
|
||||
"Mul",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ class ExtendedByteAndMask(PeepholeOptimizationExprBase):
|
|||
expr_classes = (BinaryOp,) # all expressions are allowed
|
||||
|
||||
def optimize(self, expr: BinaryOp, **kwargs):
|
||||
#
|
||||
if expr.op == "And" and isinstance(expr.operands[1], Const):
|
||||
mask = expr.operands[1].value
|
||||
to_bits = _MASK_TO_BITS.get(mask)
|
||||
|
|
|
|||
|
|
@ -13,26 +13,25 @@ class RemoveEmptyIfBody(PeepholeOptimizationStmtBase):
|
|||
stmt_classes = (ConditionalJump,)
|
||||
|
||||
def optimize(self, stmt: ConditionalJump, stmt_idx: int | None = None, block=None, **kwargs):
|
||||
cond = stmt.condition
|
||||
cond_in = stmt.condition
|
||||
true_target_in = stmt.true_target
|
||||
false_target_in = stmt.false_target
|
||||
cond = cond_in
|
||||
|
||||
# if (!cond) {} else { ITE(cond, true_branch, false_branch } ==> if (cond) { ITE(...) } else {}
|
||||
if isinstance(stmt.false_target, ITE) and isinstance(cond, UnaryOp) and cond.op == "Not":
|
||||
new_true_target = stmt.false_target
|
||||
if isinstance(false_target_in, ITE) and isinstance(cond, UnaryOp) and cond.op == "Not":
|
||||
new_true_target = false_target_in
|
||||
new_true_idx = stmt.false_target_idx
|
||||
new_false_target = stmt.true_target
|
||||
new_false_target = true_target_in
|
||||
new_false_idx = stmt.true_target_idx
|
||||
cond = cond.operand
|
||||
else:
|
||||
new_true_target = stmt.true_target
|
||||
new_true_target = true_target_in
|
||||
new_true_idx = stmt.true_target_idx
|
||||
new_false_target = stmt.false_target
|
||||
new_false_target = false_target_in
|
||||
new_false_idx = stmt.false_target_idx
|
||||
|
||||
if (
|
||||
cond is not stmt.condition
|
||||
or new_true_target is not stmt.true_target
|
||||
or new_false_target is not stmt.false_target
|
||||
):
|
||||
if cond != cond_in or new_true_target != true_target_in or new_false_target != false_target_in:
|
||||
# it's updated
|
||||
return ConditionalJump(
|
||||
stmt.idx,
|
||||
|
|
|
|||
|
|
@ -139,8 +139,17 @@ class RemoveRedundantBitmasks(PeepholeOptimizationExprBase):
|
|||
new_op0 = op0.operands[0]
|
||||
replaced, new_operand_expr = operand_expr.replace(op0, new_op0)
|
||||
if replaced:
|
||||
expr.operand = new_operand_expr
|
||||
return expr
|
||||
return Convert(
|
||||
self.manager.next_atom(),
|
||||
expr.from_bits,
|
||||
expr.to_bits,
|
||||
expr.is_signed,
|
||||
new_operand_expr,
|
||||
from_type=expr.from_type,
|
||||
to_type=expr.to_type,
|
||||
rounding_mode=expr.rounding_mode,
|
||||
**expr.tags,
|
||||
)
|
||||
# Conv(64->32, (expr) - (expr) & 0xffffffff<64>)))
|
||||
# => Conv(64->32, (expr - expr))
|
||||
elif (
|
||||
|
|
|
|||
|
|
@ -19,12 +19,19 @@ class RemoveRedundantShifts(PeepholeOptimizationExprBase):
|
|||
|
||||
def optimize(self, expr: BinaryOp, **kwargs):
|
||||
# (expr << N) >> N ==> Convert((M-N)->M, Convert(M->(M-N), expr))
|
||||
#
|
||||
# For a *logical* right shift (Shr) the outer conversion zero-extends, which the C backend renders as a
|
||||
# bitmask (e.g. `& 0xfff`) that is correct for any width. For an *arithmetic* right shift (Sar) the outer
|
||||
# conversion must sign-extend the low (M-N) bits; that is only rendered faithfully by the C backend when
|
||||
# (M-N) is a standard integer width (8/16/32/64). For non-standard widths we leave the Sar/Shl pair intact
|
||||
# (which renders as a signed `(expr << N) >> N`) rather than emit a bogus zero-extend mask that silently
|
||||
# drops the sign bit.
|
||||
if expr.op in ("Shr", "Sar") and isinstance(expr.operands[1], Const):
|
||||
expr_a = expr.operands[0]
|
||||
n0 = expr.operands[1].value
|
||||
if isinstance(expr_a, BinaryOp) and expr_a.op in {"Shl", "Mul"} and isinstance(expr_a.operands[1], Const):
|
||||
n1 = get_expr_shift_left_amount(expr_a)
|
||||
if n0 == n1:
|
||||
if n0 == n1 and (expr.op == "Shr" or (expr_a.bits - n0) in (8, 16, 32, 64)):
|
||||
inner_expr = expr_a.operands[0]
|
||||
conv_inner_expr = Convert(
|
||||
self.manager.next_atom(),
|
||||
|
|
@ -38,7 +45,7 @@ class RemoveRedundantShifts(PeepholeOptimizationExprBase):
|
|||
self.manager.next_atom(),
|
||||
expr_a.bits - n0,
|
||||
expr.bits,
|
||||
False,
|
||||
expr.op == "Sar", # sign-extend for arithmetic shift, zero-extend for logical shift
|
||||
conv_inner_expr,
|
||||
**expr.tags,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ class RolRorRewriter(PeepholeOptimizationStmtBase):
|
|||
|
||||
op0, op1 = stmt.src.operands
|
||||
if isinstance(op0, Tmp) and isinstance(op1, Tmp):
|
||||
# matches against the two preceding statements, so it may start matching once the block changes
|
||||
self.fixpoint_reached = False
|
||||
|
||||
if stmt_idx < 2:
|
||||
return None
|
||||
|
||||
|
|
@ -70,6 +73,7 @@ class RolRorRewriter(PeepholeOptimizationStmtBase):
|
|||
and shiftleft_amount + stmt2_op1.value == stmt.dst.bits
|
||||
):
|
||||
rol_amount = Const(self.manager.next_atom(), shiftleft_amount, 8, **stmt1_op1.tags)
|
||||
self.fixpoint_reached = True
|
||||
return Assignment(
|
||||
stmt.idx,
|
||||
stmt.dst,
|
||||
|
|
@ -89,6 +93,7 @@ class RolRorRewriter(PeepholeOptimizationStmtBase):
|
|||
and (shiftleft_amount := get_expr_shift_left_amount(stmt_2.src)) is not None
|
||||
and stmt1_op1.value + shiftleft_amount == stmt.dst.bits
|
||||
):
|
||||
self.fixpoint_reached = True
|
||||
return Assignment(
|
||||
stmt.idx,
|
||||
stmt.dst,
|
||||
|
|
|
|||
|
|
@ -19,10 +19,13 @@ class SarToSignedDiv(PeepholeOptimizationExprBase):
|
|||
if expr.op == "Sar" and isinstance(expr.operands[1], Const):
|
||||
op0, const = expr.operands
|
||||
|
||||
if isinstance(op0, VirtualVariable) and op0.was_reg and stmt_idx is not None and block is not None:
|
||||
# look back by one statement to find its definition
|
||||
op0 = self.find_definition(op0, stmt_idx, block)
|
||||
# TODO: Ensure the new op0 does not have any expressions that overlap with the old op0 (a register)
|
||||
if isinstance(op0, VirtualVariable) and op0.was_reg:
|
||||
# depends on the preceding statement, so it may start matching once the block changes
|
||||
self.fixpoint_reached = False
|
||||
if stmt_idx is not None and block is not None:
|
||||
# look back by one statement to find its definition
|
||||
op0 = self.find_definition(op0, stmt_idx, block)
|
||||
# TODO: Ensure the new op0 does not have any expressions that overlap with the old op0 (a register)
|
||||
|
||||
const_value = const.value
|
||||
conv = None
|
||||
|
|
@ -75,6 +78,8 @@ class SarToSignedDiv(PeepholeOptimizationExprBase):
|
|||
if conv is not None:
|
||||
# wrap it up with a Convert again
|
||||
r = Convert(conv.idx, conv.from_bits, conv.to_bits, conv.is_signed, r, **conv.tags)
|
||||
# rewritten: the result no longer depends on the block context
|
||||
self.fixpoint_reached = True
|
||||
return r
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# pylint:disable=unused-argument
|
||||
from __future__ import annotations
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
|
||||
from .sequence_walker import SequenceWalker
|
||||
from .structurer_nodes import SequenceNode
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from angr.utils.doms import IncrementalDominators
|
|||
from angr.utils.graph import GraphUtils, dfs_back_edges, dominates, subgraph_between_nodes
|
||||
|
||||
from .condition_processor import ConditionProcessor
|
||||
from .region_overlay import OverlayManager, RegionOverlay
|
||||
from .region_overlay import OverlayManager, RegionOverlay, Tx
|
||||
from .structurer_nodes import ConditionNode, IncompleteSwitchCaseHeadStatement, MultiNode
|
||||
from .utils import copy_graph, first_nonlabel_nonphi_statement, replace_last_statement
|
||||
|
||||
|
|
@ -29,7 +29,9 @@ l = logging.getLogger(name=__name__)
|
|||
# an ever-incrementing counter
|
||||
CONDITIONNODE_ADDR = count(0xFF000000)
|
||||
|
||||
type TNode = Block | RegionOverlay | MultiNode | ConditionNode
|
||||
type TNode = Tx[Block | MultiNode | ConditionNode]
|
||||
type TOverlay = RegionOverlay[Block | MultiNode | ConditionNode]
|
||||
type TManager = OverlayManager[Block | MultiNode | ConditionNode]
|
||||
type TGraph = "networkx.DiGraph[TNode]"
|
||||
|
||||
|
||||
|
|
@ -78,8 +80,8 @@ class RegionIdentifier(Analysis):
|
|||
# copy the graph so updates don't affect the original graph
|
||||
graph = copy_graph(graph) # type: ignore
|
||||
|
||||
self.region: RegionOverlay | None = None
|
||||
self.overlay_manager: OverlayManager | None = None
|
||||
self.region: TOverlay | None = None
|
||||
self.overlay_manager: TManager | None = None
|
||||
self._start_node = None
|
||||
self._loop_headers: list | None = None
|
||||
self.regions_by_block_addrs = []
|
||||
|
|
@ -89,7 +91,7 @@ class RegionIdentifier(Analysis):
|
|||
self._expose_loop_head_backedges = expose_loop_head_backedges
|
||||
# we keep a dictionary of node and their traversal order in a quasi-topological traversal and update this
|
||||
# dictionary as we update the graph
|
||||
self._node_order: dict[Any, tuple[int, int]] = {}
|
||||
self._node_order: dict[TNode, tuple[int, int]] = {}
|
||||
|
||||
self._graph = self._analyze(graph)
|
||||
|
||||
|
|
@ -182,11 +184,11 @@ class RegionIdentifier(Analysis):
|
|||
"""
|
||||
|
||||
assert self.region is not None
|
||||
work_list: list[RegionOverlay] = [self.region]
|
||||
work_list: list[TOverlay] = [self.region]
|
||||
block_only_regions = []
|
||||
seen_regions = set()
|
||||
while work_list:
|
||||
children_regions: list[RegionOverlay] = []
|
||||
children_regions: list[TOverlay] = []
|
||||
for region in work_list:
|
||||
children_blocks = []
|
||||
for node in region.members:
|
||||
|
|
@ -198,7 +200,7 @@ class RegionIdentifier(Analysis):
|
|||
if node not in seen_regions:
|
||||
children_regions.append(node)
|
||||
children_blocks.append(
|
||||
(node.head.addr, node.head.idx if hasattr(node.head, "idx") else None)
|
||||
(node.head.addr, node.head.idx if hasattr(node.head, "idx") else None) # type: ignore
|
||||
)
|
||||
seen_regions.add(node)
|
||||
else:
|
||||
|
|
@ -459,11 +461,11 @@ class RegionIdentifier(Analysis):
|
|||
|
||||
return refined_loop_nodes, refined_exit_nodes
|
||||
|
||||
def _make_regions(self, graph: TGraph) -> RegionOverlay:
|
||||
def _make_regions(self, graph: TGraph) -> TOverlay:
|
||||
assert self.overlay_manager is not None
|
||||
root = self.overlay_manager.root
|
||||
structured_loop_headers = set()
|
||||
new_regions: list[RegionOverlay] = []
|
||||
new_regions: list[TOverlay] = []
|
||||
|
||||
# FIXME: _get_start_node() will fail if the graph is just a loop
|
||||
|
||||
|
|
@ -518,6 +520,7 @@ class RegionIdentifier(Analysis):
|
|||
# No more loops left. Structure acyclic regions.
|
||||
while new_regions:
|
||||
region = new_regions.pop(0)
|
||||
assert region.head is not None
|
||||
head = region.head
|
||||
# collapse a working copy of the region body during acyclic region identification; for the root region,
|
||||
# the phase-1 working graph already matches its member-level view
|
||||
|
|
@ -618,7 +621,7 @@ class RegionIdentifier(Analysis):
|
|||
|
||||
return region
|
||||
|
||||
def _refine_loop_successors_to_guarded_successors(self, region: RegionOverlay, graph: TGraph):
|
||||
def _refine_loop_successors_to_guarded_successors(self, region: TOverlay, graph: TGraph):
|
||||
"""
|
||||
If there are multiple successors of a loop, convert them into guarded successors. Eventually there should be
|
||||
only one loop successor. This is used in the DREAM structuring algorithm.
|
||||
|
|
@ -747,7 +750,7 @@ class RegionIdentifier(Analysis):
|
|||
self,
|
||||
head: TNode,
|
||||
graph: TGraph,
|
||||
parent_region: RegionOverlay,
|
||||
parent_region: TOverlay,
|
||||
failed_region_attempts: set[tuple[TNode, TNode]],
|
||||
cyclic: bool,
|
||||
):
|
||||
|
|
@ -873,7 +876,7 @@ class RegionIdentifier(Analysis):
|
|||
return region_created
|
||||
|
||||
@staticmethod
|
||||
def _update_graph(graph: TGraph, new_region: RegionOverlay, replaced_nodes: set[TNode]) -> None:
|
||||
def _update_graph(graph: TGraph, new_region: TOverlay, replaced_nodes: set[TNode]) -> None:
|
||||
region_in_edges = RegionIdentifier._region_in_edges(graph, new_region, data=True)
|
||||
region_out_edges = RegionIdentifier._region_out_edges(graph, new_region, data=True)
|
||||
for node in replaced_nodes:
|
||||
|
|
@ -954,7 +957,7 @@ class RegionIdentifier(Analysis):
|
|||
@staticmethod
|
||||
def _abstract_acyclic_region(
|
||||
graph: TGraph,
|
||||
region: RegionOverlay,
|
||||
region: TOverlay,
|
||||
frontier: set[TNode],
|
||||
node_order: dict[TNode, tuple[int, int]],
|
||||
dummy_endnode: TNode | None = None,
|
||||
|
|
@ -993,7 +996,7 @@ class RegionIdentifier(Analysis):
|
|||
normal_exit_node: TNode | None,
|
||||
abnormal_exit_nodes: set[TNode],
|
||||
node_order: dict[TNode, tuple[int, int]],
|
||||
) -> RegionOverlay:
|
||||
) -> TOverlay:
|
||||
loop_nodes = set(loop_nodes)
|
||||
region = self._parent_overlay_of(head).create_subregion(head, loop_nodes, cyclic=True)
|
||||
|
||||
|
|
@ -1033,7 +1036,7 @@ class RegionIdentifier(Analysis):
|
|||
|
||||
return region
|
||||
|
||||
def _parent_overlay_of(self, node: TNode) -> RegionOverlay:
|
||||
def _parent_overlay_of(self, node: TNode) -> TOverlay:
|
||||
"""Find the overlay that the given working-graph node is currently a direct member of."""
|
||||
if isinstance(node, RegionOverlay):
|
||||
assert node.parent is not None
|
||||
|
|
@ -1046,19 +1049,19 @@ class RegionIdentifier(Analysis):
|
|||
@overload
|
||||
@staticmethod
|
||||
def _region_in_edges(
|
||||
graph: TGraph, region: RegionOverlay, data: Literal[True]
|
||||
graph: TGraph, region: TOverlay, data: Literal[True]
|
||||
) -> list[tuple[TNode, TNode, dict[str, Any]]]: ...
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def _region_in_edges(graph: TGraph, region: RegionOverlay, data: Literal[False]) -> list[tuple[TNode, TNode]]: ...
|
||||
def _region_in_edges(graph: TGraph, region: TOverlay, data: Literal[False]) -> list[tuple[TNode, TNode]]: ...
|
||||
|
||||
@staticmethod
|
||||
def _region_in_edges(graph, region, data=False):
|
||||
return list(graph.in_edges(region.head, data=data))
|
||||
|
||||
@staticmethod
|
||||
def _region_out_edges(graph, region: RegionOverlay, data=False):
|
||||
def _region_out_edges(graph, region: TOverlay, data=False):
|
||||
out_edges = []
|
||||
for node in region.members:
|
||||
out_ = graph.out_edges(node, data=data)
|
||||
|
|
@ -1130,6 +1133,7 @@ class RegionIdentifier(Analysis):
|
|||
|
||||
def _ensure_jump_at_loop_exit_ends(self, node: TNode) -> None:
|
||||
if isinstance(node, Block):
|
||||
assert node.original_size is not None
|
||||
if not node.statements:
|
||||
node.statements.append(
|
||||
Jump(
|
||||
|
|
|
|||
|
|
@ -1,18 +1,30 @@
|
|||
# pylint:disable=protected-access
|
||||
# pylint:disable=protected-access,invalid-sequence-index,unsubscriptable-object
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping
|
||||
from typing import Any
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Protocol, cast
|
||||
|
||||
import networkx
|
||||
|
||||
l = logging.getLogger(name=__name__)
|
||||
|
||||
|
||||
class OverlayManager:
|
||||
class RegionBound(Protocol):
|
||||
"""
|
||||
The protocol indicating the minimum capabilities of graph nodes to be managed by an overlay region.
|
||||
"""
|
||||
|
||||
@property
|
||||
def addr(self) -> int: ...
|
||||
|
||||
|
||||
type Tx[U: RegionBound] = "U | RegionOverlay[U]"
|
||||
|
||||
|
||||
class OverlayManager[T: RegionBound]:
|
||||
"""
|
||||
OverlayManager owns the single shared control-flow graph that all RegionOverlay objects are views of, plus the
|
||||
node-to-innermost-overlay ownership map.
|
||||
|
|
@ -32,8 +44,8 @@ class OverlayManager:
|
|||
"root",
|
||||
)
|
||||
|
||||
def __init__(self, graph: networkx.DiGraph, expose_loop_head_backedges: bool = False):
|
||||
self.graph = graph
|
||||
def __init__(self, graph: networkx.DiGraph[T], expose_loop_head_backedges: bool = False):
|
||||
self.graph = cast("networkx.DiGraph[Tx[T]]", graph)
|
||||
self.expose_loop_head_backedges = expose_loop_head_backedges
|
||||
self._version: int = 0
|
||||
# per-node topology version: bumped when an edge incident to a node changes (in the shared graph or in the
|
||||
|
|
@ -42,26 +54,27 @@ class OverlayManager:
|
|||
# flip node ownership/representatives broadly (create_subregion/dissolve/finalize) and by rollback (whose
|
||||
# inverse closures mutate the graph outside the touch-instrumented primitives); a change forces a full
|
||||
# cache clear.
|
||||
self._node_version: dict[Any, int] = {}
|
||||
self._node_version: dict[Tx[T], int] = {}
|
||||
self._adj_epoch: int = 0
|
||||
self._undo_log: list[Callable[[], None]] | None = None
|
||||
|
||||
# NOTE: this is the only callsite w/ head=None. If we can add a head param to this constructor we can clean up
|
||||
self.root = RegionOverlay(self, None, cyclic=False)
|
||||
self.root._under = set(graph)
|
||||
self.root._members = set(graph)
|
||||
self._owner: dict[Any, RegionOverlay] = dict.fromkeys(graph, self.root)
|
||||
self._owner: dict[Tx[T], RegionOverlay[T]] = dict.fromkeys(graph, self.root)
|
||||
|
||||
@property
|
||||
def version(self) -> int:
|
||||
return self._version
|
||||
|
||||
def owner_of(self, node) -> RegionOverlay | None:
|
||||
def owner_of(self, node: Tx[T]) -> RegionOverlay[T] | None:
|
||||
return self._owner.get(node)
|
||||
|
||||
def _bump(self) -> None:
|
||||
self._version += 1
|
||||
|
||||
def _touch(self, node) -> None:
|
||||
def _touch(self, node: Tx[T]) -> None:
|
||||
"""Bump a node's topology version (its cached view-adjacency must be rebuilt). Also bump every enclosing
|
||||
overlay: in an enclosing region's view the node is represented by the child overlay that contains it, so a
|
||||
change to the node's edges changes that representative's adjacency too. Monotonic; not undone on rollback,
|
||||
|
|
@ -188,7 +201,7 @@ class OverlayManager:
|
|||
self._record(inverse)
|
||||
|
||||
|
||||
class RegionOverlay:
|
||||
class RegionOverlay[T: RegionBound]:
|
||||
"""
|
||||
A single-entry region marked over the shared graph held by an OverlayManager. The region tree is built out of
|
||||
RegionOverlay objects (RegionIdentifier emits them) and they are the only region type the decompiler uses.
|
||||
|
|
@ -231,8 +244,8 @@ class RegionOverlay:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mgr: OverlayManager,
|
||||
head,
|
||||
mgr: OverlayManager[T],
|
||||
head: Tx[T] | None,
|
||||
cyclic: bool,
|
||||
cyclic_ancestor: bool = False,
|
||||
parent: RegionOverlay | None = None,
|
||||
|
|
@ -243,16 +256,16 @@ class RegionOverlay:
|
|||
self.cyclic_ancestor = cyclic_ancestor
|
||||
self.parent = parent
|
||||
self.children: list[RegionOverlay] = []
|
||||
self._members: set = set()
|
||||
self._under: set = set()
|
||||
self._members: set[Tx[T]] = set()
|
||||
self._under: set[T] = set()
|
||||
# edges (pairs of shared-graph nodes) hidden from this overlay's views only
|
||||
self._hidden: set[tuple[Any, Any]] = set()
|
||||
self._hidden: set[tuple[Tx[T], Tx[T]]] = set()
|
||||
# view-level edge pairs hidden from the with-successors view only
|
||||
self._hidden_full: set[tuple[Any, Any]] = set()
|
||||
self._hidden_full: set[tuple[Tx[T], Tx[T]]] = set()
|
||||
# view-level edge pairs injected into the with-successors view only (successor absorption)
|
||||
self._extra_full_edges: set[tuple[Any, Any]] = set()
|
||||
self._extra_full_edges: set[tuple[Tx[T], Tx[T]]] = set()
|
||||
# scratch edge marks (e.g., Phoenix's cyclic_refinement_outgoing), scoped to this overlay
|
||||
self.edge_marks: defaultdict[str, set[tuple[Any, Any]]] = defaultdict(set)
|
||||
self.edge_marks: defaultdict[str, set[tuple[Tx[T], Tx[T]]]] = defaultdict(set)
|
||||
# the node this overlay was finalized into, if any
|
||||
self.replacement = None
|
||||
|
||||
|
|
@ -267,6 +280,8 @@ class RegionOverlay:
|
|||
|
||||
@property
|
||||
def addr(self):
|
||||
if self.head is None:
|
||||
raise TypeError("Region with no head has no address")
|
||||
return self.head.addr
|
||||
|
||||
#
|
||||
|
|
@ -274,11 +289,11 @@ class RegionOverlay:
|
|||
#
|
||||
|
||||
@property
|
||||
def manager(self) -> OverlayManager:
|
||||
def manager(self) -> OverlayManager[T]:
|
||||
return self._mgr
|
||||
|
||||
@property
|
||||
def members(self) -> set:
|
||||
def members(self) -> set[Tx[T]]:
|
||||
return self._members
|
||||
|
||||
def ancestors(self) -> set[RegionOverlay]:
|
||||
|
|
@ -290,15 +305,17 @@ class RegionOverlay:
|
|||
node = node.parent
|
||||
return result
|
||||
|
||||
def underlying_nodes(self) -> set:
|
||||
def underlying_nodes(self) -> set[T]:
|
||||
"""All shared-graph nodes inside this region (including nodes of nested regions)."""
|
||||
return self._under
|
||||
|
||||
@staticmethod
|
||||
def _underlying(x) -> set:
|
||||
def _underlying(x) -> set[T]:
|
||||
return x._under if isinstance(x, RegionOverlay) else {x}
|
||||
|
||||
def create_subregion(self, head, members: Iterable, cyclic: bool, cyclic_ancestor: bool = False) -> RegionOverlay:
|
||||
def create_subregion(
|
||||
self, head: Tx[T], members: Iterable, cyclic: bool, cyclic_ancestor: bool = False
|
||||
) -> RegionOverlay[T]:
|
||||
"""
|
||||
Carve a new child overlay out of this overlay. ``members`` must be a subset of this overlay's members
|
||||
(shared-graph nodes owned by this overlay and/or existing child overlays); ``head`` must be one of them.
|
||||
|
|
@ -376,10 +393,10 @@ class RegionOverlay:
|
|||
# Derived views
|
||||
#
|
||||
|
||||
def _is_hidden(self, src, dst) -> bool:
|
||||
def _is_hidden(self, src: Tx[T], dst: Tx[T]) -> bool:
|
||||
return (src, dst) in self._hidden
|
||||
|
||||
def _hidden_context_head_under(self) -> frozenset | set:
|
||||
def _hidden_context_head_under(self) -> set[Tx[T]]:
|
||||
"""
|
||||
Crossing edges that target the head of the region's processing context (the nearest cyclic ancestor, or
|
||||
the root region) were invisible during region identification: in-edges of the head are stripped before
|
||||
|
|
@ -397,7 +414,7 @@ class RegionOverlay:
|
|||
return frozenset()
|
||||
return self._underlying(anc.head)
|
||||
|
||||
def _crossing_out_edges(self) -> Iterator[tuple[Any, Any, dict]]:
|
||||
def _crossing_out_edges(self) -> Iterator[tuple[Tx[T], Tx[T], dict[str, Any]]]:
|
||||
"""All shared-graph edges leaving this region, except hidden ones."""
|
||||
graph = self._mgr.graph
|
||||
under = self._under
|
||||
|
|
@ -413,7 +430,7 @@ class RegionOverlay:
|
|||
def _in_loop(self) -> bool:
|
||||
return self.cyclic or self.cyclic_ancestor
|
||||
|
||||
def successor_nodes(self) -> set:
|
||||
def successor_nodes(self) -> set[Tx[T]]:
|
||||
"""
|
||||
The derived successor set of this region: representatives of all shared-graph nodes targeted by edges
|
||||
leaving the region.
|
||||
|
|
@ -429,14 +446,14 @@ class RegionOverlay:
|
|||
self._cache_succs = (self._mgr.version, succs)
|
||||
return succs
|
||||
|
||||
def _quotient_edges(self, with_successors: bool) -> Iterator[tuple[Any, Any, dict]]:
|
||||
def _quotient_edges(self, with_successors: bool) -> Iterator[tuple[Tx[T], Tx[T], dict[str, Any]]]:
|
||||
"""
|
||||
Derive the edges of the region view (member -> member, and if requested member -> successor and
|
||||
successor -> successor) from the shared graph.
|
||||
"""
|
||||
graph = self._mgr.graph
|
||||
under = self._under
|
||||
member_of: dict[Any, Any] = {}
|
||||
member_of: dict[Tx[T], Tx[T]] = {}
|
||||
for m in self._members:
|
||||
for n in self._underlying(m):
|
||||
member_of[n] = m
|
||||
|
|
@ -483,7 +500,7 @@ class RegionOverlay:
|
|||
|
||||
def view_graph(
|
||||
self, full: bool = False, include_marked: bool = False, blacklisted_edges: frozenset = frozenset()
|
||||
) -> RegionOverlayGraph:
|
||||
) -> RegionOverlayGraph[T]:
|
||||
"""A zero-copy, networkx-compatible view of this region (see RegionOverlayGraph)."""
|
||||
if blacklisted_edges:
|
||||
return RegionOverlayGraph(
|
||||
|
|
@ -501,7 +518,7 @@ class RegionOverlay:
|
|||
# node so that RegionOverlayGraph can answer adjacency queries without materializing anything.
|
||||
#
|
||||
|
||||
def _iter_view_out_edges(self, n, full: bool) -> Iterator[tuple[Any, dict]]:
|
||||
def _iter_view_out_edges(self, n, full: bool) -> Iterator[tuple[Tx[T], dict[str, Any]]]:
|
||||
"""Visible out-edges of view node ``n`` (a member, or a successor in the full view), deduplicated."""
|
||||
graph = self._mgr.graph
|
||||
under = self._under
|
||||
|
|
@ -522,7 +539,7 @@ class RegionOverlay:
|
|||
if rep_v not in seen:
|
||||
seen.add(rep_v)
|
||||
yield rep_v, data
|
||||
elif full and v not in hidden_head:
|
||||
elif hidden_head is not None and v not in hidden_head:
|
||||
rep_v = self._representative_outside(v)
|
||||
if rep_v is not None and rep_v not in seen:
|
||||
seen.add(rep_v)
|
||||
|
|
@ -560,7 +577,7 @@ class RegionOverlay:
|
|||
seen.add(v)
|
||||
yield v, {}
|
||||
|
||||
def _iter_view_in_edges(self, n, full: bool) -> Iterator[tuple[Any, dict]]:
|
||||
def _iter_view_in_edges(self, n: Tx[T], full: bool) -> Iterator[tuple[Tx[T], dict[str, Any]]]:
|
||||
"""Visible in-edges of view node ``n``, deduplicated. The transpose of _iter_view_out_edges."""
|
||||
graph = self._mgr.graph
|
||||
under = self._under
|
||||
|
|
@ -621,21 +638,21 @@ class RegionOverlay:
|
|||
seen.add(u)
|
||||
yield u, {}
|
||||
|
||||
def view(self) -> RegionOverlayGraph:
|
||||
def view(self) -> RegionOverlayGraph[T]:
|
||||
"""The region graph (members only): a zero-copy networkx-compatible view; treat it as read-only."""
|
||||
return self.view_graph(full=False)
|
||||
|
||||
def view_with_successors(self) -> RegionOverlayGraph:
|
||||
def view_with_successors(self) -> RegionOverlayGraph[T]:
|
||||
"""The region graph including successor nodes: a zero-copy view; treat it as read-only."""
|
||||
return self.view_graph(full=True)
|
||||
|
||||
@property
|
||||
def raw_graph(self) -> RegionOverlayGraph:
|
||||
def raw_graph(self) -> RegionOverlayGraph[T]:
|
||||
"""The member view including marked edges (the old graph with cyclic_refinement_outgoing attrs present)."""
|
||||
return self.view_graph(full=False, include_marked=True)
|
||||
|
||||
@property
|
||||
def raw_graph_with_successors(self) -> RegionOverlayGraph:
|
||||
def raw_graph_with_successors(self) -> RegionOverlayGraph[T]:
|
||||
"""The with-successors view including marked edges."""
|
||||
return self.view_graph(full=True, include_marked=True)
|
||||
|
||||
|
|
@ -644,15 +661,15 @@ class RegionOverlay:
|
|||
#
|
||||
|
||||
@property
|
||||
def graph(self) -> networkx.DiGraph:
|
||||
def graph(self) -> networkx.DiGraph[Tx[T]]:
|
||||
return self.view()
|
||||
|
||||
@property
|
||||
def graph_with_successors(self) -> networkx.DiGraph:
|
||||
def graph_with_successors(self) -> networkx.DiGraph[Tx[T]]:
|
||||
return self.view_with_successors()
|
||||
|
||||
@property
|
||||
def successors(self) -> set:
|
||||
def successors(self) -> set[Tx[T]]:
|
||||
return self.successor_nodes()
|
||||
|
||||
@property
|
||||
|
|
@ -710,7 +727,7 @@ class RegionOverlay:
|
|||
self._on_node_added(node)
|
||||
self._invalidate()
|
||||
|
||||
def remove_node(self, node, absorbed_into=None, absorb_out_edges: bool = True) -> None:
|
||||
def remove_node(self, node: Tx[T], absorbed_into: Tx[T] | None = None, absorb_out_edges: bool = True) -> None:
|
||||
"""
|
||||
Remove a node. If ``node`` is a member (or a member overlay's node), it is removed from the shared graph
|
||||
for real. If it is a successor of this region, the removal is interpreted as hiding all edges from this
|
||||
|
|
@ -728,7 +745,9 @@ class RegionOverlay:
|
|||
self.hide_edge_to_successor(node)
|
||||
return
|
||||
external_in_edges = []
|
||||
rewire_out_edges = [] # (dst, data, hide): edges to rewire onto absorbed_into in the shared graph
|
||||
rewire_out_edges: list[
|
||||
tuple[Tx[T], dict[str, Any], bool]
|
||||
] = [] # (dst, data, hide): edges to rewire onto absorbed_into in the shared graph
|
||||
if absorbed_into is not None:
|
||||
external_in_edges = [
|
||||
(src, data)
|
||||
|
|
@ -753,13 +772,14 @@ class RegionOverlay:
|
|||
for src, data in external_in_edges:
|
||||
self._mgr.graph_add_edge(src, absorbed_into, **data)
|
||||
hidden_added = []
|
||||
for dst, data, hide in rewire_out_edges:
|
||||
self._mgr.graph_add_edge(absorbed_into, dst, **data)
|
||||
if hide and (absorbed_into, dst) not in self._hidden:
|
||||
self._hidden.add((absorbed_into, dst))
|
||||
hidden_added.append((absorbed_into, dst))
|
||||
if hidden_added:
|
||||
self._mgr._record(lambda: self._hidden.difference_update(hidden_added))
|
||||
if absorbed_into is not None:
|
||||
for dst, data, hide in rewire_out_edges:
|
||||
self._mgr.graph_add_edge(absorbed_into, dst, **data)
|
||||
if hide and (absorbed_into, dst) not in self._hidden:
|
||||
self._hidden.add((absorbed_into, dst))
|
||||
hidden_added.append((absorbed_into, dst))
|
||||
if hidden_added:
|
||||
self._mgr._record(lambda: self._hidden.difference_update(hidden_added))
|
||||
self._invalidate()
|
||||
|
||||
def hide_edge_to_successor(self, succ) -> None:
|
||||
|
|
@ -782,7 +802,7 @@ class RegionOverlay:
|
|||
self._mgr._touch(rep)
|
||||
self._invalidate()
|
||||
|
||||
def underlying_edge_pairs(self, src, dst) -> list[tuple[Any, Any]]:
|
||||
def underlying_edge_pairs(self, src: Tx[T], dst: Tx[T]) -> list[tuple[Tx[T], Tx[T]]]:
|
||||
graph = self._mgr.graph
|
||||
under_src = self._underlying(src)
|
||||
under_dst = self._underlying(dst)
|
||||
|
|
@ -795,7 +815,7 @@ class RegionOverlay:
|
|||
pairs.append((u, v))
|
||||
return pairs
|
||||
|
||||
def add_edge(self, src, dst, **data) -> None:
|
||||
def add_edge(self, src: Tx[T], dst: Tx[T], **data) -> None:
|
||||
"""
|
||||
Add a real edge to the shared graph. Overlay endpoints are resolved to underlying nodes: the destination
|
||||
resolves to its entry (head chain); overlay sources are not supported. Endpoints that are not in the
|
||||
|
|
@ -805,6 +825,7 @@ class RegionOverlay:
|
|||
dst_ = dst
|
||||
while isinstance(dst_, RegionOverlay):
|
||||
dst_ = dst_.head
|
||||
assert dst_ is not None
|
||||
if src not in self._mgr.graph:
|
||||
self.add_node(src)
|
||||
if dst_ not in self._mgr.graph:
|
||||
|
|
@ -815,7 +836,7 @@ class RegionOverlay:
|
|||
self._mgr._record(lambda: self._hidden.add((src, dst_)))
|
||||
self._invalidate()
|
||||
|
||||
def detach_edge(self, src, dst) -> None:
|
||||
def detach_edge(self, src: Tx[T], dst: Tx[T]) -> None:
|
||||
"""
|
||||
Remove an edge from the shared graph for real (e.g., when the edge has been virtualized into a goto).
|
||||
Overlay endpoints remove all underlying edges between the two node sets.
|
||||
|
|
@ -833,7 +854,7 @@ class RegionOverlay:
|
|||
self._mgr._touch(dst)
|
||||
self._invalidate()
|
||||
|
||||
def mark_edge(self, src, dst, **attrs) -> None:
|
||||
def mark_edge(self, src: Tx[T], dst: Tx[T], **attrs) -> None:
|
||||
"""
|
||||
Mark a view-level edge (e.g. cyclic_refinement_outgoing) so RegionOverlayGraph hides it by default.
|
||||
Marks live in overlay state, never reach the shared graph, and are remapped/cleared with the region.
|
||||
|
|
@ -847,13 +868,13 @@ class RegionOverlay:
|
|||
self._mgr._touch(dst)
|
||||
self._invalidate()
|
||||
|
||||
def absorb_successor_into(self, succ, new_node) -> None:
|
||||
def absorb_successor_into(self, succ: Tx[T], new_node: Tx[T]) -> None:
|
||||
"""
|
||||
Absorb a successor node into a structured member node in this region's with-successors view only (the
|
||||
successor still belongs to an enclosing region): the successor's view out-edges are re-attached to the
|
||||
member node as view-only extra edges, then the successor disappears from this region's views.
|
||||
"""
|
||||
added = []
|
||||
added: list[tuple[Tx[T], Tx[T]]] = []
|
||||
for dst, _ in self.view_with_successors().overlay._iter_view_out_edges(succ, full=True):
|
||||
if dst is not new_node and (new_node, dst) not in self._extra_full_edges:
|
||||
self._extra_full_edges.add((new_node, dst))
|
||||
|
|
@ -865,18 +886,18 @@ class RegionOverlay:
|
|||
self._mgr._touch(dst)
|
||||
self.hide_edge_to_successor(succ)
|
||||
|
||||
def drop_edge_marks_from(self, node, key) -> None:
|
||||
def drop_edge_marks_from(self, node: Tx[T], key: str) -> None:
|
||||
"""Clear marks on all out-edges of a node (the new_node after a replace), undoably."""
|
||||
removed = [(u, v) for (u, v) in self.edge_marks[key] if u is node]
|
||||
if removed:
|
||||
self.edge_marks[key].difference_update(removed)
|
||||
self._mgr._record(lambda: self.edge_marks.update(removed))
|
||||
self._mgr._record(lambda: self.edge_marks[key].update(removed))
|
||||
self._mgr._touch(node)
|
||||
for _, v in removed:
|
||||
self._mgr._touch(v)
|
||||
self._invalidate()
|
||||
|
||||
def remove_edge_with_successors_only(self, src, dst) -> None:
|
||||
def remove_edge_with_successors_only(self, src: Tx[T], dst: Tx[T]) -> None:
|
||||
"""
|
||||
Hide an edge from the with-successors view only, leaving the member view and the shared graph alone (a
|
||||
rare asymmetric bookkeeping pattern in Phoenix's switch-case structuring).
|
||||
|
|
@ -888,11 +909,11 @@ class RegionOverlay:
|
|||
self._mgr._touch(dst)
|
||||
self._invalidate()
|
||||
|
||||
def hide_edge(self, src, dst) -> None:
|
||||
def hide_edge(self, src: Tx[T], dst: Tx[T]) -> None:
|
||||
"""
|
||||
Remove an edge from this overlay's views only. Enclosing regions still see the underlying edge(s).
|
||||
"""
|
||||
added = []
|
||||
added: list[tuple[Tx[T], Tx[T]]] = []
|
||||
for u, v in self.underlying_edge_pairs(src, dst):
|
||||
if (u, v) not in self._hidden:
|
||||
self._hidden.add((u, v))
|
||||
|
|
@ -903,7 +924,9 @@ class RegionOverlay:
|
|||
self._mgr._touch(dst)
|
||||
self._invalidate()
|
||||
|
||||
def replace_nodes(self, old_node_0, new_node, old_node_1=None, self_loop: bool = True) -> None:
|
||||
def replace_nodes(
|
||||
self, old_node_0: Tx[T], new_node: Tx[T], old_node_1: Tx[T] | None = None, self_loop: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Replace one or two member nodes with a new node, preserving and rewiring all underlying edges (including
|
||||
edges from/to nodes outside this region, which is how results become visible to enclosing regions).
|
||||
|
|
@ -961,12 +984,12 @@ class RegionOverlay:
|
|||
self._mgr._record(lambda: setattr(self, "head", old_head))
|
||||
self._invalidate()
|
||||
|
||||
def _remap_bookkeeping(self, old_nodes: set, new_node) -> None:
|
||||
def _remap_bookkeeping(self, old_nodes: set[Tx[T]], new_node: Tx[T]) -> None:
|
||||
"""Remap hidden edges and edge marks that reference replaced nodes, here and in all enclosing overlays."""
|
||||
anc: RegionOverlay | None = self
|
||||
anc = self
|
||||
while anc is not None:
|
||||
for attr in ("_hidden", "_hidden_full", "_extra_full_edges"):
|
||||
pairs: set[tuple[Any, Any]] = getattr(anc, attr)
|
||||
pairs: set[tuple[Tx[T], Tx[T]]] = getattr(anc, attr)
|
||||
stale = [(u, v) for u, v in pairs if u in old_nodes or v in old_nodes]
|
||||
if stale:
|
||||
remapped = [
|
||||
|
|
@ -1002,7 +1025,7 @@ class RegionOverlay:
|
|||
# Region lifecycle
|
||||
#
|
||||
|
||||
def snapshot_successors(self) -> set:
|
||||
def snapshot_successors(self) -> set[Tx[T]]:
|
||||
"""
|
||||
Capture this region's structural successors and how many member edges reach each, taken before the region
|
||||
is structured. finalize() uses it to re-establish the region-to-successor edges that structuring removes
|
||||
|
|
@ -1011,12 +1034,12 @@ class RegionOverlay:
|
|||
return set(self.successor_nodes())
|
||||
|
||||
@staticmethod
|
||||
def _resolve_entry(node):
|
||||
def _resolve_entry(node) -> T | None:
|
||||
while isinstance(node, RegionOverlay):
|
||||
node = node.head
|
||||
return node
|
||||
|
||||
def finalize(self, result_node=None, succ_snapshot=None):
|
||||
def finalize(self, result_node: Tx[T] | None = None, succ_snapshot: set[Tx[T]] | None = None):
|
||||
"""
|
||||
Collapse this fully-structured region into its parent: the region must consist of a single member node
|
||||
(the structuring result), which takes the region's place among the parent's members. Returns that node.
|
||||
|
|
@ -1053,6 +1076,7 @@ class RegionOverlay:
|
|||
s_entry = self._resolve_entry(s)
|
||||
if (
|
||||
s_entry is not result_node
|
||||
and s_entry is not None
|
||||
and s_entry is not parent_loop_head
|
||||
and s_entry in graph
|
||||
and not graph.has_edge(result_node, s_entry)
|
||||
|
|
@ -1097,7 +1121,8 @@ class RegionOverlay:
|
|||
self._invalidate()
|
||||
return result_node
|
||||
|
||||
def collapse_to(self, result_node):
|
||||
# NOTE: this function seems unused
|
||||
def collapse_to(self, result_node: RegionOverlay[T]):
|
||||
"""
|
||||
Collapse this region into its parent by replacing all of its member nodes with a single external result
|
||||
node (the structuring result). Used by structurers that compute their result without destructively
|
||||
|
|
@ -1116,8 +1141,8 @@ class RegionOverlay:
|
|||
underset = self._under
|
||||
|
||||
# capture crossing edges (one endpoint inside the region, the other outside) before removing the members
|
||||
in_edges: list[tuple[Any, dict]] = []
|
||||
out_edges: list[tuple[Any, dict]] = []
|
||||
in_edges: list[tuple[Tx[T], dict[str, Any]]] = []
|
||||
out_edges: list[tuple[Tx[T], dict[str, Any]]] = []
|
||||
seen_in: set = set()
|
||||
seen_out: set = set()
|
||||
for u in under:
|
||||
|
|
@ -1231,12 +1256,12 @@ class RegionOverlay:
|
|||
_PARANOID_ADJ_CHECK = bool(os.environ.get("ANGR_PARANOID_ADJ"))
|
||||
|
||||
|
||||
class _OverlayNodeAtlas(Mapping):
|
||||
class _OverlayNodeAtlas[T: RegionBound](Mapping[Tx[T], dict[str, Any]]):
|
||||
"""Lazy node mapping of a RegionOverlayGraph: the overlay's view nodes, attributes from the shared graph."""
|
||||
|
||||
__slots__ = ("_rog",)
|
||||
|
||||
def __init__(self, rog: RegionOverlayGraph):
|
||||
def __init__(self, rog: RegionOverlayGraph[T]):
|
||||
self._rog = rog
|
||||
|
||||
def __len__(self):
|
||||
|
|
@ -1258,12 +1283,12 @@ class _OverlayNodeAtlas(Mapping):
|
|||
return shared.nodes[n] if n in shared else {}
|
||||
|
||||
|
||||
class _OverlayAdjInner(Mapping):
|
||||
class _OverlayAdjInner[T: RegionBound](Mapping[Tx[T], dict[str, Any]]):
|
||||
"""Adjacency of one view node: target -> edge data, derived on construction from the overlay."""
|
||||
|
||||
__slots__ = ("_d",)
|
||||
|
||||
def __init__(self, rog: RegionOverlayGraph, n, pred: bool):
|
||||
def __init__(self, rog: RegionOverlayGraph[T], n, pred: bool):
|
||||
overlay = rog.overlay
|
||||
it = overlay._iter_view_in_edges(n, rog.full) if pred else overlay._iter_view_out_edges(n, rog.full)
|
||||
if pred:
|
||||
|
|
@ -1284,26 +1309,26 @@ class _OverlayAdjInner(Mapping):
|
|||
return self._d[n]
|
||||
|
||||
|
||||
class _OverlayAdjAtlas(Mapping):
|
||||
class _OverlayAdjAtlas[T: RegionBound](Mapping[Tx[T], _OverlayAdjInner[T]]):
|
||||
"""Outer adjacency mapping of a RegionOverlayGraph: view node -> _OverlayAdjInner."""
|
||||
|
||||
__slots__ = ("_cache", "_epoch", "_pred", "_rog", "_succ_cache", "_succ_version")
|
||||
|
||||
def __init__(self, rog: RegionOverlayGraph, pred: bool):
|
||||
def __init__(self, rog: RegionOverlayGraph[T], pred: bool):
|
||||
self._rog = rog
|
||||
self._pred = pred
|
||||
# Phoenix queries the same node's adjacency repeatedly (.successors/.predecessors/.in_degree/.out_degree/
|
||||
# .has_edge all route through here); cache the derived _OverlayAdjInner per node, keyed by that node's
|
||||
# topology version so an unrelated mutation does not evict it. _epoch tracks the manager's coarse epoch
|
||||
# (bumped by lifecycle ops / rollback) and forces a full clear when it changes.
|
||||
self._cache: dict[Any, tuple[int, _OverlayAdjInner]] = {}
|
||||
self._cache: dict[Tx[T], tuple[int, _OverlayAdjInner]] = {}
|
||||
self._epoch: int | None = None
|
||||
# successor-node adjacency cache: a successor's view adjacency depends on the whole region's successor
|
||||
# set and view state, not just on that node, so it cannot be keyed by the node's own version. Key the
|
||||
# whole cache by the manager's global version instead (bumped by every mutation and view-state change,
|
||||
# the same invariant _node_set relies on): reads between two mutations -- dominator fixpoints, SAILR's
|
||||
# per-edge in_degree queries -- hit the cache, and any mutation drops it wholesale.
|
||||
self._succ_cache: dict[Any, _OverlayAdjInner] = {}
|
||||
self._succ_cache: dict[Tx[T], _OverlayAdjInner] = {}
|
||||
self._succ_version: int | None = None
|
||||
|
||||
def __len__(self):
|
||||
|
|
@ -1363,7 +1388,7 @@ class _OverlayAdjAtlas(Mapping):
|
|||
return entry[1]
|
||||
|
||||
|
||||
class RegionOverlayGraph[T](networkx.DiGraph):
|
||||
class RegionOverlayGraph[T: RegionBound](networkx.DiGraph[Tx[T]] if TYPE_CHECKING else networkx.DiGraph):
|
||||
"""
|
||||
A read-only, networkx-compatible view of a RegionOverlay that stores no copy of the region's subgraph: all
|
||||
queries traverse the original shared graph through the overlay's membership. Compatible with every networkx
|
||||
|
|
@ -1378,10 +1403,10 @@ class RegionOverlayGraph[T](networkx.DiGraph):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
overlay: RegionOverlay,
|
||||
overlay: RegionOverlay[T],
|
||||
full: bool = False,
|
||||
include_marked: bool = False,
|
||||
blacklisted_edges: frozenset[tuple[Any, Any]] = frozenset(),
|
||||
blacklisted_edges: frozenset[tuple[Tx[T], Tx[T]]] = frozenset(),
|
||||
):
|
||||
super().__init__()
|
||||
self.overlay = overlay
|
||||
|
|
@ -1399,7 +1424,7 @@ class RegionOverlayGraph[T](networkx.DiGraph):
|
|||
# internals
|
||||
#
|
||||
|
||||
def _node_set(self) -> frozenset:
|
||||
def _node_set(self) -> frozenset[Tx[T]]:
|
||||
version = self.overlay.manager.version
|
||||
cached = self._ns_cache
|
||||
if cached is not None and cached[0] == version:
|
||||
|
|
@ -1414,14 +1439,14 @@ class RegionOverlayGraph[T](networkx.DiGraph):
|
|||
self._ns_cache = (version, ns)
|
||||
return ns
|
||||
|
||||
def _pair_visible(self, src, dst) -> bool:
|
||||
def _pair_visible(self, src: Tx[T], dst: Tx[T]) -> bool:
|
||||
if not self.include_marked and any((src, dst) in marks for marks in self.overlay.edge_marks.values()):
|
||||
return False
|
||||
if (src, dst) in self.blacklisted_edges:
|
||||
return False
|
||||
return not (self.full and (src, dst) in self.overlay._hidden_full)
|
||||
|
||||
def _variant(self, fullgraph, all_edges) -> RegionOverlayGraph:
|
||||
def _variant(self, fullgraph: bool | None, all_edges: bool | None = None) -> RegionOverlayGraph[T]:
|
||||
full = self.full if fullgraph is None else fullgraph
|
||||
include_marked = self.include_marked if all_edges is None else all_edges
|
||||
if full == self.full and include_marked == self.include_marked:
|
||||
|
|
@ -1435,24 +1460,24 @@ class RegionOverlayGraph[T](networkx.DiGraph):
|
|||
#
|
||||
|
||||
@property
|
||||
def full_view(self) -> RegionOverlayGraph:
|
||||
def full_view(self) -> RegionOverlayGraph[T]:
|
||||
"""The with-successors sibling of this view (zero-copy)."""
|
||||
return self._variant(True, None)
|
||||
|
||||
@property
|
||||
def member_view(self) -> RegionOverlayGraph:
|
||||
def member_view(self) -> RegionOverlayGraph[T]:
|
||||
"""The members-only sibling of this view (zero-copy)."""
|
||||
return self._variant(False, None)
|
||||
|
||||
def with_all_edges(self) -> RegionOverlayGraph:
|
||||
def with_all_edges(self) -> RegionOverlayGraph[T]:
|
||||
"""A sibling view that includes edges marked through RegionOverlay.mark_edge."""
|
||||
return self._variant(None, True)
|
||||
|
||||
def filtered(self) -> RegionOverlayGraph:
|
||||
def filtered(self) -> RegionOverlayGraph[T]:
|
||||
"""A sibling view that hides edges marked through RegionOverlay.mark_edge (the default)."""
|
||||
return self._variant(None, False)
|
||||
|
||||
def to_acyclic_by_order(self, node_order) -> RegionOverlayGraph:
|
||||
def to_acyclic_by_order(self, node_order: Mapping[Tx[T], int]) -> RegionOverlayGraph[T]:
|
||||
"""
|
||||
An acyclic view of this graph, obtained by blacklisting back edges (edges whose source is ordered at or
|
||||
after their destination in ``node_order``). Replaces utils.graph.to_acyclic_graph without a copy.
|
||||
|
|
@ -1460,20 +1485,19 @@ class RegionOverlayGraph[T](networkx.DiGraph):
|
|||
back_edges = [(u, v) for u, v in self.edges if node_order[u] >= node_order[v]]
|
||||
return self.to_acyclic(back_edges)
|
||||
|
||||
def to_acyclic(self, blacklisted_edges) -> RegionOverlayGraph:
|
||||
def to_acyclic(self, blacklisted_edges: Sequence[tuple[Tx[T], Tx[T]]]) -> RegionOverlayGraph[T]:
|
||||
"""
|
||||
A new view with the given (view-level) edges additionally blacklisted; used to traverse the region as an
|
||||
acyclic graph without copying it.
|
||||
"""
|
||||
extra = frozenset((u, v) for u, v in blacklisted_edges)
|
||||
return RegionOverlayGraph(
|
||||
self.overlay,
|
||||
full=self.full,
|
||||
include_marked=self.include_marked,
|
||||
blacklisted_edges=self.blacklisted_edges | extra,
|
||||
blacklisted_edges=self.blacklisted_edges.union(blacklisted_edges),
|
||||
)
|
||||
|
||||
def reverse_view(self) -> RegionOverlayGraph:
|
||||
def reverse_view(self) -> RegionOverlayGraph[T]:
|
||||
"""The reversed view of this graph (zero-copy)."""
|
||||
g = RegionOverlayGraph(
|
||||
self.overlay,
|
||||
|
|
@ -1481,13 +1505,14 @@ class RegionOverlayGraph[T](networkx.DiGraph):
|
|||
include_marked=self.include_marked,
|
||||
blacklisted_edges=frozenset({(v, u) for u, v in self.blacklisted_edges}),
|
||||
)
|
||||
g._succ, g._pred = g._pred, g._succ # swap the adjacency mappings to reverse the graph
|
||||
# swap the adjacency mappings to reverse the graph
|
||||
g._succ, g._pred = g._pred, g._succ # type: ignore
|
||||
# g._adj is synced with _succ
|
||||
return g
|
||||
|
||||
def materialize(self) -> networkx.DiGraph:
|
||||
def materialize(self) -> networkx.DiGraph[Tx[T]]:
|
||||
"""An independent networkx.DiGraph copy of this view."""
|
||||
g: networkx.DiGraph = networkx.DiGraph()
|
||||
g: networkx.DiGraph[Tx[T]] = networkx.DiGraph()
|
||||
g.add_nodes_from(self._node_set())
|
||||
for u in self._node_set():
|
||||
for v, data in self._adj[u].items():
|
||||
|
|
@ -1499,24 +1524,24 @@ class RegionOverlayGraph[T](networkx.DiGraph):
|
|||
# use .full_view / .member_view for those)
|
||||
#
|
||||
|
||||
def edge_marked(self, u, v, mark_name: str | None = None) -> bool:
|
||||
def edge_marked(self, u: Tx[T], v: Tx[T], mark_name: str | None = None) -> bool:
|
||||
if mark_name is not None:
|
||||
return (u, v) in self.overlay.edge_marks.get(mark_name, set())
|
||||
return any((u, v) in marks for marks in self.overlay.edge_marks.values())
|
||||
|
||||
def successors(self, n, fullgraph: bool | None = None, all_edges: bool | None = None):
|
||||
def successors(self, n: Tx[T], fullgraph: bool | None = None, all_edges: bool | None = None):
|
||||
g = self._variant(fullgraph, all_edges)
|
||||
if g is self:
|
||||
return super().successors(n)
|
||||
return g.successors(n)
|
||||
|
||||
def predecessors(self, n, fullgraph: bool | None = None, all_edges: bool | None = None):
|
||||
def predecessors(self, n: Tx[T], fullgraph: bool | None = None, all_edges: bool | None = None):
|
||||
g = self._variant(fullgraph, all_edges)
|
||||
if g is self:
|
||||
return super().predecessors(n)
|
||||
return g.predecessors(n)
|
||||
|
||||
def has_edge(self, u, v, fullgraph: bool | None = None, all_edges: bool | None = None) -> bool:
|
||||
def has_edge(self, u: Tx[T], v: Tx[T], fullgraph: bool | None = None, all_edges: bool | None = None) -> bool:
|
||||
g = self._variant(fullgraph, all_edges)
|
||||
if g is self:
|
||||
return super().has_edge(u, v)
|
||||
|
|
@ -1526,17 +1551,20 @@ class RegionOverlayGraph[T](networkx.DiGraph):
|
|||
# overrides for inherited methods that would construct self.__class__() without arguments
|
||||
#
|
||||
|
||||
def copy(self, as_view: bool = False) -> networkx.DiGraph:
|
||||
# NOTE: this does not respect the intended semantics of as_view
|
||||
def copy(self, as_view: bool = False) -> networkx.DiGraph[Tx[T]]:
|
||||
return self.materialize()
|
||||
|
||||
def subgraph(self, nodes) -> networkx.DiGraph:
|
||||
def subgraph(self, nodes) -> networkx.DiGraph[Tx[T]]:
|
||||
# Build only the induced subgraph (cost O(induced subgraph)) instead of materialize()+restrict (cost O(whole
|
||||
# region graph)): callers (e.g. quasi_topological_sort_nodes' SCC handling) want a small induced subgraph and
|
||||
# then mutate it. Returns an independent, mutable networkx.DiGraph with exactly the nodes/edges/data (and the
|
||||
# same node/edge iteration order) that self.materialize().subgraph(nodes) would have produced.
|
||||
if not isinstance(nodes, Iterable):
|
||||
raise TypeError("Please use the non-pathological versions of the NetworkX api")
|
||||
node_set = self._node_set()
|
||||
selset = {n for n in nodes if n in node_set}
|
||||
g: networkx.DiGraph = networkx.DiGraph()
|
||||
g: networkx.DiGraph[Tx[T]] = networkx.DiGraph()
|
||||
g.add_nodes_from(n for n in node_set if n in selset)
|
||||
for u in g:
|
||||
for v, data in self._adj[u].items():
|
||||
|
|
@ -1544,5 +1572,6 @@ class RegionOverlayGraph[T](networkx.DiGraph):
|
|||
g.add_edge(u, v, **data)
|
||||
return g
|
||||
|
||||
def to_directed(self, as_view: bool = False) -> networkx.DiGraph:
|
||||
# NOTE: this does not respect the intended semantics of as_view
|
||||
def to_directed(self, as_view: bool = False) -> networkx.DiGraph[Tx[T]]:
|
||||
return self.materialize()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# pylint:disable=unused-argument,arguments-differ
|
||||
from __future__ import annotations
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.analyses.decompiler.sequence_walker import SequenceWalker
|
||||
from angr.analyses.decompiler.structurer_nodes import (
|
||||
CascadingConditionNode,
|
||||
|
|
|
|||
|
|
@ -174,7 +174,6 @@ class LoopNodeFinder(SequenceWalker):
|
|||
def _handle_Loop(self, node: LoopNode, **kwargs):
|
||||
super()._handle_Loop(node, **kwargs)
|
||||
self.loop_nodes.append(node)
|
||||
return None
|
||||
|
||||
|
||||
class MultiStatementExpressionAssignmentFinder(AILBlockRewriter):
|
||||
|
|
@ -387,8 +386,6 @@ class ExpressionCounter(SequenceWalker):
|
|||
super()._handle_Loop(node, **kwargs)
|
||||
self._outer_scope = outer_scope
|
||||
|
||||
return None
|
||||
|
||||
def _handle_SwitchCase(self, node: SwitchCaseNode, **kwargs):
|
||||
self._collect_uses(node.switch_expr, ConditionLocation(node.addr))
|
||||
return super()._handle_SwitchCase(node, **kwargs)
|
||||
|
|
@ -602,11 +599,12 @@ class ExpressionReplacer(AILBlockRewriter):
|
|||
else:
|
||||
new_statements.append(stmt_)
|
||||
|
||||
new_expr = self._handle_expr(0, expr.expr, stmt_idx, stmt, block)
|
||||
if new_expr is not None and new_expr is not expr.expr:
|
||||
inner_in = expr.expr
|
||||
new_expr = self._handle_expr(0, inner_in, stmt_idx, stmt, block)
|
||||
if new_expr is not None and new_expr != inner_in:
|
||||
changed = True
|
||||
else:
|
||||
new_expr = expr.expr
|
||||
new_expr = inner_in
|
||||
|
||||
if changed:
|
||||
if not new_statements:
|
||||
|
|
@ -624,23 +622,26 @@ class ExpressionReplacer(AILBlockRewriter):
|
|||
if is_phi_assignment(stmt):
|
||||
return stmt
|
||||
|
||||
if isinstance(stmt.dst, VirtualVariable) and stmt.dst.varid in self._assignments:
|
||||
dst_in = stmt.dst
|
||||
src_in = stmt.src
|
||||
|
||||
if isinstance(dst_in, VirtualVariable) and dst_in.varid in self._assignments:
|
||||
return stmt
|
||||
|
||||
changed = False
|
||||
|
||||
dst = self._handle_expr(0, stmt.dst, stmt_idx, stmt, block)
|
||||
if dst is not stmt.dst and not isinstance(dst, (Call, ITE)):
|
||||
dst = self._handle_expr(0, dst_in, stmt_idx, stmt, block)
|
||||
if dst != dst_in and not isinstance(dst, (Call, ITE)):
|
||||
changed = True
|
||||
else:
|
||||
dst = stmt.dst
|
||||
dst = dst_in
|
||||
assert isinstance(dst, Atom)
|
||||
|
||||
src = self._handle_expr(1, stmt.src, stmt_idx, stmt, block)
|
||||
if src is not stmt.src:
|
||||
src = self._handle_expr(1, src_in, stmt_idx, stmt, block)
|
||||
if src != src_in:
|
||||
changed = True
|
||||
else:
|
||||
src = stmt.src
|
||||
src = src_in
|
||||
|
||||
if changed:
|
||||
return Assignment(stmt.idx, dst, src, **stmt.tags)
|
||||
|
|
@ -744,7 +745,6 @@ class ExpressionFolder(SequenceWalker):
|
|||
node.condition = r
|
||||
|
||||
# again, do not replace into the loop body
|
||||
return None
|
||||
|
||||
def _handle_SwitchCase(self, node: SwitchCaseNode, **kwargs):
|
||||
replacer = ExpressionReplacer(self._assignments, self._uses, self._variable_map)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
import angr.ailment as ailment
|
||||
from angr import ailment
|
||||
from angr.analyses.decompiler.goto_manager import Goto
|
||||
from angr.analyses.decompiler.sequence_walker import SequenceWalker
|
||||
from angr.analyses.decompiler.structurer_nodes import (
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue