├── .github ├── FUNDING.yml ├── check.sh ├── dependabot.yml └── workflows │ └── check-and-lint.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Changelog.md ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── README.tpl ├── clippy.toml ├── sample ├── Cargo.lock ├── Cargo.toml └── src │ ├── lib.rs │ └── main.rs ├── sample_cdylib ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs ├── sample_merged ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs ├── sample_rlib ├── Cargo.lock ├── Cargo.toml └── lib.rs └── src ├── asm.rs ├── asm └── statements.rs ├── cached_lines.rs ├── demangle.rs ├── disasm.rs ├── lib.rs ├── llvm.rs ├── main.rs ├── mca.rs ├── mir.rs └── opts.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [pacak] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -e 3 | 4 | ./branch-asm "$@" 5 | diff -u <( ./master-asm "$@" 2> /dev/null ) --label master <( ./branch-asm "$@" 2> /dev/null ) --label branch 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | rebase-strategy: "disabled" 11 | -------------------------------------------------------------------------------- /.github/workflows/check-and-lint.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | strategy: 9 | matrix: 10 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 11 | name: Tests on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Configure cache 19 | uses: actions/cache@v4 20 | with: 21 | path: | 22 | ~/.cargo/registry 23 | ~/.cargo/git 24 | target 25 | key: ${{ matrix.os }}-cargo-${{ hashFiles('./Cargo.lock') }} 26 | 27 | - name: Install rust toolchain 28 | uses: dtolnay/rust-toolchain@stable 29 | with: 30 | toolchain: stable 31 | components: rustfmt clippy rust-src 32 | targets: armv7-unknown-linux-gnueabihf 33 | 34 | - name: Check formatting 35 | run: cargo fmt --all --check 36 | 37 | - name: Clippy 38 | run: cargo clippy -- -D clippy::disallowed_macros 39 | 40 | - name: Build all the things 41 | run: cargo build --all-features --all-targets 42 | 43 | - name: Run unit tests 44 | run: cargo test --all-features 45 | 46 | - name: copy branch executable (non-windows) 47 | shell: bash 48 | if: ${{ matrix.os != 'windows-latest' }} 49 | run: mv ./target/debug/cargo-asm ./branch-asm 50 | 51 | - name: copy branch executable (windows) 52 | shell: bash 53 | if: ${{ matrix.os == 'windows-latest' }} 54 | run: mv ./target/debug/cargo-asm.exe ./branch-asm.exe 55 | 56 | - name: Checkout master 57 | uses: actions/checkout@v4 58 | if: ${{ github.ref != 'refs/heads/master' }} 59 | with: 60 | clean: false 61 | ref: "master" 62 | 63 | - name: Compile master 64 | if: ${{ github.ref != 'refs/heads/master' }} 65 | run: cargo build --all-features --bin cargo-asm 66 | 67 | - name: copy master executable (non windows) 68 | shell: bash 69 | if: ${{ github.ref != 'refs/heads/master' && matrix.os != 'windows-latest' }} 70 | run: mv ./target/debug/cargo-asm ./master-asm 71 | 72 | - name: copy master executable (windows) 73 | shell: bash 74 | if: ${{ github.ref != 'refs/heads/master' && matrix.os == 'windows-latest' }} 75 | run: mv ./target/debug/cargo-asm.exe ./master-asm.exe 76 | 77 | 78 | - name: Upload binaries (non windows) 79 | if: ${{ matrix.os != 'windows-latest' && github.ref != 'refs/heads/master' }} 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: binaries-${{ matrix.os }} 83 | path: | 84 | master-asm 85 | branch-asm 86 | 87 | - name: Upload binaries (windows) 88 | if: ${{ matrix.os == 'windows-latest' && github.ref != 'refs/heads/master' }} 89 | uses: actions/upload-artifact@v4 90 | with: 91 | name: binaries-${{ matrix.os }} 92 | path: | 93 | master-asm.exe 94 | branch-asm.exe 95 | 96 | 97 | compare: 98 | needs: build 99 | if: ${{ github.ref != 'refs/heads/master' }} 100 | strategy: 101 | matrix: 102 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 103 | 104 | fail-fast: false # failure doesn't mean results are bad, it means they are different 105 | # and I still want to see the difference for all the OSes 106 | name: ${{ matrix.os }} difference vs master 107 | runs-on: ${{ matrix.os }} 108 | 109 | steps: 110 | - name: Checkout repo 111 | uses: actions/checkout@v4 112 | 113 | - name: Install rust toolchain 114 | uses: dtolnay/rust-toolchain@stable 115 | with: 116 | toolchain: stable 117 | components: rustfmt clippy rust-src 118 | targets: armv7-unknown-linux-gnueabihf 119 | 120 | - name: Download prebuilt binaries 121 | uses: actions/download-artifact@v4 122 | with: 123 | name: binaries-${{ matrix.os }} 124 | 125 | - name: set executable bit 126 | shell: bash 127 | if: ${{ matrix.os != 'windows-latest' }} 128 | run: | 129 | chmod +x ./branch-asm 130 | chmod +x ./master-asm 131 | 132 | - name: disassembler on a binary 133 | shell: bash 134 | if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} # linux only 135 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --bin sample sample::main 0 --disasm 136 | 137 | - name: disassembler on a library 138 | shell: bash 139 | if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} # linux only 140 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib sample::main 0 --disasm 141 | 142 | - name: run disassembler directly on a file 143 | shell: bash 144 | if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} # linux only 145 | run: ./.github/check.sh --file sample/target/release/sample sample::main 0 146 | 147 | - name: Native version of cargo-show-asm (Intel ASM) 148 | shell: bash 149 | if: ${{ !cancelled() }} 150 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib --intel sample::main --rust 151 | 152 | - name: Native version of cargo-show-asm (Intel ASM) + native CPU 153 | shell: bash 154 | if: ${{ !cancelled() }} 155 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib --native --intel sample::main --rust 156 | 157 | - name: Native version of cargo-show-asm (Intel ASM) + native CPU + no default features 158 | shell: bash 159 | if: ${{ !cancelled() }} 160 | run: ./.github/check.sh -vvv --manifest-path sample/Cargo.toml --lib --native --intel sample::main --rust --no-default-features 161 | 162 | - name: Native version of cargo-show-asm (Intel ASM) + atom 163 | shell: bash 164 | if: ${{ !cancelled() }} 165 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib --target-cpu atom --intel sample::main --rust 166 | 167 | - name: Native version of cargo-show-asm with symbol mangling (Intel ASM) 168 | shell: bash 169 | if: ${{ !cancelled() }} 170 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib --intel sample::main --rust --keep-mangled 171 | 172 | - name: Native version of cargo-show-asm (LLVM) 173 | shell: bash 174 | if: ${{ !cancelled() }} 175 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib --llvm sample::main 176 | 177 | - name: Native version of cargo-show-asm with symbol mangling (LLVM) 178 | shell: bash 179 | if: ${{ !cancelled() }} 180 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib --llvm --keep-mangled sample::main 181 | 182 | - name: Native version of cargo-show-asm (LLVM Input) 183 | shell: bash 184 | if: ${{ !cancelled() }} 185 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib --llvm-input sample::main 186 | 187 | - name: Native version of cargo-show-asm (MIR) 188 | shell: bash 189 | if: ${{ !cancelled() }} 190 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib --mir "main()" 191 | 192 | - name: Crosscompiled version of cargo-show-asm (Intel ASM) 193 | shell: bash 194 | if: ${{ !cancelled() }} 195 | run: ./.github/check.sh --manifest-path sample/Cargo.toml --lib --intel sample::main --target armv7-unknown-linux-gnueabihf 196 | 197 | - name: Rlib project, AT&T asm 198 | shell: bash 199 | if: ${{ !cancelled() }} 200 | run: ./.github/check.sh --manifest-path sample_rlib/Cargo.toml --att 201 | 202 | - name: cdylib project 203 | shell: bash 204 | if: ${{ !cancelled() }} 205 | run: ./.github/check.sh --manifest-path sample_cdylib/Cargo.toml add 206 | 207 | - name: cdylib project, underscore prefix 208 | shell: bash 209 | if: ${{ !cancelled() }} 210 | run: ./.github/check.sh --manifest-path sample_cdylib/Cargo.toml _mul 211 | 212 | - name: merged functions simd 213 | shell: bash 214 | if: ${{ !cancelled() && matrix.os != 'macos-latest' }} # uses x86 primops 215 | run: ./.github/check.sh --manifest-path sample_merged/Cargo.toml sample_merged::merged_0 --include-constants -c 1 216 | 217 | - name: merged functions simd 218 | shell: bash 219 | if: ${{ !cancelled() && matrix.os != 'macos-latest' }} # uses x86 primops 220 | run: ./.github/check.sh --manifest-path sample_merged/Cargo.toml sample_merged::merged_1 --include-constants -c 1 221 | 222 | - name: merged functions extern c 223 | shell: bash 224 | if: ${{ !cancelled() }} 225 | run: ./.github/check.sh --manifest-path sample_merged/Cargo.toml sample_merged::extern_c_0 -c 1 226 | 227 | - name: merged functions extern c 228 | shell: bash 229 | if: ${{ !cancelled() }} 230 | run: ./.github/check.sh --manifest-path sample_merged/Cargo.toml sample_merged::extern_c_1 -c 1 231 | 232 | - name: merged functions plain 233 | shell: bash 234 | if: ${{ !cancelled() }} 235 | run: ./.github/check.sh --manifest-path sample_merged/Cargo.toml sample_merged::plain_0 -c 1 236 | 237 | - name: merged functions plain 238 | shell: bash 239 | if: ${{ !cancelled() }} 240 | run: ./.github/check.sh --manifest-path sample_merged/Cargo.toml sample_merged::plain_1 -c 1 241 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /*/target 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.98" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 25 | 26 | [[package]] 27 | name = "ar" 28 | version = "0.9.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d67af77d68a931ecd5cbd8a3b5987d63a1d1d1278f7f6a60ae33db485cdebb69" 31 | 32 | [[package]] 33 | name = "bpaf" 34 | version = "0.9.20" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "473976d7a8620bb1e06dcdd184407c2363fe4fec8e983ee03ed9197222634a31" 37 | dependencies = [ 38 | "bpaf_derive", 39 | "owo-colors", 40 | "supports-color 3.0.2", 41 | ] 42 | 43 | [[package]] 44 | name = "bpaf_derive" 45 | version = "0.5.17" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "fefb4feeec9a091705938922f26081aad77c64cd2e76cd1c4a9ece8e42e1618a" 48 | dependencies = [ 49 | "proc-macro2", 50 | "quote", 51 | "syn", 52 | ] 53 | 54 | [[package]] 55 | name = "camino" 56 | version = "1.1.9" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 59 | dependencies = [ 60 | "serde", 61 | ] 62 | 63 | [[package]] 64 | name = "capstone" 65 | version = "0.13.0" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "015ef5d5ca1743e3f94af9509ba6bd2886523cfee46e48d15c2ef5216fd4ac9a" 68 | dependencies = [ 69 | "capstone-sys", 70 | "libc", 71 | ] 72 | 73 | [[package]] 74 | name = "capstone-sys" 75 | version = "0.17.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "2267cb8d16a1e4197863ec4284ffd1aec26fe7e57c58af46b02590a0235809a0" 78 | dependencies = [ 79 | "cc", 80 | "libc", 81 | ] 82 | 83 | [[package]] 84 | name = "cargo-platform" 85 | version = "0.1.9" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" 88 | dependencies = [ 89 | "serde", 90 | ] 91 | 92 | [[package]] 93 | name = "cargo-show-asm" 94 | version = "0.2.49" 95 | dependencies = [ 96 | "anyhow", 97 | "ar", 98 | "bpaf", 99 | "capstone", 100 | "cargo-show-asm", 101 | "cargo_metadata", 102 | "line-span", 103 | "nom", 104 | "object", 105 | "owo-colors", 106 | "regex", 107 | "rustc-demangle", 108 | "same-file", 109 | "serde", 110 | "supports-color 3.0.2", 111 | ] 112 | 113 | [[package]] 114 | name = "cargo_metadata" 115 | version = "0.19.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" 118 | dependencies = [ 119 | "camino", 120 | "cargo-platform", 121 | "semver", 122 | "serde", 123 | "serde_json", 124 | "thiserror", 125 | ] 126 | 127 | [[package]] 128 | name = "cc" 129 | version = "1.2.5" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" 132 | dependencies = [ 133 | "shlex", 134 | ] 135 | 136 | [[package]] 137 | name = "cfg-if" 138 | version = "1.0.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 141 | 142 | [[package]] 143 | name = "crc32fast" 144 | version = "1.4.2" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 147 | dependencies = [ 148 | "cfg-if", 149 | ] 150 | 151 | [[package]] 152 | name = "flate2" 153 | version = "1.0.35" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 156 | dependencies = [ 157 | "crc32fast", 158 | "miniz_oxide", 159 | ] 160 | 161 | [[package]] 162 | name = "hermit-abi" 163 | version = "0.4.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 166 | 167 | [[package]] 168 | name = "is-terminal" 169 | version = "0.4.13" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" 172 | dependencies = [ 173 | "hermit-abi", 174 | "libc", 175 | "windows-sys 0.52.0", 176 | ] 177 | 178 | [[package]] 179 | name = "is_ci" 180 | version = "1.2.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" 183 | 184 | [[package]] 185 | name = "itoa" 186 | version = "1.0.14" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 189 | 190 | [[package]] 191 | name = "libc" 192 | version = "0.2.169" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 195 | 196 | [[package]] 197 | name = "line-span" 198 | version = "0.1.5" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "29fc123b2f6600099ca18248f69e3ee02b09c4188c0d98e9a690d90dec3e408a" 201 | 202 | [[package]] 203 | name = "memchr" 204 | version = "2.7.4" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 207 | 208 | [[package]] 209 | name = "miniz_oxide" 210 | version = "0.8.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" 213 | dependencies = [ 214 | "adler2", 215 | ] 216 | 217 | [[package]] 218 | name = "nom" 219 | version = "8.0.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 222 | dependencies = [ 223 | "memchr", 224 | ] 225 | 226 | [[package]] 227 | name = "object" 228 | version = "0.37.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "6273adb7096cf9ab4335f258e627d8230e69d40d45567d678f552dcec6245215" 231 | dependencies = [ 232 | "flate2", 233 | "memchr", 234 | "ruzstd", 235 | ] 236 | 237 | [[package]] 238 | name = "owo-colors" 239 | version = "4.2.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" 242 | dependencies = [ 243 | "supports-color 2.1.0", 244 | "supports-color 3.0.2", 245 | ] 246 | 247 | [[package]] 248 | name = "proc-macro2" 249 | version = "1.0.92" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 252 | dependencies = [ 253 | "unicode-ident", 254 | ] 255 | 256 | [[package]] 257 | name = "quote" 258 | version = "1.0.37" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 261 | dependencies = [ 262 | "proc-macro2", 263 | ] 264 | 265 | [[package]] 266 | name = "regex" 267 | version = "1.11.1" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 270 | dependencies = [ 271 | "aho-corasick", 272 | "memchr", 273 | "regex-automata", 274 | "regex-syntax", 275 | ] 276 | 277 | [[package]] 278 | name = "regex-automata" 279 | version = "0.4.9" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 282 | dependencies = [ 283 | "aho-corasick", 284 | "memchr", 285 | "regex-syntax", 286 | ] 287 | 288 | [[package]] 289 | name = "regex-syntax" 290 | version = "0.8.5" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 293 | 294 | [[package]] 295 | name = "rustc-demangle" 296 | version = "0.1.24" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 299 | 300 | [[package]] 301 | name = "ruzstd" 302 | version = "0.8.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "3640bec8aad418d7d03c72ea2de10d5c646a598f9883c7babc160d91e3c1b26c" 305 | dependencies = [ 306 | "twox-hash", 307 | ] 308 | 309 | [[package]] 310 | name = "ryu" 311 | version = "1.0.18" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 314 | 315 | [[package]] 316 | name = "same-file" 317 | version = "1.0.6" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 320 | dependencies = [ 321 | "winapi-util", 322 | ] 323 | 324 | [[package]] 325 | name = "semver" 326 | version = "1.0.24" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" 329 | dependencies = [ 330 | "serde", 331 | ] 332 | 333 | [[package]] 334 | name = "serde" 335 | version = "1.0.216" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 338 | dependencies = [ 339 | "serde_derive", 340 | ] 341 | 342 | [[package]] 343 | name = "serde_derive" 344 | version = "1.0.216" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 347 | dependencies = [ 348 | "proc-macro2", 349 | "quote", 350 | "syn", 351 | ] 352 | 353 | [[package]] 354 | name = "serde_json" 355 | version = "1.0.134" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" 358 | dependencies = [ 359 | "itoa", 360 | "memchr", 361 | "ryu", 362 | "serde", 363 | ] 364 | 365 | [[package]] 366 | name = "shlex" 367 | version = "1.3.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 370 | 371 | [[package]] 372 | name = "supports-color" 373 | version = "2.1.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" 376 | dependencies = [ 377 | "is-terminal", 378 | "is_ci", 379 | ] 380 | 381 | [[package]] 382 | name = "supports-color" 383 | version = "3.0.2" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" 386 | dependencies = [ 387 | "is_ci", 388 | ] 389 | 390 | [[package]] 391 | name = "syn" 392 | version = "2.0.91" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" 395 | dependencies = [ 396 | "proc-macro2", 397 | "quote", 398 | "unicode-ident", 399 | ] 400 | 401 | [[package]] 402 | name = "thiserror" 403 | version = "2.0.9" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" 406 | dependencies = [ 407 | "thiserror-impl", 408 | ] 409 | 410 | [[package]] 411 | name = "thiserror-impl" 412 | version = "2.0.9" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" 415 | dependencies = [ 416 | "proc-macro2", 417 | "quote", 418 | "syn", 419 | ] 420 | 421 | [[package]] 422 | name = "twox-hash" 423 | version = "2.1.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908" 426 | 427 | [[package]] 428 | name = "unicode-ident" 429 | version = "1.0.14" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 432 | 433 | [[package]] 434 | name = "winapi-util" 435 | version = "0.1.9" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 438 | dependencies = [ 439 | "windows-sys 0.59.0", 440 | ] 441 | 442 | [[package]] 443 | name = "windows-sys" 444 | version = "0.52.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 447 | dependencies = [ 448 | "windows-targets", 449 | ] 450 | 451 | [[package]] 452 | name = "windows-sys" 453 | version = "0.59.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 456 | dependencies = [ 457 | "windows-targets", 458 | ] 459 | 460 | [[package]] 461 | name = "windows-targets" 462 | version = "0.52.6" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 465 | dependencies = [ 466 | "windows_aarch64_gnullvm", 467 | "windows_aarch64_msvc", 468 | "windows_i686_gnu", 469 | "windows_i686_gnullvm", 470 | "windows_i686_msvc", 471 | "windows_x86_64_gnu", 472 | "windows_x86_64_gnullvm", 473 | "windows_x86_64_msvc", 474 | ] 475 | 476 | [[package]] 477 | name = "windows_aarch64_gnullvm" 478 | version = "0.52.6" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 481 | 482 | [[package]] 483 | name = "windows_aarch64_msvc" 484 | version = "0.52.6" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 487 | 488 | [[package]] 489 | name = "windows_i686_gnu" 490 | version = "0.52.6" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 493 | 494 | [[package]] 495 | name = "windows_i686_gnullvm" 496 | version = "0.52.6" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 499 | 500 | [[package]] 501 | name = "windows_i686_msvc" 502 | version = "0.52.6" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 505 | 506 | [[package]] 507 | name = "windows_x86_64_gnu" 508 | version = "0.52.6" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 511 | 512 | [[package]] 513 | name = "windows_x86_64_gnullvm" 514 | version = "0.52.6" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 517 | 518 | [[package]] 519 | name = "windows_x86_64_msvc" 520 | version = "0.52.6" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 523 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-show-asm" 3 | version = "0.2.49" 4 | edition = "2021" 5 | description = "A cargo subcommand that displays the generated assembly of Rust source code." 6 | categories = ["development-tools::cargo-plugins", "development-tools::debugging"] 7 | keywords = ["assembly", "plugins", "cargo"] 8 | repository = "https://github.com/pacak/cargo-show-asm" 9 | homepage = "https://github.com/pacak/cargo-show-asm" 10 | authors = ["Michael Baykov "] 11 | readme = "README.md" 12 | license = "MIT OR Apache-2.0" 13 | 14 | [dependencies] 15 | anyhow = "1" 16 | ar = { version = "0.9", optional = true } 17 | bpaf = { version = "0.9.20", features = ["bpaf_derive", "autocomplete"] } 18 | capstone = { version = "0.13", optional = true } 19 | cargo_metadata = "0.19.2" 20 | line-span = "0.1" 21 | nom = "8" 22 | object = { version = "0.37", optional = true } 23 | owo-colors = { version = "4", features = ["supports-colors"] } 24 | regex = "1" 25 | rustc-demangle = "0.1" 26 | same-file = "1.0.6" 27 | serde = "=1.0.216" 28 | supports-color = "3.0" 29 | 30 | [dev-dependencies] 31 | bpaf = { version = "0.9.20", features = ["bpaf_derive", "autocomplete", "docgen"] } 32 | cargo-show-asm = { path = ".", features = ["disasm"] } 33 | 34 | 35 | [features] 36 | bright-color = ["bpaf/bright-color"] 37 | default = ["dull-color"] 38 | disasm = ["ar", "capstone", "object"] 39 | dull-color = ["bpaf/dull-color"] 40 | 41 | [[bin]] 42 | name = "cargo-asm" 43 | path = "src/main.rs" 44 | 45 | [workspace.metadata.cauwugo] 46 | bpaf = true 47 | 48 | [profile.release-lto] 49 | lto = true 50 | codegen-units = 1 51 | inherits = "release" 52 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.2.49] - 2025-04-06 4 | - Add a short alias `-F` in addition to `--features` (#390) 5 | thanks @joseluis 6 | - try to support cursed combinations of forward and backward slashes 7 | in file names created in case of crosscompilation (#382) 8 | 9 | ## [0.2.48] - 2025-02-22 10 | - better support for ARM MCA and disassembly (#378) and (#375) 11 | thanks @kornelski 12 | - default verbosity changes - (#377) 13 | - clippy improvements - (#376) 14 | 15 | ## [0.2.47] - 2025-01-21 16 | - don't try to override RUSTFLAGS unless needed - this should keep .config/cargo working (#364) 17 | 18 | ## [0.2.46] - 2025-01-15 19 | - `--silent` flag can be used to suppress some of the user directed informatin 20 | thanks @tgross35 21 | 22 | ## [0.2.45] - 2024-12-23 23 | - flags related to target cpu are now passed to all the dependencies if specified 24 | thanks @zheland 25 | - bump deps in tests 26 | - bump deps 27 | 28 | ## [0.2.44] - 2024-12-20 29 | - improve error for mismaching/corrupted rust-src 30 | - support for ARM disassembly 31 | thanks @fg-cfh 32 | - bump serde that was pinned due to it shipping binary blobs 33 | - bump deps 34 | 35 | ## [0.2.43] - 2024-12-07 36 | - `-vv` also prints invoked cargo command (#345) 37 | thanks @zheland 38 | - bump deps 39 | 40 | ## [0.2.42] - 2024-11-10 41 | - `--quiet` option that gets passed to `cargo 42 | - Also search for context in `.set` statements - for merged functions 43 | this mean that when you are showing the alias with `-c 1` - the actual 44 | implementation will show up as well (#338) 45 | 46 | ## [0.2.41] - 2024-10-13 47 | - make sure not to drop used labels (#318) 48 | - add release-lto profile for slightly smaller/faster version 49 | thanks @zamazan4ik for the suggestion 50 | - detect and render merged functions (#310) 51 | - update docs (#320) 52 | - smarter approach for detecting constants (#315) 53 | - smarter CI (#79) 54 | - bump deps 55 | 56 | ## [0.2.40] - 2024-10-01 57 | - more consistend behavior when only one item is detected (#312) 58 | thanks @zheland 59 | - fixed llvm output for no_mangle functions (#313) 60 | thanks @zheland 61 | - bump deps 62 | 63 | ## [0.2.39] - 2024-09-19 64 | - support --config KEY=VAL option that is passed directly to cargo 65 | - bump deps 66 | 67 | ## [0.2.38] - 2024-07-02 68 | - slightly smarter artifact detection, shouldn't panic with wasm crates 69 | - bump deps 70 | 71 | ## [0.2.37] - 2024-06-27 72 | - support combination of --everything and --rust 73 | - bump deps 74 | 75 | ## [0.2.36] - 2024-06-02 76 | - even better support for no_mangle names on windows 77 | thanks to @evmar 78 | - bump deps 79 | 80 | ## [0.2.35] - 2024-05-06 81 | - don't include constants by default 82 | - include local jump labels in disassembly 83 | - include instructions in hex in disassembly 84 | - avoid memory addresses with displacement in disassembly 85 | - bump bpaf 86 | 87 | ## [0.2.34] - 2024-04-25 88 | - don't force debuginfo in llvm, fixes #269 89 | 90 | ## [0.2.33] - 2024-04-24 91 | - Experimental support for disassembly, `cargo-show-asm` needs to be compiled with "disasm" 92 | feature. 93 | 94 | With that you can pass `--disasm` flag to disassemble binary artifacts (`.rlib` files or 95 | executables) created by cargo. 96 | 97 | To work with PGO, BOLT or other optimizations that require non standard build process you 98 | can pass path to binary directly with `--file`. 99 | 100 | For `cargo-show-asm` to detect symbols in your code you need to disable stripping by adding 101 | something like this to `Cargo.toml` 102 | 103 | ``` 104 | [profile.release] 105 | strip = false 106 | ``` 107 | 108 | At the moment interleaving rust source (`--rust`) is not supported 109 | - bump deps 110 | 111 | 112 | ## [0.2.32] - 2024-04-13 113 | - include more instructions in `--simplify` 114 | - handle combinations like `--mca --intel` in addition to `--mca-intel` which is now deprecated 115 | - cosmetic improvements in produced output 116 | - a bunch of internal improvements 117 | - drop `once_cell` dependency 118 | 119 | ## [0.2.31] - 2024-04-04 120 | - include relevant constants in produced asm output 121 | this can be disabled with `--no-constants` 122 | - bump deps 123 | 124 | ## [0.2.30] - 2024-02-11 125 | - Add an option `-c` / `--context` to recursively include functions called from target as 126 | additional context 127 | 128 | ## [0.2.29] - 2024-01-23 129 | - fix function selection by index, see https://github.com/pacak/cargo-show-asm/issues/244 130 | 131 | ## [0.2.28] - 2024-01-17 132 | - Add a set of options to limit rust source code to workspace, regular crates or all available 133 | code: 134 | 135 | --this-workspace Show rust sources from current workspace only 136 | --all-crates Show rust sources from current workspace and from rust registry 137 | --all-sources Show all the rust sources including stdlib and compiler 138 | 139 | 140 | ## [0.2.27] - 2024-01-14 141 | - look for rustc source code in the right place, see https://github.com/pacak/cargo-show-asm/issues/238 142 | 143 | ## [0.2.26] - 2024-01-09 144 | - avoid using hard to see colors 145 | thanks to @epontan 146 | - bump deps 147 | 148 | ## [0.2.25] - 2023-12-31 149 | - Improve accuracy of llvm lines, see https://github.com/pacak/cargo-show-asm/pull/229 150 | thanks to @osiewicz 151 | - fix CI 152 | 153 | ## [0.2.24] - 2023-12-28 154 | - add an option to keep mangled name, thanks to @osiewicz 155 | - add syntax highlight for mangled names 156 | - bump dependencies 157 | 158 | ## [0.2.23] - 2023-11-26 159 | - Add an option to strip blank lines and make it default, original behavior is accessible 160 | with `-B` option 161 | 162 | ## [0.2.22] - 2023-10-10 163 | - better support for no_mangle in macOS 164 | - ignore empty source files - seen them on Windows 165 | - bump a bunch of deps 166 | - add license files 167 | 168 | ## [0.2.21] - 2023-08-12 169 | - support wonky non-utf8 files produced by rustc/llvm o_O 170 | 171 | ## [0.2.20] - 2023-06-17 172 | - workaround for fancier debuginfo not supported by cargo-metadata 173 | - usage in README is now generated in markdown 174 | 175 | ## [0.2.19] - 2023-06-05 176 | - bump bpaf to 0.9.1, usage in README is now generated 177 | - bump deps 178 | 179 | ## [0.2.18] - 2023-05-11 180 | - you can also specify default profile using `CARGO_SHOW_ASM_PROFILE` env variable 181 | - bump bpaf to 0.8.0, add dull colors by default 182 | 183 | ## [0.2.17] - 2023-04-11 184 | - look harder for source code, don't panic if it can't be found 185 | - bump deps 186 | 187 | ## [0.2.16] - 2023-04-04 188 | - drop some dependencies 189 | - support for strange looking file names in dwarf info 190 | 191 | ## [0.2.15] - 2023-03-09 192 | - Override lto configuration to lto=no, #146 193 | 194 | ## [0.2.14] - 2023-02-22 195 | - Allow to pass -C flags directly to rustc 196 | - --llvm-input to show llvm-ir before any LLVM passes 197 | - Only generate debug info for LLVM resulting in cleaner 198 | Thanks to @jonasmalacofilho 199 | 200 | ## [0.2.13] - 2023-02-03 201 | - support cdylib crates 202 | - bump deps 203 | 204 | ## [0.2.12] - 2023-01-13 205 | - allow to pass -Z flags directly to cargo 206 | - support for llvm-mca 207 | 208 | ## [0.2.11] - 2023-01-11 209 | - fix filtering by index and name at the same time 210 | - --test, --bench, etc. can be used without argument to list available items 211 | thanks to @danielparks 212 | - bump deps 213 | 214 | ## [0.2.10] - 2023-01-09 215 | - support for nightly -Z asm-comments 216 | 217 | ## [0.2.9] - 2023-01-07 218 | - improve error messages 219 | - properly handle exception handling code on Windows 220 | thanks to @al13n321 221 | - support --rust on Windows 222 | thanks to @al13n321 223 | 224 | ## [0.2.8] - 2023-01-02 225 | - bump dependencies 226 | 227 | ## [0.2.7] - 2022-11-26 228 | - support mangled names 229 | - fix select-by-index 230 | 231 | ## [0.2.6] - 2022-11-23 232 | - use color for cargo diagnostics 233 | Thanks to @coolreader18 234 | - support for WASM target 235 | Thanks to @coolreader18 236 | 237 | ## [0.2.5] - 2022-11-21 238 | - include README.md into docs.rs docs 239 | - dump function by index as well as by name 240 | - improve label colorization and stripping 241 | Thanks to @RustyYato 242 | - bump dependencies 243 | 244 | ## [0.2.4] - 2022-11-12 245 | - `--simplify` option - to strip some of the things that are not cpu instructions 246 | from the asm output 247 | 248 | ## [0.2.3] - 2022-11-05 249 | - support rlib projects + tests 250 | 251 | ## [0.2.2] - 2022-11-01 252 | - fix `--color` and `--no-color`, regression since 0.2.0 253 | 254 | ## [0.2.1] - 2022-10-29 255 | - number of macOS specific bugfixes 256 | - update deps 257 | - more detailed output with verbosity flags 258 | 259 | ## [0.2.0] - 2022-10-22 260 | - replaced libcargo with invoking cargo 261 | Thanks to @oxalica 262 | - renamed `--feature` -> `--features` 263 | - dropped backward compatibility `-no-defaut-features` 264 | - implemented `--everything` to dump the whole file demangled 265 | 266 | ## [0.1.24] - 2022-10-15 267 | - support custom profiles 268 | - support reading rust sources from registries 269 | 270 | ## [0.1.23] - 2022-10-11 271 | - update dependenies + bpaf 272 | - optional colorful command line parser output 273 | 274 | ## [0.1.22] - 2022-10-03 275 | - strip redundant labels by default 276 | - cleaning up 277 | - removing glob in favor of std::fs - windows CI now works 278 | - document completions 279 | 280 | ## [0.1.21] - 2022-09-29 281 | - options for vendored libgit2 and openssl 282 | - documentation improvements 283 | Thanks to @dtolnay, @matthiasbeyer and @saethlin 284 | - support --native and --target-cpu 285 | - when dumping a function - dump full section 286 | 287 | ## [0.1.20] - 2022-09-24 288 | - Update cargo version to 0.65 289 | - Bump bpaf 290 | Thanks to @elichai 291 | 292 | ## [0.1.19] - 2022-09-23 293 | - detect missing sources and suggest to install them 294 | 295 | ## [0.1.18] - 2022-09-15 296 | - bugfix to package selection in non-virtual workspaces 297 | 298 | ## [0.1.17] - 2022-09-12 299 | - fix typo in default features 300 | Thanks to @mooli 301 | - fix more cross-compilation issues 302 | Thanks to @WIgor 303 | 304 | ## [0.1.16] - 2022-09-03 305 | - Fix parsing of file directive on nightly 306 | - Bump bpaf 307 | Thanks to @yotamofek 308 | 309 | ## [0.1.15] - 2022-08-23 310 | - Update bpaf to 0.5.2, should start give more user friendly suggestions 311 | 312 | ## [0.1.14] - 2022-08-20 313 | - Also accept target dir from `env:CARGO_TARGET_DIR` 314 | 315 | ## [0.1.13] - 2022-08-16 316 | - Upgrade cargo dependency 317 | 318 | ## [0.1.12] - 2022-08-01 319 | - Dump single match as is 320 | 321 | ## [0.1.11] - 2022-07-23 322 | - Improved cross-compilation support 323 | 324 | ## [0.1.10] - 2022-07-05 325 | - Upgrade cargo dependency 326 | 327 | ## [0.1.9] - 2022-07-01 328 | - Upgrade cargo dependency 329 | 330 | ## [0.1.8] - 2022-06-24 331 | - arm asm bugfixes 332 | - Bump the dependencies, mostly cargo to 0.62 333 | 334 | ## [0.1.7] - 2022-05-25 335 | - arm asm bugfixes 336 | thanks to @RustyYato 337 | 338 | ## [0.1.6] - 2022-05-22 339 | - Limited support for MIR 340 | 341 | ## [0.1.5] - 2022-05-16 342 | - bump dependencies 343 | 344 | ## [0.1.4] - 2022-04-20 345 | - Limited support for LLVM-IR 346 | 347 | ## [0.1.3] - 2022-04-20 348 | - Limited support for Windows: 349 | Works a bit better thanks to @nico-abram 350 | 351 | ## [0.1.2] - 2022-04-15 352 | - Limited support for Windows: 353 | showing asm code for function mostly works, adding rust code annotation doesn't. 354 | 355 | ## [0.1.1] - 2022-04-14 356 | - First public release 357 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022-2024 Mikhail Baykov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-show-asm 2 | 3 | A cargo subcommand that displays the Assembly, LLVM-IR, MIR and WASM generated for Rust source code. 4 | 5 | # Install 6 | 7 | ```console 8 | $ cargo install cargo-show-asm 9 | ``` 10 | 11 | # Features 12 | 13 | - Platform support: 14 | 15 | - OS: Linux and macOS. Limited support for Windows 16 | - Rust: nightly and stable. 17 | - Architectures: `x86`, `x86_64`, `aarch64`, etc. 18 | - Cross-compilation support. 19 | 20 | - Displaying: 21 | 22 | - Assembly in Intel or AT&T syntax. 23 | - Corresponding Rust source code alongside assembly. 24 | - llvm-ir. 25 | - rustc MIR 26 | - Wasm code 27 | - llvm-mca analysis 28 | 29 | # cargo asm 30 | 31 | Show the code rustc generates for any function 32 | 33 | **Usage**: **`cargo asm`** \[**`-p`**=_`SPEC`_\] \[_`ARTIFACT`_\] \[**`-M`**=_`ARG`_\]... \[_`TARGET-CPU`_\] \[**`--rust`**\] \[**`-c`**=_`COUNT`_\] \[**`--simplify`**\] \[**`--include-constants`**\] \[**`--this-workspace`** | **`--all-crates`** | **`--all-sources`**\] _`OUTPUT-FORMAT`_ \[**`--everything`** | _`FUNCTION`_ \[_`INDEX`_\]\] 34 | 35 | Usage: 36 | 1. Focus on a single assembly producing target: 37 | 38 | ```text 39 | % cargo asm -p isin --lib # here we are targeting lib in isin crate 40 | 41 | 42 | ``` 43 | 2. Narrow down a function: 44 | 45 | ```text 46 | % cargo asm -p isin --lib from_ # here "from_" is part of the function you are interested intel 47 | 48 | 49 | ``` 50 | 3. Get the full results: 51 | 52 | ```text 53 | % cargo asm -p isin --lib isin::base36::from_alphanum 54 | ``` 55 | 56 | 57 | **Pick artifact for analysis:** 58 | - **` --lib`** — 59 | Show results from library code 60 | - **` --test`**=_`TEST`_ — 61 | Show results from an integration test 62 | - **` --bench`**=_`BENCH`_ — 63 | Show results from a benchmark 64 | - **` --example`**=_`EXAMPLE`_ — 65 | Show results from an example 66 | - **` --bin`**=_`BIN`_ — 67 | Show results from a binary 68 | 69 | 70 | 71 | **Cargo options** 72 | - **` --manifest-path`**=_`PATH`_ — 73 | Path to Cargo.toml, defaults to one in current folder 74 | - **` --config`**=_``_ — 75 | Override a cargo configuration value 76 | - **` --target-dir`**=_`DIR`_ — 77 | Use custom target directory for generated artifacts, create if missing 78 | 79 | Uses environment variable **`CARGO_TARGET_DIR`** 80 | - **` --dry`** — 81 | Produce a build plan instead of actually building 82 | - **` --frozen`** — 83 | Requires Cargo.lock and cache to be up-to-date 84 | - **` --locked`** — 85 | Requires Cargo.lock to be up-to-date 86 | - **` --offline`** — 87 | Run without accessing the network 88 | - **`-q`**, **`--quiet`** — 89 | Do not print cargo log messages 90 | - **` --no-default-features`** — 91 | Do not activate `default` feature 92 | - **` --all-features`** — 93 | Activate all available features 94 | - **`-F`**, **`--features`**=_`FEATURE`_ — 95 | A feature to activate, can be used multiple times 96 | - **` --release`** — 97 | Compile in release mode (default) 98 | - **` --dev`** — 99 | Compile in dev mode 100 | - **` --profile`**=_`PROFILE`_ — 101 | Build for this specific profile, you can also use `dev` and `release` here 102 | 103 | Uses environment variable **`CARGO_SHOW_ASM_PROFILE`** 104 | - **` --target`**=_`TRIPLE`_ — 105 | Build for the target triple 106 | - **`-C`**=_`FLAG`_ — 107 | Codegen flags to rustc, see 'rustc -C help' for details 108 | - **`-Z`**=_`FLAG`_ — 109 | Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details 110 | 111 | 112 | 113 | **Postprocessing options:** 114 | - **` --rust`** — 115 | Print interleaved Rust code 116 | - **`-c`**, **`--context`**=_`COUNT`_ — 117 | Include other called functions, recursively, up to COUNT depth 118 | 119 | [default: 0] 120 | - **` --color`** — 121 | Enable color highlighting 122 | - **` --no-color`** — 123 | Disable color highlighting 124 | - **` --full-name`** — 125 | Include full demangled name instead of just prefix 126 | - **` --short-name`** — 127 | Include demangled names without hash suffix (default) 128 | - **` --keep-mangled`** — 129 | Do not demangle symbol names 130 | - **`-K`**, **`--keep-labels`** — 131 | Keep all the original labels 132 | - **`-B`**, **`--keep-blanks`** — 133 | Strip redundant labels, but keep spaces in their place 134 | - **`-R`**, **`--reduce-labels`** — 135 | Strip redundant labels entirely 136 | - **`-v`**, **`--verbose`** — 137 | more verbose output, can be specified multiple times 138 | - **`-s`**, **`--silent`** — 139 | print less user-forward information to make consumption by tools easier 140 | - **` --simplify`** — 141 | Try to strip some of the non-assembly instruction information 142 | - **` --include-constants`** — 143 | Include sections containing string literals and other constants 144 | - **`-b`**, **`--keep-blank`** — 145 | Keep blank lines 146 | - **` --this-workspace`** — 147 | Show rust sources from current workspace only 148 | - **` --all-crates`** — 149 | Show rust sources from current workspace and from rust registry 150 | - **` --all-sources`** — 151 | Show all the rust sources including stdlib and compiler 152 | 153 | 154 | 155 | **Pick output type:** 156 | - **` --asm`** — 157 | Show assembly 158 | - **` --disasm`** — 159 | Disassembly binaries or object files 160 | - **` --llvm`** — 161 | Show llvm-ir 162 | - **` --llvm-input`** — 163 | Show llvm-ir before any LLVM passes 164 | - **` --mir`** — 165 | Show MIR 166 | - **` --wasm`** — 167 | Show WASM, needs wasm32-unknown-unknown target installed 168 | - **` --mca`** — 169 | Show llvm-mca anasysis 170 | - **` --intel`** — 171 | Use Intel style for assembly 172 | - **` --att`** — 173 | Use AT&T style for assembly 174 | 175 | 176 | 177 | **Pick item to display from the artifact** 178 | - **` --everything`** — 179 | Dump the whole file 180 | - _`FUNCTION`_ — 181 | Dump a function with a given name, filter functions by name 182 | - _`INDEX`_ — 183 | Select specific function when there's several with the same name 184 | 185 | 186 | 187 | **Available options:** 188 | - **`-p`**, **`--package`**=_`SPEC`_ — 189 | Package to use, defaults to a current one, 190 | 191 | required for workspace projects, can also point to a dependency 192 | - **` --file`**=_`PATH`_ — 193 | Disassemble or process this file instead of calling cargo, 194 | requires cargo-show-asm to be compiled with disasm feature 195 | 196 | You can specify executable, rlib or an object file 197 | - **`-M`**, **`--mca-arg`**=_`ARG`_ — 198 | Pass parameter to llvm-mca for mca targets 199 | - **` --native`** — 200 | Optimize for the CPU running the compiler 201 | - **` --target-cpu`**=_`CPU`_ — 202 | Optimize code for a specific CPU, see 'rustc --print target-cpus' 203 | - **`-h`**, **`--help`** — 204 | Prints help information 205 | - **`-V`**, **`--version`** — 206 | Prints version information 207 | 208 | 209 | 210 | 211 | You can start by running `cargo asm` with no parameters - it will suggest how to narrow the 212 | search scope - for workspace crates you need to specify a crate to work with, for crates 213 | defining several targets (lib, binaries, examples) you need to specify exactly which target to 214 | use. In a workspace `cargo asm` lists only workspace members as suggestions but any crate from 215 | workspace tree is available. 216 | 217 | Once `cargo asm` focuses on a single target it will run rustc producing assembly file and will 218 | try to list of available public functions: 219 | 220 | ```console,ignore 221 | $ cargo asm --lib 222 | Try one of those 223 | "<&T as core::fmt::Display>::fmt" [17, 12, 12, 12, 12, 19, 19, 12] 224 | "<&mut W as core::fmt::Write>::write_char" [20] 225 | "<&mut W as core::fmt::Write>::write_fmt" [38] 226 | "<&mut W as core::fmt::Write>::write_str" [90] 227 | ">::parse" [263] 228 | # ... 229 | ``` 230 | 231 | Name in quotes is demangled rust name, numbers in square brackets represent number of lines 232 | in asm file. Function with the same name can be present in several instances. 233 | 234 | Specifying exact function name or a uniquely identifying part of it will print its assembly code 235 | 236 | ```console,ignore 237 | $ cargo asm --lib "cargo_show_asm::opts::focus::{{closure}}" 238 | ``` 239 | To pick between different alternatives you can either specify the index 240 | 241 | ```console,ignore 242 | $ cargo asm --lib "cargo_show_asm::opts::focus::{{closure}}" 2 243 | ``` 244 | Or start using full names with hex included: 245 | 246 | ```console,ignore 247 | $ cargo asm --lib --full-name 248 | # ... 249 | $ cargo asm --lib "once_cell::imp::OnceCell::initialize::h9c5c7d5bd745000b" 250 | ``` 251 | 252 | `cargo-show-asm` comes with a built-in search function. Just pass partial name 253 | instead of a full one and only matching functions will be listed 254 | 255 | ```console 256 | $ cargo asm --lib Debug 257 | ``` 258 | 259 | # My function isn't there! 260 | 261 | `rustc` will only generate the code for your function if it knows what type it is, including 262 | generic parameters and if it is exported (in case of a library) and not inlined (in case of a 263 | binary, example, test, etc.). If your function takes a generic parameter - try making a monomorphic 264 | wrapper around it and make it `pub` and `#[inline(never)]`. 265 | 266 | Alternatively if your function is too small - `rustc` might decide to inline it automatically 267 | with the same result. Marking it with `#[inline(never)]` fixes this problem. 268 | See https://github.com/rust-lang/rust/pull/116505 for more details 269 | 270 | # Include related functions? 271 | 272 | So suppose you have a function `foo` that calls some other function - `bar`. With `--context N` 273 | or it's short variant `-c N` you can ask cargo-show-asm to include body of bar to the input. 274 | This is done recursively up to N steps. See https://github.com/pacak/cargo-show-asm/issues/247 275 | 276 | 277 | # What about `cargo-asm`? 278 | 279 | `cargo-asm` is not maintained: . This crate is a reimplementation which addresses a number of its shortcomings, including: 280 | 281 | * `cargo-asm` recompiles everything every time with 1 codegen unit, which is slow and also not necessarily what is in your release profile. `cargo-show-asm` avoids that. 282 | 283 | * Because of how `cargo-asm` handles demangling the output looks like asm but isn't actually asm. It contains a bunch of extra commas which makes reusing it more annoying. 284 | 285 | * `cargo-asm` always uses colors unless you pass a flag while `cargo-show-asm` changes its default behavior if output is not sent to a terminal. 286 | 287 | * `cargo-show-asm` also supports MIR (note that the formatting of human-readable MIR is not stable). 288 | 289 | # Shell completion 290 | 291 | `cargo-asm` comes with shell completion generated by [`bpaf`](https://crates.io/crates/bpaf), 292 | use one of the lines below and place it into the place right for your shell. 293 | 294 | ```console 295 | $ cargo-asm --bpaf-complete-style-bash 296 | $ cargo-asm --bpaf-complete-style-zsh 297 | $ cargo-asm --bpaf-complete-style-fish 298 | $ cargo-asm --bpaf-complete-style-elvish 299 | ``` 300 | 301 | You'll need to use it as `cargo-asm` command rather than `cargo asm` to take advantage of it. 302 | 303 | 304 | # Colorful line parser output 305 | 306 | You can install `cargo-show-asm` with one of two features to get prettier command line 307 | ```console 308 | cargo install cargo-show-asm -F bright-color 309 | cargo install cargo-show-asm -F dull-color 310 | ``` 311 | 312 | # License 313 | This project is licensed under either of 314 | 315 | * Apache License, Version 2.0, (LICENSE-APACHE or ) 316 | * MIT license (LICENSE-MIT or ) 317 | 318 | at your option. 319 | 320 | # Contribution 321 | 322 | Unless you explicitly state otherwise, any contribution intentionally submitted 323 | for inclusion in this project by you, as defined in the Apache-2.0 license, 324 | shall be dual licensed as above, without any additional terms or conditions. 325 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # cargo-show-asm 2 | 3 | A cargo subcommand that displays the Assembly, LLVM-IR, MIR and WASM generated for Rust source code. 4 | 5 | # Install 6 | 7 | ```console 8 | $ cargo install cargo-show-asm 9 | ``` 10 | 11 | # Features 12 | 13 | - Platform support: 14 | 15 | - OS: Linux and macOS. Limited support for Windows 16 | - Rust: nightly and stable. 17 | - Architectures: `x86`, `x86_64`, `aarch64`, etc. 18 | - Cross-compilation support. 19 | 20 | - Displaying: 21 | 22 | - Assembly in Intel or AT&T syntax. 23 | - Corresponding Rust source code alongside assembly. 24 | - llvm-ir. 25 | - rustc MIR 26 | - Wasm code 27 | - llvm-mca analysis 28 | 29 | 30 | 31 | You can start by running `cargo asm` with no parameters - it will suggest how to narrow the 32 | search scope - for workspace crates you need to specify a crate to work with, for crates 33 | defining several targets (lib, binaries, examples) you need to specify exactly which target to 34 | use. In a workspace `cargo asm` lists only workspace members as suggestions but any crate from 35 | workspace tree is available. 36 | 37 | Once `cargo asm` focuses on a single target it will run rustc producing assembly file and will 38 | try to list of available public functions: 39 | 40 | ```console,ignore 41 | $ cargo asm --lib 42 | Try one of those 43 | "<&T as core::fmt::Display>::fmt" [17, 12, 12, 12, 12, 19, 19, 12] 44 | "<&mut W as core::fmt::Write>::write_char" [20] 45 | "<&mut W as core::fmt::Write>::write_fmt" [38] 46 | "<&mut W as core::fmt::Write>::write_str" [90] 47 | ">::parse" [263] 48 | # ... 49 | ``` 50 | 51 | Name in quotes is demangled rust name, numbers in square brackets represent number of lines 52 | in asm file. Function with the same name can be present in several instances. 53 | 54 | Specifying exact function name or a uniquely identifying part of it will print its assembly code 55 | 56 | ```console,ignore 57 | $ cargo asm --lib "cargo_show_asm::opts::focus::{{closure}}" 58 | ``` 59 | To pick between different alternatives you can either specify the index 60 | 61 | ```console,ignore 62 | $ cargo asm --lib "cargo_show_asm::opts::focus::{{closure}}" 2 63 | ``` 64 | Or start using full names with hex included: 65 | 66 | ```console,ignore 67 | $ cargo asm --lib --full-name 68 | # ... 69 | $ cargo asm --lib "once_cell::imp::OnceCell::initialize::h9c5c7d5bd745000b" 70 | ``` 71 | 72 | `cargo-show-asm` comes with a built-in search function. Just pass partial name 73 | instead of a full one and only matching functions will be listed 74 | 75 | ```console 76 | $ cargo asm --lib Debug 77 | ``` 78 | 79 | # My function isn't there! 80 | 81 | `rustc` will only generate the code for your function if it knows what type it is, including 82 | generic parameters and if it is exported (in case of a library) and not inlined (in case of a 83 | binary, example, test, etc.). If your function takes a generic parameter - try making a monomorphic 84 | wrapper around it and make it `pub` and `#[inline(never)]`. 85 | 86 | Alternatively if your function is too small - `rustc` might decide to inline it automatically 87 | with the same result. Marking it with `#[inline(never)]` fixes this problem. 88 | See https://github.com/rust-lang/rust/pull/116505 for more details 89 | 90 | # Include related functions? 91 | 92 | So suppose you have a function `foo` that calls some other function - `bar`. With `--context N` 93 | or it's short variant `-c N` you can ask cargo-show-asm to include body of bar to the input. 94 | This is done recursively up to N steps. See https://github.com/pacak/cargo-show-asm/issues/247 95 | 96 | 97 | # What about `cargo-asm`? 98 | 99 | `cargo-asm` is not maintained: . This crate is a reimplementation which addresses a number of its shortcomings, including: 100 | 101 | * `cargo-asm` recompiles everything every time with 1 codegen unit, which is slow and also not necessarily what is in your release profile. `cargo-show-asm` avoids that. 102 | 103 | * Because of how `cargo-asm` handles demangling the output looks like asm but isn't actually asm. It contains a bunch of extra commas which makes reusing it more annoying. 104 | 105 | * `cargo-asm` always uses colors unless you pass a flag while `cargo-show-asm` changes its default behavior if output is not sent to a terminal. 106 | 107 | * `cargo-show-asm` also supports MIR (note that the formatting of human-readable MIR is not stable). 108 | 109 | # Shell completion 110 | 111 | `cargo-asm` comes with shell completion generated by [`bpaf`](https://crates.io/crates/bpaf), 112 | use one of the lines below and place it into the place right for your shell. 113 | 114 | ```console 115 | $ cargo-asm --bpaf-complete-style-bash 116 | $ cargo-asm --bpaf-complete-style-zsh 117 | $ cargo-asm --bpaf-complete-style-fish 118 | $ cargo-asm --bpaf-complete-style-elvish 119 | ``` 120 | 121 | You'll need to use it as `cargo-asm` command rather than `cargo asm` to take advantage of it. 122 | 123 | 124 | # Colorful line parser output 125 | 126 | You can install `cargo-show-asm` with one of two features to get prettier command line 127 | ```console 128 | cargo install cargo-show-asm -F bright-color 129 | cargo install cargo-show-asm -F dull-color 130 | ``` 131 | 132 | # License 133 | This project is licensed under either of 134 | 135 | * Apache License, Version 2.0, (LICENSE-APACHE or ) 136 | * MIT license (LICENSE-MIT or ) 137 | 138 | at your option. 139 | 140 | # Contribution 141 | 142 | Unless you explicitly state otherwise, any contribution intentionally submitted 143 | for inclusion in this project by you, as defined in the Apache-2.0 license, 144 | shall be dual licensed as above, without any additional terms or conditions. 145 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | disallowed-macros = [ 2 | { path = "std::print", reason = "it throws ugly errors, use safeprint" }, 3 | { path = "std::eprint", reason = "it throws ugly errors, use esafeprint" }, 4 | { path = "std::println", reason = "it throws ugly errors, use safeprintln" }, 5 | { path = "std::eprintln", reason = "it throws ugly errors, use esafeprint" }, 6 | ] 7 | -------------------------------------------------------------------------------- /sample/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.7.8" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" 10 | dependencies = [ 11 | "getrandom", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "cfg-if" 18 | version = "1.0.0" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 21 | 22 | [[package]] 23 | name = "getrandom" 24 | version = "0.2.15" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 27 | dependencies = [ 28 | "cfg-if", 29 | "libc", 30 | "wasi", 31 | ] 32 | 33 | [[package]] 34 | name = "hashbrown" 35 | version = "0.12.3" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 38 | dependencies = [ 39 | "ahash", 40 | ] 41 | 42 | [[package]] 43 | name = "libc" 44 | version = "0.2.169" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 47 | 48 | [[package]] 49 | name = "once_cell" 50 | version = "1.20.2" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 53 | 54 | [[package]] 55 | name = "rand_core" 56 | version = "0.6.4" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 59 | 60 | [[package]] 61 | name = "sample" 62 | version = "0.1.0" 63 | dependencies = [ 64 | "hashbrown", 65 | "rand_core", 66 | ] 67 | 68 | [[package]] 69 | name = "version_check" 70 | version = "0.9.5" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 73 | 74 | [[package]] 75 | name = "wasi" 76 | version = "0.11.0+wasi-snapshot-preview1" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 79 | -------------------------------------------------------------------------------- /sample/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sample" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | hashbrown = "0.12.0" 10 | rand_core = "0.6" 11 | 12 | 13 | [features] 14 | default = ["superbanana"] 15 | superbanana = [] 16 | 17 | 18 | [profile.release] 19 | debug = "line-tables-only" 20 | 21 | [workspace] 22 | -------------------------------------------------------------------------------- /sample/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::disallowed_macros)] 2 | 3 | use rand_core::block::{BlockRng, BlockRngCore}; 4 | use rand_core::{RngCore, SeedableRng}; 5 | 6 | pub struct MyRngCore(pub [u8; 32]); 7 | 8 | impl BlockRngCore for MyRngCore { 9 | type Item = u32; 10 | type Results = [u32; 16]; 11 | 12 | fn generate(&mut self, results: &mut Self::Results) { 13 | for (to, from) in std::iter::zip(results.iter_mut(), self.0.iter()) { 14 | *to = *from as u32; 15 | } 16 | } 17 | } 18 | 19 | impl SeedableRng for MyRngCore { 20 | type Seed = [u8; 32]; 21 | 22 | // Will appear in --llvm-input (before LLVM passes), but not in --llvm (after LLVM passes). 23 | #[inline] 24 | fn from_seed(seed: Self::Seed) -> Self { 25 | Self(seed) 26 | } 27 | } 28 | 29 | #[cfg(not(feature = "superbanana"))] 30 | #[inline(never)] 31 | pub fn main() -> u32 { 32 | 1 + 1 33 | } 34 | 35 | #[inline(never)] 36 | pub fn panics() { 37 | panic!("oh noes asdf wef wef wf wefwefwef wef! {}", "bob"); 38 | } 39 | 40 | pub struct Bar(pub u32); 41 | impl Bar { 42 | #[no_mangle] 43 | pub fn make_bar(a: u32, b: u32) -> Self { 44 | Self(a + b) 45 | } 46 | } 47 | 48 | #[cfg(feature = "superbanana")] 49 | #[inline(never)] 50 | pub fn main() { 51 | let mut rng = BlockRng::::seed_from_u64(0); 52 | for ix in 0..10 { 53 | println!("{ix} rng values: {}", rng.next_u32()); 54 | } 55 | 56 | use hashbrown::HashSet; 57 | let mut set = HashSet::new(); 58 | set.insert("a"); 59 | set.insert("b"); 60 | 61 | // Will print in an arbitrary order. 62 | for x in set.iter() { 63 | println!("{x}"); 64 | } 65 | 66 | println!("Total: {}", get_length(set)); 67 | } 68 | 69 | #[inline(never)] 70 | fn get_length(it: hashbrown::HashSet) -> usize { 71 | it.len() 72 | } 73 | 74 | pub fn okay() { 75 | let mut rng = BlockRng::::seed_from_u64(0); 76 | for ix in 0..10 { 77 | println!("{ix} rng values: {}", rng.next_u32()); 78 | } 79 | 80 | use hashbrown::HashSet; 81 | let mut set = HashSet::new(); 82 | set.insert("a"); 83 | set.insert("b"); 84 | 85 | // Will print in an arbitrary order. 86 | for x in set.iter() { 87 | println!("{x}"); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sample/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | sample::main() 3 | } 4 | -------------------------------------------------------------------------------- /sample_cdylib/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "xx" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /sample_cdylib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xx" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | 11 | [lib] 12 | crate-type = ["cdylib"] 13 | [workspace] 14 | 15 | -------------------------------------------------------------------------------- /sample_cdylib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[no_mangle] 2 | pub extern "C" fn add(left: usize, right: usize) -> usize { 3 | left + right 4 | } 5 | 6 | #[no_mangle] 7 | pub extern "C" fn sub(left: usize, right: usize) -> usize { 8 | left - right 9 | } 10 | 11 | #[no_mangle] 12 | pub extern "C" fn _mul(left: usize, right: usize) -> usize { 13 | left * right 14 | } 15 | -------------------------------------------------------------------------------- /sample_merged/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "sample_merged" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /sample_merged/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sample_merged" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /sample_merged/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[inline(never)] 2 | #[cfg(target_arch = "x86_64")] 3 | pub fn merged_0() { 4 | let simd_reg = unsafe { 5 | std::arch::x86_64::_mm_set_epi8(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) 6 | }; 7 | std::hint::black_box(simd_reg); 8 | } 9 | 10 | #[inline(never)] 11 | #[cfg(target_arch = "x86_64")] 12 | pub fn merged_1() { 13 | let simd_reg = unsafe { 14 | std::arch::x86_64::_mm_set_epi8(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) 15 | }; 16 | std::hint::black_box(simd_reg); 17 | } 18 | 19 | #[inline(never)] 20 | pub extern "C" fn extern_c_0() -> u32 { 21 | 2 22 | } 23 | 24 | #[inline(never)] 25 | pub extern "C" fn extern_c_1() -> u32 { 26 | 1 + 1 27 | } 28 | 29 | #[inline(never)] 30 | pub fn plain_0() -> u32 { 31 | 1 32 | } 33 | 34 | #[inline(never)] 35 | pub fn plain_1() -> u32 { 36 | 2 - 1 37 | } 38 | -------------------------------------------------------------------------------- /sample_rlib/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "shadow-rs" 7 | version = "2.2.0" 8 | -------------------------------------------------------------------------------- /sample_rlib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shadow-rs" 3 | version = "2.2.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | path = "lib.rs" 9 | # the rlib crate is so that we build and run doctests 10 | crate-type = ["rlib", "staticlib"] 11 | 12 | [workspace] 13 | 14 | -------------------------------------------------------------------------------- /sample_rlib/lib.rs: -------------------------------------------------------------------------------- 1 | #[inline(never)] 2 | pub fn add(a: usize, b: usize) -> usize { 3 | a + b 4 | } 5 | -------------------------------------------------------------------------------- /src/asm.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::missing_errors_doc)] 2 | use crate::asm::statements::Label; 3 | use crate::cached_lines::CachedLines; 4 | use crate::demangle::LabelKind; 5 | use crate::{ 6 | color, demangle, esafeprintln, get_context_for, safeprintln, Dumpable, Item, RawLines, URange, 7 | }; 8 | // TODO, use https://sourceware.org/binutils/docs/as/index.html 9 | use crate::opts::{Format, NameDisplay, RedundantLabels, SourcesFrom}; 10 | 11 | mod statements; 12 | 13 | use nom::Parser as _; 14 | use owo_colors::OwoColorize; 15 | use statements::{parse_statement, Loc}; 16 | pub use statements::{Directive, GenericDirective, Instruction, Statement}; 17 | use std::borrow::Cow; 18 | use std::cell::RefCell; 19 | use std::collections::{BTreeMap, BTreeSet, HashMap}; 20 | use std::ops::Range; 21 | use std::path::{Path, PathBuf}; 22 | 23 | type SourceFile = (PathBuf, Option<(Source, CachedLines)>); 24 | 25 | pub fn parse_file(input: &str) -> anyhow::Result> { 26 | // eat all statements until the eof, so we can report the proper errors on failed parse 27 | match nom::multi::many0(parse_statement).parse(input) { 28 | Ok(("", stmts)) => Ok(stmts), 29 | Ok((leftovers, _)) => 30 | { 31 | #[allow(clippy::redundant_else)] 32 | if leftovers.len() < 1000 { 33 | anyhow::bail!("Didn't consume everything, leftovers: {leftovers:?}") 34 | } else { 35 | let head = &leftovers[..leftovers 36 | .char_indices() 37 | .nth(200) 38 | .expect("Shouldn't have that much unicode here...") 39 | .0]; 40 | anyhow::bail!("Didn't consume everything, leftovers prefix: {head:?}"); 41 | } 42 | } 43 | Err(err) => anyhow::bail!("Couldn't parse the .s file: {err}"), 44 | } 45 | } 46 | 47 | #[must_use] 48 | pub fn find_items(lines: &[Statement]) -> BTreeMap> { 49 | let mut res = BTreeMap::new(); 50 | 51 | let mut sec_start = 0; 52 | let mut item: Option = None; 53 | let mut names = BTreeMap::new(); 54 | 55 | for (ix, line) in lines.iter().enumerate() { 56 | if line.is_section_start() { 57 | if item.is_none() { 58 | sec_start = ix; 59 | } else { 60 | // on Windows, when panic unwinding is enabled, the compiler can 61 | // produce multiple blocks of exception-handling code for a 62 | // function, annotated by .seh_* directives (which we ignore). 63 | // For some reason (maybe a bug? or maybe we're misunderstanding 64 | // something?), each of those blocks starts with a .section 65 | // directive identical to the one at the start of the function. 66 | // We have to ignore such duplicates here, otherwise we'd output 67 | // only the last exception-handling block instead of the whole 68 | // function. 69 | // 70 | // See https://github.com/pacak/cargo-show-asm/issues/110 71 | } 72 | } else if line.is_global() && sec_start + 3 < ix { 73 | // On Linux and Windows every global function gets its own section. 74 | // On Mac for some reason this is not the case, so we have to look for 75 | // symbols marked globl within the section. So if we encounter a globl 76 | // deep enough within the current section treat it as a new section start. 77 | // This little hack allows to include full section on Windows/Linux but 78 | // still capture full function body on Mac. 79 | sec_start = ix; 80 | } else if line.is_end_of_fn() { 81 | let sec_end = ix; 82 | let range = sec_start..sec_end; 83 | if let Some(mut item) = item.take() { 84 | item.len = ix - item.len; 85 | item.non_blank_len = item.len; 86 | res.insert(item, range); 87 | } 88 | } else if let Statement::Label(label) = line { 89 | if let Some(dem) = demangle::demangled(label.id) { 90 | let hashed = format!("{dem:?}"); 91 | let name = format!("{dem:#?}"); 92 | let name_entry = names.entry(name.clone()).or_insert(0); 93 | item = Some(Item { 94 | mangled_name: label.id.to_owned(), 95 | name, 96 | hashed, 97 | index: *name_entry, 98 | len: ix, 99 | non_blank_len: 0, 100 | }); 101 | *name_entry += 1; 102 | } else if matches!(label.kind, LabelKind::Unknown | LabelKind::Global) { 103 | if let Some(mut i) = handle_non_mangled_labels(lines, ix, label, sec_start) { 104 | let name_entry = names.entry(i.name.clone()).or_insert(0); 105 | i.index = *name_entry; 106 | item = Some(i); 107 | *name_entry += 1; 108 | } 109 | } 110 | } 111 | } 112 | 113 | // detect merged functions 114 | // we'll define merged function as something with a global label and a reference to a different 115 | // global label 116 | 117 | let globals = lines 118 | .iter() 119 | .enumerate() 120 | .filter_map(|(ix, line)| { 121 | if let Statement::Directive(Directive::Global(name)) = line { 122 | Some((name, ix)) 123 | } else { 124 | None 125 | } 126 | }) 127 | .collect::>(); 128 | 129 | for (end, line) in lines.iter().enumerate() { 130 | let Statement::Directive(Directive::SetValue(name, _)) = line else { 131 | continue; 132 | }; 133 | let Some(start) = globals.get(name).copied() else { 134 | continue; 135 | }; 136 | 137 | // Merged function is different on different system, lol. 138 | // 139 | // Linux: a sequence of 3 items 140 | // 141 | // .globl _ZN13sample_merged3two17h0afab563317f9d7bE 142 | // .type _ZN13sample_merged3two17h0afab563317f9d7bE,@function 143 | // .set _ZN13sample_merged3two17h0afab563317f9d7bE, _ZN13sample_merged12one_plus_one17h408b56cb936d6f10E 144 | // 145 | // MacOS: a sequence of 2 items 146 | // 147 | // .globl _ZN13sample_merged3two17h0afab563317f9d7bE 148 | // .set _ZN13sample_merged3two17h0afab563317f9d7bE, _ZN13sample_merged12one_plus_one17h408b56cb936d6f10E 149 | // 150 | // Windows: a sequence of 6-ish items, different on CI machine LOL 151 | // 152 | // .globl _ZN13sample_merged7two_num17h2372a6fab541fa02E 153 | // .def _ZN13sample_merged7two_num17h2372a6fab541fa02E; 154 | // .scl 2; 155 | // .type 32; 156 | // .endef 157 | // .set _ZN13sample_merged7two_num17h2372a6fab541fa02E, _ZN13sample_merged12one_plus_one17h96e22123e4e22951E 158 | 159 | let range = start..end + 1; 160 | if range.len() > 10 { 161 | // merged function body should contain just a few lines, use 162 | // this as a sanity check 163 | continue; 164 | } 165 | let sym = name; 166 | if let Some(dem) = demangle::demangled(sym) { 167 | let hashed = format!("{dem:?}"); 168 | let name = format!("{dem:#?}"); 169 | let name_entry = names.entry(name.clone()).or_insert(0); 170 | res.insert( 171 | Item { 172 | mangled_name: sym.to_string(), 173 | name, 174 | hashed, 175 | index: *name_entry, 176 | len: range.len(), 177 | non_blank_len: range.len(), 178 | }, 179 | range, 180 | ); 181 | *name_entry += 1; 182 | } 183 | } 184 | 185 | res 186 | } 187 | 188 | /// Handles the non-mangled labels found in the given lines of ASM statements. 189 | /// 190 | /// Returns item if the label is a valid function item, otherwise returns None. 191 | /// NOTE: Does not set `item.index`. 192 | fn handle_non_mangled_labels( 193 | lines: &[Statement], 194 | ix: usize, 195 | label: &Label, 196 | sec_start: usize, 197 | ) -> Option { 198 | match lines.get(sec_start) { 199 | Some(Statement::Directive(Directive::SectionStart(ss))) => { 200 | // The first macOS symbol is found in this section. 201 | // Symbols after this are resolved by matching globl Generic Directive below 202 | // because of the sec_start hack in `find_items`. 203 | const MACOS_TEXT_SECTION: &str = "__TEXT,__text,regular,pure_instructions"; 204 | // Windows symbols each have their own section with this prefix. 205 | const WINDOWS_TEXT_SECTION_PREFIX: &str = ".text,\"xr\",one_only,"; 206 | let is_mac = *ss == MACOS_TEXT_SECTION; 207 | let is_windows = ss.starts_with(WINDOWS_TEXT_SECTION_PREFIX); 208 | if is_windows || is_mac { 209 | // Search for .globl between sec_start and ix 210 | for line in &lines[sec_start..ix] { 211 | if let Statement::Directive(Directive::Global(g)) = line { 212 | // last bool is responsible for stripping leading underscore. 213 | // Stripping is not needed on Linux and 64-bit Windows. 214 | // Currently we want to strip underscore on MacOS 215 | // TODO: on 32-bit Windows we ought to remove underscores 216 | if let Some(item) = get_item_in_section(ix, label, g, is_mac) { 217 | return Some(item); 218 | } 219 | } 220 | } 221 | None 222 | } else { 223 | // Linux symbols each have their own section, named with this prefix. 224 | get_item_in_section(ix, label, ss.strip_prefix(".text.")?, false) 225 | } 226 | } 227 | // Some(Statement::Directive(Directive::Generic(GenericDirective(g)))) => { 228 | // macOS symbols after the first are matched here. 229 | // get_item_in_section(PrefixKind::Global, ix, label, g, true) 230 | // } 231 | Some(Statement::Directive(Directive::Global(g))) => get_item_in_section(ix, label, g, true), 232 | _ => None, 233 | } 234 | } 235 | 236 | /// Checks if the place (ss) starts with the `label`. Place can be either section or .global 237 | /// Creates a new [`Item`], but sets `item.index` to 0. 238 | fn get_item_in_section(ix: usize, label: &Label, ss: &str, strip_underscore: bool) -> Option { 239 | if !ss.starts_with(label.id) { 240 | return None; 241 | } 242 | let name = if strip_underscore && label.id.starts_with('_') { 243 | String::from(&label.id[1..]) 244 | } else { 245 | String::from(label.id) 246 | }; 247 | Some(Item { 248 | mangled_name: label.id.to_owned(), 249 | name: name.clone(), 250 | hashed: name, 251 | index: 0, // Written later in find_items 252 | len: ix, 253 | non_blank_len: 0, 254 | }) 255 | } 256 | 257 | fn used_labels<'a>(stmts: &'_ [Statement<'a>]) -> BTreeSet<&'a str> { 258 | stmts 259 | .iter() 260 | .filter_map(|stmt| match stmt { 261 | Statement::Label(_) | Statement::Nothing => None, 262 | Statement::Directive(dir) => match dir { 263 | Directive::File(_) 264 | | Directive::Loc(_) 265 | | Directive::Global(_) 266 | | Directive::SubsectionsViaSym 267 | | Directive::SymIsFun(_) => None, 268 | Directive::Data(_, val) | Directive::SetValue(_, val) => Some(*val), 269 | Directive::Generic(g) => Some(g.0), 270 | Directive::SectionStart(ss) => Some(*ss), 271 | }, 272 | Statement::Instruction(i) => i.args, 273 | Statement::Dunno(s) => Some(s), 274 | }) 275 | .flat_map(demangle::local_labels) 276 | .collect::>() 277 | } 278 | 279 | /// Scans for referenced constants 280 | fn scan_constant( 281 | name: &str, 282 | sections: &BTreeMap<&str, usize>, 283 | body: &[Statement], 284 | ) -> Option { 285 | let start = *sections.get(name)?; 286 | let end = start 287 | + body[start + 1..] 288 | .iter() 289 | .take_while(|s| matches!(s, Statement::Directive(Directive::Data(_, _)))) 290 | .count() 291 | + 1; 292 | Some(URange { start, end }) 293 | } 294 | 295 | fn dump_range( 296 | files: &BTreeMap, 297 | fmt: &Format, 298 | print_range: Range, 299 | body: &[Statement], // full body 300 | ) -> anyhow::Result<()> { 301 | let print_range = URange::from(print_range); 302 | let mut prev_loc = Loc::default(); 303 | 304 | let stmts = &body[print_range]; 305 | let used = if fmt.redundant_labels == RedundantLabels::Keep { 306 | BTreeSet::new() 307 | } else { 308 | used_labels(stmts) 309 | }; 310 | 311 | let mut empty_line = false; 312 | for (ix, line) in stmts.iter().enumerate() { 313 | if fmt.verbosity > 3 { 314 | safeprintln!("{line:?}"); 315 | } 316 | if let Statement::Directive(Directive::File(_)) = &line { 317 | // do nothing, this directive was used previously to initialize rust sources 318 | } else if let Statement::Directive(Directive::Loc(loc)) = &line { 319 | if !fmt.rust { 320 | continue; 321 | } 322 | if loc.line == 0 { 323 | continue; 324 | } 325 | if loc == &prev_loc { 326 | continue; 327 | } 328 | prev_loc = *loc; 329 | match files.get(&loc.file) { 330 | Some((fname, Some((source, file)))) => { 331 | if source.show_for(fmt.sources_from) { 332 | let rust_line = &file.get(loc.line as usize - 1).expect( 333 | "Corrupted rust-src installation? Try re-adding rust-src component.", 334 | ); 335 | let pos = format!("\t\t// {} : {}", fname.display(), loc.line); 336 | safeprintln!("{}", color!(pos, OwoColorize::cyan)); 337 | safeprintln!( 338 | "\t\t{}", 339 | color!(rust_line.trim_start(), OwoColorize::bright_red) 340 | ); 341 | } 342 | } 343 | Some((fname, None)) => { 344 | if fmt.verbosity > 1 { 345 | safeprintln!( 346 | "\t\t{} {}", 347 | color!("//", OwoColorize::cyan), 348 | color!( 349 | "Can't locate the file, please open a ticket with cargo-show-asm", 350 | OwoColorize::red 351 | ), 352 | ); 353 | } 354 | let pos = format!("\t\t// {} : {}", fname.display(), loc.line); 355 | safeprintln!("{}", color!(pos, OwoColorize::cyan)); 356 | } 357 | None => { 358 | panic!("DWARF file refers to an undefined location {loc:?}"); 359 | } 360 | } 361 | empty_line = false; 362 | } else if let Statement::Label(Label { 363 | kind: kind @ (LabelKind::Local | LabelKind::Temp), 364 | id, 365 | }) = line 366 | { 367 | match fmt.redundant_labels { 368 | // We always include used labels and labels at the very 369 | // beginning of the fragment - those are used for data declarations 370 | _ if ix == 0 || used.contains(id) => { 371 | safeprintln!("{line}"); 372 | } 373 | RedundantLabels::Keep => { 374 | safeprintln!("{line}"); 375 | } 376 | RedundantLabels::Blanks => { 377 | if !empty_line && *kind != LabelKind::Temp { 378 | safeprintln!(); 379 | empty_line = true; 380 | } 381 | } 382 | RedundantLabels::Strip => {} 383 | } 384 | } else { 385 | if fmt.simplify && line.boring() { 386 | continue; 387 | } 388 | 389 | empty_line = false; 390 | match fmt.name_display { 391 | NameDisplay::Full => safeprintln!("{line:#}"), 392 | NameDisplay::Short => safeprintln!("{line}"), 393 | NameDisplay::Mangled => safeprintln!("{line:-}"), 394 | } 395 | } 396 | } 397 | 398 | Ok(()) 399 | } 400 | 401 | #[derive(Debug, Clone)] 402 | pub enum Source { 403 | Crate, 404 | External, 405 | Stdlib, 406 | Rustc, 407 | } 408 | 409 | impl Source { 410 | fn show_for(&self, from: SourcesFrom) -> bool { 411 | match self { 412 | Source::Crate => true, 413 | Source::External => match from { 414 | SourcesFrom::ThisWorkspace => false, 415 | SourcesFrom::AllCrates | SourcesFrom::AllSources => true, 416 | }, 417 | Source::Rustc | Source::Stdlib => match from { 418 | SourcesFrom::ThisWorkspace | SourcesFrom::AllCrates => false, 419 | SourcesFrom::AllSources => true, 420 | }, 421 | } 422 | } 423 | } 424 | 425 | // DWARF information contains references to source files 426 | // It can point to 3 different items: 427 | // 1. a real file, cargo-show-asm can just read it 428 | // 2. a file from rustlib, sources are under $sysroot/lib/rustlib/src/rust/$suffix 429 | // Some examples: 430 | // /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/iter/range.rs 431 | // /private/tmp/rust-20230325-7327-rbrpyq/rustc-1.68.1-src/library/core/src/option.rs 432 | // /rustc/cc66ad468955717ab92600c770da8c1601a4ff33\\library\\core\\src\\convert\\mod.rs 433 | // 3. a file from prebuilt (?) hashbrown, sources are probably available under 434 | // cargo registry, most likely under ~/.cargo/registry/$suffix 435 | // Some examples: 436 | // /cargo/registry/src/github.com-1ecc6299db9ec823/hashbrown-0.12.3/src/raw/bitmask.rs 437 | // /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/hashbrown-0.12.3/src/map.rs 438 | // 4. rustc sources: 439 | // /rustc/89e2160c4ca5808657ed55392620ed1dbbce78d1/compiler/rustc_span/src/span_encoding.rs 440 | // $sysroot/lib/rustlib/rust-src/rust/compiler/rustc_span/src/span_encoding.rs 441 | fn locate_sources(sysroot: &Path, workspace: &Path, path: &Path) -> Option<(Source, PathBuf)> { 442 | let mut path = Cow::Borrowed(path); 443 | // a real file that simply exists 444 | if path.exists() { 445 | let source = if path.starts_with(workspace) { 446 | Source::Crate 447 | } else { 448 | Source::External 449 | }; 450 | 451 | return Some((source, path.into())); 452 | } 453 | 454 | let no_rust_src = || { 455 | esafeprintln!( 456 | "You need to install rustc sources to be able to see the rust annotations, try\n\ 457 | \trustup component add rust-src" 458 | ); 459 | std::process::exit(1); 460 | }; 461 | 462 | // then during crosscompilation we can get this cursed mix of path names 463 | // 464 | // /rustc/cc66ad468955717ab92600c770da8c1601a4ff33\\library\\core\\src\\convert\\mod.rs 465 | // 466 | // where one bit comes from the host platform and second bit comes from the target platform 467 | // This feels like a problem in upstream, but supporting that is not _that_ hard. 468 | // 469 | // I think this should take care of Linux and MacOS support 470 | if (path.starts_with("/rustc/") || path.starts_with("/private/tmp")) 471 | && path 472 | .as_os_str() 473 | .to_str() 474 | .is_some_and(|s| s.contains("\\") && s.contains("/")) 475 | { 476 | let cursed_path = path 477 | .as_os_str() 478 | .to_str() 479 | .expect("They are coming from a text file"); 480 | path = Cow::Owned(PathBuf::from(cursed_path.replace("\\", "/"))); 481 | } 482 | 483 | // /rustc/89e2160c4ca5808657ed55392620ed1dbbce78d1/compiler/rustc_span/src/span_encoding.rs 484 | if path.starts_with("/rustc") && path.iter().any(|c| c == "compiler") { 485 | let mut source = sysroot.join("lib/rustlib/rustc-src/rust"); 486 | for part in path.components().skip(3) { 487 | source.push(part); 488 | } 489 | 490 | if source.exists() { 491 | return Some((Source::Rustc, source)); 492 | } else { 493 | no_rust_src(); 494 | } 495 | } 496 | 497 | // rust sources, Linux style 498 | if path.starts_with("/rustc/") { 499 | let mut source = sysroot.join("lib/rustlib/src/rust"); 500 | for part in path.components().skip(3) { 501 | source.push(part); 502 | } 503 | if source.exists() { 504 | return Some((Source::Stdlib, source)); 505 | } else { 506 | no_rust_src(); 507 | } 508 | } 509 | 510 | // rust sources, MacOS style 511 | if path.starts_with("/private/tmp") && path.components().any(|c| c.as_os_str() == "library") { 512 | let mut source = sysroot.join("lib/rustlib/src/rust"); 513 | for part in path.components().skip(5) { 514 | source.push(part); 515 | } 516 | if source.exists() { 517 | return Some((Source::Stdlib, source)); 518 | } else { 519 | no_rust_src(); 520 | } 521 | } 522 | 523 | // cargo registry, Linux and macOS look for cargo/registry and .cargo/registry 524 | if let Some(ix) = path 525 | .components() 526 | .position(|c| c.as_os_str() == "cargo" || c.as_os_str() == ".cargo") 527 | .and_then(|ix| path.components().nth(ix).zip(Some(ix))) 528 | .and_then(|(c, ix)| (c.as_os_str() == "registry").then_some(ix)) 529 | { 530 | // It does what I want as far as *nix is concerned, might not work for Windows... 531 | #[allow(deprecated)] 532 | let mut source = std::env::home_dir().expect("No home dir?"); 533 | 534 | source.push(".cargo"); 535 | for part in path.components().skip(ix) { 536 | source.push(part); 537 | } 538 | if source.exists() { 539 | return Some((Source::External, source)); 540 | } else { 541 | panic!( 542 | "{path:?} looks like it can be a cargo registry reference but we failed to get it" 543 | ); 544 | } 545 | } 546 | 547 | None 548 | } 549 | 550 | fn load_rust_sources( 551 | sysroot: &Path, 552 | workspace: &Path, 553 | statements: &[Statement], 554 | fmt: &Format, 555 | files: &mut BTreeMap, 556 | ) { 557 | for line in statements { 558 | if let Statement::Directive(Directive::File(f)) = line { 559 | files.entry(f.index).or_insert_with(|| { 560 | let path = f.path.as_full_path().into_owned(); 561 | if fmt.verbosity > 2 { 562 | safeprintln!("Reading file #{} {}", f.index, path.display()); 563 | } 564 | 565 | if let Some((source, filepath)) = locate_sources(sysroot, workspace, &path) { 566 | if fmt.verbosity > 3 { 567 | safeprintln!("Resolved name is {filepath:?}"); 568 | } 569 | let sources = std::fs::read_to_string(&filepath).expect("Can't read a file"); 570 | if sources.is_empty() { 571 | if fmt.verbosity > 0 { 572 | safeprintln!("Ignoring empty file {filepath:?}!"); 573 | } 574 | (path, None) 575 | } else { 576 | if fmt.verbosity > 3 { 577 | safeprintln!("Got {} bytes", sources.len()); 578 | } 579 | let lines = CachedLines::without_ending(sources); 580 | (path, Some((source, lines))) 581 | } 582 | } else { 583 | if fmt.verbosity > 1 { 584 | safeprintln!("File not found {}", path.display()); 585 | } 586 | (path, None) 587 | } 588 | }); 589 | } 590 | } 591 | } 592 | 593 | impl RawLines for Statement<'_> { 594 | fn lines(&self) -> Option<&str> { 595 | match self { 596 | Statement::Instruction(i) => i.args, 597 | Statement::Directive(Directive::SetValue(_, i)) => Some(i), 598 | _ => None, 599 | } 600 | } 601 | } 602 | 603 | pub struct Asm<'a> { 604 | workspace: &'a Path, 605 | sysroot: &'a Path, 606 | sources: RefCell>, 607 | } 608 | 609 | impl<'a> Asm<'a> { 610 | pub fn new(workspace: &'a Path, sysroot: &'a Path) -> Self { 611 | Self { 612 | workspace, 613 | sysroot, 614 | sources: Default::default(), 615 | } 616 | } 617 | } 618 | 619 | impl Dumpable for Asm<'_> { 620 | type Line<'l> = Statement<'l>; 621 | 622 | fn split_lines(contents: &str) -> anyhow::Result>> { 623 | parse_file(contents) 624 | } 625 | 626 | fn find_items(lines: &[Self::Line<'_>]) -> BTreeMap> { 627 | find_items(lines) 628 | } 629 | 630 | fn dump_range(&self, fmt: &Format, lines: &[Self::Line<'_>]) -> anyhow::Result<()> { 631 | dump_range(&self.sources.borrow(), fmt, 0..lines.len(), lines) 632 | } 633 | 634 | fn extra_context( 635 | &self, 636 | fmt: &Format, 637 | lines: &[Self::Line<'_>], 638 | range: Range, 639 | items: &BTreeMap>, 640 | ) -> Vec> { 641 | let mut res = get_context_for(fmt.context, lines, range.clone(), items); 642 | if fmt.rust { 643 | load_rust_sources( 644 | self.sysroot, 645 | self.workspace, 646 | lines, 647 | fmt, 648 | &mut self.sources.borrow_mut(), 649 | ); 650 | } 651 | 652 | if fmt.include_constants { 653 | let print_range = URange::from(range.clone()); 654 | // scan for referenced constants such as strings, scan needs to be done recursively 655 | let mut pending = vec![print_range]; 656 | let mut seen: BTreeSet = BTreeSet::new(); 657 | 658 | // Let's define a constant as a label followed by one or more data declarations 659 | let constants = lines 660 | .iter() 661 | .enumerate() 662 | .filter_map(|(ix, stmt)| { 663 | let Statement::Label(Label { id, .. }) = stmt else { 664 | return None; 665 | }; 666 | matches!( 667 | lines.get(ix + 1), 668 | Some(Statement::Directive(Directive::Data(_, _))) 669 | ) 670 | .then_some((*id, ix)) 671 | }) 672 | .collect::>(); 673 | while let Some(subset) = pending.pop() { 674 | seen.insert(subset); 675 | for s in &lines[subset] { 676 | if let Statement::Instruction(Instruction { 677 | args: Some(arg), .. 678 | }) 679 | | Statement::Directive(Directive::Generic(GenericDirective(arg))) = s 680 | { 681 | for label in crate::demangle::local_labels(arg) { 682 | if let Some(constant_range) = scan_constant(label, &constants, lines) { 683 | if !seen.contains(&constant_range) 684 | && !print_range.fully_contains(constant_range) 685 | { 686 | pending.push(constant_range); 687 | } 688 | } 689 | } 690 | } 691 | } 692 | } 693 | seen.remove(&print_range); 694 | for range in &seen { 695 | res.push(range.start..range.end); 696 | } 697 | } 698 | 699 | if fmt.simplify { 700 | res.retain(|range| { 701 | lines[range.start..range.end] 702 | .iter() 703 | .any(|s| !(s.boring() || matches!(s, Statement::Nothing | Statement::Label(_)))) 704 | }); 705 | } 706 | 707 | res 708 | } 709 | } 710 | -------------------------------------------------------------------------------- /src/asm/statements.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::path::Path; 3 | use std::sync::OnceLock; 4 | 5 | use nom::branch::alt; 6 | use nom::bytes::complete::{escaped_transform, tag, take_while1, take_while_m_n}; 7 | use nom::character::complete::{self, newline, none_of, not_line_ending, one_of, space0, space1}; 8 | use nom::combinator::{map, opt, recognize, value, verify}; 9 | use nom::multi::count; 10 | use nom::sequence::{delimited, pair, preceded, terminated}; 11 | use nom::{AsChar, IResult, Parser as _}; 12 | use owo_colors::OwoColorize; 13 | use regex::Regex; 14 | 15 | use crate::demangle::LabelKind; 16 | use crate::opts::NameDisplay; 17 | use crate::{color, demangle}; 18 | 19 | #[derive(Clone, Debug, Eq, PartialEq)] 20 | pub enum Statement<'a> { 21 | Label(Label<'a>), 22 | Directive(Directive<'a>), 23 | Instruction(Instruction<'a>), 24 | Nothing, 25 | Dunno(&'a str), 26 | } 27 | 28 | #[derive(Clone, Debug, Eq, PartialEq)] 29 | pub struct Instruction<'a> { 30 | pub op: &'a str, 31 | pub args: Option<&'a str>, 32 | } 33 | 34 | impl<'a> Instruction<'a> { 35 | pub fn parse(input: &'a str) -> IResult<&'a str, Self> { 36 | preceded(tag("\t"), alt((Self::parse_regular, Self::parse_sharp))).parse(input) 37 | } 38 | 39 | fn parse_sharp(input: &'a str) -> IResult<&'a str, Self> { 40 | let sharps = take_while_m_n(1, 2, |c| c == '#'); 41 | let sharp_tag = pair(sharps, not_line_ending); 42 | map(recognize(sharp_tag), |op| Instruction { op, args: None }).parse(input) 43 | } 44 | 45 | fn parse_regular(input: &'a str) -> IResult<&'a str, Self> { 46 | // NOTE: ARM allows `.` inside instruction names e.g. `b.ne` for branch not equal 47 | // Wasm also uses `.` in instr names, and uses `_` for `end_function` 48 | let op = take_while1(|c| AsChar::is_alphanum(c) || matches!(c, '.' | '_')); 49 | let args = opt(preceded(space1, not_line_ending)); 50 | map(pair(op, args), |(op, args)| Instruction { op, args }).parse(input) 51 | } 52 | } 53 | 54 | fn parse_data_dec(input: &str) -> IResult<&str, Directive> { 55 | static DATA_DEC: OnceLock = OnceLock::new(); 56 | // all of those can insert something as well... Not sure if it's a full list or not 57 | // .long, .short .octa, .quad, .word, 58 | // .single .double .float 59 | // .ascii, .asciz, .string, .string8 .string16 .string32 .string64 60 | // .2byte .4byte .8byte 61 | // .dc 62 | // .inst .insn 63 | let reg = DATA_DEC.get_or_init(|| { 64 | // regexp is inspired by the compiler explorer 65 | Regex::new( 66 | "^\\s*\\.(ascii|asciz|[1248]?byte|dc(?:\\.[abdlswx])?|dcb(?:\\.[bdlswx])?\ 67 | |ds(?:\\.[bdlpswx])?|double|dword|fill|float|half|hword|int|long|octa|quad|\ 68 | short|single|skip|space|string(?:8|16|32|64)?|value|word|xword|zero)\\s+([^\\n]+)", 69 | ) 70 | .expect("regexp should be valid") 71 | }); 72 | 73 | let Some(cap) = reg.captures(input) else { 74 | use nom::error::*; 75 | return Err(nom::Err::Error(Error::new(input, ErrorKind::Eof))); 76 | }; 77 | let (Some(instr), Some(data)) = (cap.get(1), cap.get(2)) else { 78 | panic!("regexp should be valid and capture found something"); 79 | }; 80 | Ok(( 81 | &input[data.range().end..], 82 | Directive::Data(instr.as_str(), data.as_str()), 83 | )) 84 | } 85 | 86 | impl Statement<'_> { 87 | /// Should we skip it for --simplify output? 88 | pub fn boring(&self) -> bool { 89 | if let Statement::Directive(Directive::SetValue(_, _)) = self { 90 | return false; 91 | } 92 | if let Statement::Directive(Directive::SectionStart(name)) = self { 93 | if name.starts_with(".data") || name.starts_with(".rodata") { 94 | return false; 95 | } 96 | } 97 | matches!(self, Statement::Directive(_) | Statement::Dunno(_)) 98 | } 99 | } 100 | 101 | impl std::fmt::Display for Instruction<'_> { 102 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 103 | let display = NameDisplay::from(&*f); 104 | if self.op.starts_with("#DEBUG_VALUE:") { 105 | write!(f, "{}", color!(self.op, OwoColorize::blue))?; 106 | } else { 107 | write!(f, "{}", color!(self.op, OwoColorize::bright_blue))?; 108 | } 109 | if let Some(args) = self.args { 110 | let args = demangle::contents(args, display); 111 | let w_label = demangle::color_local_labels(&args); 112 | write!(f, " {w_label}")?; 113 | } 114 | Ok(()) 115 | } 116 | } 117 | 118 | impl std::fmt::Display for Statement<'_> { 119 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 120 | match self { 121 | Statement::Label(l) => l.fmt(f), 122 | Statement::Directive(d) => { 123 | if f.alternate() { 124 | write!(f, "{d:#}") 125 | } else { 126 | write!(f, "{d}") 127 | } 128 | } 129 | Statement::Instruction(i) => { 130 | if f.sign_minus() { 131 | write!(f, "\t{i:-#}") 132 | } else if f.alternate() { 133 | write!(f, "\t{i:#}") 134 | } else { 135 | write!(f, "\t{i}") 136 | } 137 | } 138 | Statement::Nothing => Ok(()), 139 | Statement::Dunno(l) => write!(f, "{l}"), 140 | } 141 | } 142 | } 143 | 144 | impl std::fmt::Display for Directive<'_> { 145 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 146 | let display = NameDisplay::from(&*f); 147 | match self { 148 | Directive::File(ff) => ff.fmt(f), 149 | Directive::Loc(l) => l.fmt(f), 150 | Directive::Generic(g) => g.fmt(f), 151 | Directive::SetValue(key, val) => { 152 | let key = demangle::contents(key, display); 153 | let val = demangle::contents(val, display); 154 | write!( 155 | f, 156 | ".{} {}, {}", 157 | color!("set", OwoColorize::bright_magenta), 158 | color!(key, OwoColorize::bright_cyan), 159 | color!(val, OwoColorize::bright_cyan) 160 | ) 161 | } 162 | Directive::SectionStart(s) => { 163 | let dem = demangle::contents(s, display); 164 | write!(f, "{} {dem}", color!(".section", OwoColorize::bright_red)) 165 | } 166 | Directive::SubsectionsViaSym => write!( 167 | f, 168 | ".{}", 169 | color!("subsections_via_symbols", OwoColorize::bright_red) 170 | ), 171 | Directive::SymIsFun(s) => { 172 | let dem = demangle::contents(s, display); 173 | write!( 174 | f, 175 | ".{}\t{dem},@function", 176 | color!("type", OwoColorize::bright_magenta) 177 | ) 178 | } 179 | Directive::Data(ty, data) => { 180 | let data = demangle::contents(data, display); 181 | let w_label = demangle::color_local_labels(&data); 182 | write!( 183 | f, 184 | "\t.{}\t{}", 185 | color!(ty, OwoColorize::bright_magenta), 186 | color!(w_label, OwoColorize::bright_cyan) 187 | ) 188 | } 189 | Directive::Global(data) => { 190 | let data = demangle::contents(data, display); 191 | let w_label = demangle::color_local_labels(&data); 192 | write!( 193 | f, 194 | "\t.{}\t{}", 195 | color!("globl", OwoColorize::bright_magenta), 196 | color!(w_label, OwoColorize::bright_cyan) 197 | ) 198 | } 199 | } 200 | } 201 | } 202 | 203 | impl std::fmt::Display for FilePath { 204 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 205 | std::fmt::Display::fmt(&self.as_full_path().display(), f) 206 | } 207 | } 208 | 209 | impl std::fmt::Display for File<'_> { 210 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 211 | write!(f, "\t.file\t{} {}", self.index, self.path)?; 212 | if let Some(md5) = self.md5 { 213 | write!(f, " {md5}")?; 214 | } 215 | Ok(()) 216 | } 217 | } 218 | 219 | impl std::fmt::Display for GenericDirective<'_> { 220 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 221 | let display = NameDisplay::from(&*f); 222 | write!( 223 | f, 224 | "\t.{}", 225 | color!( 226 | demangle::contents(self.0, display), 227 | OwoColorize::bright_magenta 228 | ) 229 | ) 230 | } 231 | } 232 | 233 | impl std::fmt::Display for Loc<'_> { 234 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 235 | match self.extra { 236 | Some(x) => write!( 237 | f, 238 | "\t.loc\t{file} {line} {col} {x}", 239 | file = self.file, 240 | line = self.line, 241 | col = self.column, 242 | ), 243 | None => write!( 244 | f, 245 | "\t.loc\t{file} {line} {col}", 246 | file = self.file, 247 | line = self.line, 248 | col = self.column 249 | ), 250 | } 251 | } 252 | } 253 | 254 | impl From<&std::fmt::Formatter<'_>> for NameDisplay { 255 | fn from(f: &std::fmt::Formatter) -> Self { 256 | if f.sign_minus() { 257 | NameDisplay::Mangled 258 | } else if f.alternate() { 259 | NameDisplay::Full 260 | } else { 261 | NameDisplay::Short 262 | } 263 | } 264 | } 265 | 266 | impl std::fmt::Display for Label<'_> { 267 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 268 | let display = NameDisplay::from(&*f); 269 | write!( 270 | f, 271 | "{}:", 272 | color!( 273 | demangle::contents(self.id, display), 274 | OwoColorize::bright_yellow 275 | ) 276 | ) 277 | } 278 | } 279 | 280 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 281 | pub struct Label<'a> { 282 | pub id: &'a str, 283 | pub kind: LabelKind, 284 | } 285 | 286 | impl<'a> Label<'a> { 287 | pub fn parse(input: &'a str) -> IResult<&'a str, Self> { 288 | // TODO: label can't start with a digit 289 | let no_comment = tag(":"); 290 | let comment = terminated( 291 | tag(":"), 292 | ( 293 | take_while1(|c| c == ' '), 294 | tag("# @"), 295 | take_while1(|c| c != '\n'), 296 | ), 297 | ); 298 | map( 299 | terminated(take_while1(good_for_label), alt((comment, no_comment))), 300 | |id: &str| Label { 301 | id, 302 | kind: demangle::label_kind(id), 303 | }, 304 | ) 305 | .parse(input) 306 | } 307 | } 308 | 309 | #[derive(Copy, Clone, Debug, Eq, Default)] 310 | pub struct Loc<'a> { 311 | pub file: u64, 312 | pub line: u64, 313 | pub column: u64, 314 | pub extra: Option<&'a str>, 315 | } 316 | 317 | impl PartialEq for Loc<'_> { 318 | fn eq(&self, other: &Self) -> bool { 319 | self.file == other.file && self.line == other.line 320 | } 321 | } 322 | 323 | impl<'a> Loc<'a> { 324 | pub fn parse(input: &'a str) -> IResult<&'a str, Self> { 325 | // DWARF2 (Unix): .loc fileno lineno [column] [options] 326 | // CodeView (Windows): .cv_loc functionid fileno lineno [column] [prologue_end] [is_stmt value] 327 | map( 328 | ( 329 | alt(( 330 | tag("\t.loc\t"), 331 | terminated(tag("\t.cv_loc\t"), (complete::u64, space1)), 332 | )), 333 | complete::u64, 334 | space1, 335 | complete::u64, 336 | space1, 337 | complete::u64, 338 | opt(preceded(tag(" "), take_while1(|c| c != '\n'))), 339 | ), 340 | |(_, file, _, line, _, column, extra)| Loc { 341 | file, 342 | line, 343 | column, 344 | extra, 345 | }, 346 | ) 347 | .parse(input) 348 | } 349 | } 350 | 351 | #[derive(Clone, Debug, PartialEq, Eq)] 352 | pub enum FilePath { 353 | FullPath(String), 354 | PathAndFileName { path: String, name: String }, 355 | } 356 | 357 | impl FilePath { 358 | pub fn as_full_path(&self) -> Cow<'_, Path> { 359 | match self { 360 | FilePath::FullPath(path) => Cow::Borrowed(Path::new(path)), 361 | FilePath::PathAndFileName { path, name } => Cow::Owned(Path::new(path).join(name)), 362 | } 363 | } 364 | } 365 | 366 | #[derive(Clone, Debug, PartialEq, Eq)] 367 | pub struct File<'a> { 368 | pub index: u64, 369 | pub path: FilePath, 370 | pub md5: Option<&'a str>, 371 | } 372 | 373 | fn parse_quoted_string(input: &str) -> IResult<&str, String> { 374 | // Inverse of MCAsmStreamer::PrintQuotedString() in MCAsmStreamer.cpp in llvm. 375 | delimited( 376 | tag("\""), 377 | escaped_transform( 378 | none_of("\\\""), 379 | '\\', 380 | alt(( 381 | value('\\', tag("\\")), 382 | value('\"', tag("\"")), 383 | value('\x08', tag("b")), 384 | value('\x0c', tag("f")), 385 | value('\n', tag("n")), 386 | value('\r', tag("r")), 387 | value('\t', tag("t")), 388 | // 3 digits in base 8 389 | map(count(one_of("01234567"), 3), |digits| { 390 | let mut v = 0u8; 391 | for c in digits { 392 | v = (v << 3) | c.to_digit(8).unwrap() as u8; 393 | } 394 | char::from(v) 395 | }), 396 | )), 397 | ), 398 | tag("\""), 399 | ) 400 | .parse(input) 401 | } 402 | 403 | // Workaround for a problem in llvm code that produces debug symbols on Windows. 404 | // As of the time of writing, CodeViewDebug::getFullFilepath() in CodeViewDebug.cpp 405 | // replaces all occurrences of "\\" with "\". 406 | // This breaks paths that start with "\\?\" (a prefix instructing Windows to skip 407 | // filename parsing) - they turn into "\?\", which is invalid. 408 | // Here we turn "\?\" back into "\\?\". 409 | // Hopefully this will get fixed in llvm, and we'll remove this. 410 | fn fixup_windows_file_path(mut p: String) -> String { 411 | if p.starts_with("\\?\\") { 412 | p.insert(0, '\\'); 413 | } 414 | p 415 | } 416 | 417 | impl<'a> File<'a> { 418 | pub fn parse(input: &'a str) -> IResult<&'a str, Self> { 419 | // DWARF2/DWARF5 (Unix): .file fileno [dirname] "filename" [md5] 420 | // CodeView (Windows): .cv_file fileno "filename" ["checksum"] [checksumkind] 421 | alt(( 422 | map( 423 | ( 424 | tag("\t.file\t"), 425 | complete::u64, 426 | space1, 427 | parse_quoted_string, 428 | opt(preceded(space1, parse_quoted_string)), 429 | opt(preceded(space1, complete::hex_digit1)), 430 | ), 431 | |(_, fileno, _, filepath, filename, md5)| File { 432 | index: fileno, 433 | path: match filename { 434 | Some(filename) => FilePath::PathAndFileName { 435 | path: filepath, 436 | name: filename, 437 | }, 438 | None => FilePath::FullPath(filepath), 439 | }, 440 | md5, 441 | }, 442 | ), 443 | map( 444 | ( 445 | tag("\t.cv_file\t"), 446 | complete::u64, 447 | space1, 448 | parse_quoted_string, 449 | opt(preceded( 450 | space1, 451 | delimited(tag("\""), complete::hex_digit1, tag("\"")), 452 | )), 453 | opt(preceded(space1, complete::u64)), 454 | ), 455 | |(_, fileno, _, filename, checksum, checksumkind)| File { 456 | index: fileno, 457 | path: FilePath::FullPath(fixup_windows_file_path(filename)), 458 | // FileChecksumKind enum: { None, MD5, SHA1, SHA256 } 459 | // (from llvm's CodeView.h) 460 | md5: if checksumkind == Some(1) { 461 | checksum 462 | } else { 463 | None 464 | }, 465 | }, 466 | ), 467 | )) 468 | .parse(input) 469 | } 470 | } 471 | 472 | #[test] 473 | fn test_parse_label() { 474 | assert_eq!( 475 | Label::parse("GCC_except_table0:"), 476 | Ok(( 477 | "", 478 | Label { 479 | id: "GCC_except_table0", 480 | kind: LabelKind::Unknown, 481 | } 482 | )) 483 | ); 484 | assert_eq!( 485 | Label::parse("__ZN4core3ptr50drop_in_place$LT$rand..rngs..thread..ThreadRng$GT$17hba90ed09529257ccE:"), 486 | Ok(( 487 | "", 488 | Label { 489 | id: "__ZN4core3ptr50drop_in_place$LT$rand..rngs..thread..ThreadRng$GT$17hba90ed09529257ccE", 490 | kind: LabelKind::Global, 491 | } 492 | )) 493 | ); 494 | assert_eq!( 495 | Label::parse(".Lexception0:"), 496 | Ok(( 497 | "", 498 | Label { 499 | id: ".Lexception0", 500 | kind: LabelKind::Local 501 | } 502 | )) 503 | ); 504 | assert_eq!( 505 | Label::parse("LBB0_1:"), 506 | Ok(( 507 | "", 508 | Label { 509 | id: "LBB0_1", 510 | kind: LabelKind::Local 511 | } 512 | )) 513 | ); 514 | assert_eq!( 515 | Label::parse("Ltmp12:"), 516 | Ok(( 517 | "", 518 | Label { 519 | id: "Ltmp12", 520 | kind: LabelKind::Temp 521 | } 522 | )) 523 | ); 524 | assert_eq!( 525 | Label::parse("__ZN4core3ptr50drop_in_place$LT$rand..rngs..thread..ThreadRng$GT$17hba90ed09529257ccE: # @\"rand\""), 526 | Ok(( 527 | "", 528 | Label { 529 | id: "__ZN4core3ptr50drop_in_place$LT$rand..rngs..thread..ThreadRng$GT$17hba90ed09529257ccE", 530 | kind: LabelKind::Global, 531 | } 532 | )) 533 | ); 534 | assert_eq!( 535 | Label::parse("_ZN44_$LT$$RF$T$u20$as$u20$core..fmt..Display$GT$3fmt17h6557947cc19e5571E: # @\"_ZN44_$LT$$RF$T$u20$as$u20$core..fmt..Display$GT$3fmt17h6557947cc19e5571E\""), 536 | Ok(( 537 | "", 538 | Label { 539 | id: "_ZN44_$LT$$RF$T$u20$as$u20$core..fmt..Display$GT$3fmt17h6557947cc19e5571E", 540 | kind: LabelKind::Global, 541 | } 542 | )) 543 | ); 544 | assert_eq!( 545 | Label::parse( 546 | "_ZN6sample4main17hb59e25bba3071c26E: # @_ZN6sample4main17hb59e25bba3071c26E" 547 | ), 548 | Ok(( 549 | "", 550 | Label { 551 | id: "_ZN6sample4main17hb59e25bba3071c26E", 552 | kind: LabelKind::Global, 553 | } 554 | )) 555 | ); 556 | } 557 | 558 | #[test] 559 | fn test_parse_loc() { 560 | assert_eq!( 561 | Loc::parse("\t.loc\t31 26 29"), 562 | Ok(( 563 | "", 564 | Loc { 565 | file: 31, 566 | line: 26, 567 | column: 29, 568 | extra: None 569 | } 570 | )) 571 | ); 572 | assert_eq!( 573 | Loc::parse("\t.loc\t31 26 29 is_stmt 0"), 574 | Ok(( 575 | "", 576 | Loc { 577 | file: 31, 578 | line: 26, 579 | column: 29, 580 | extra: Some("is_stmt 0") 581 | } 582 | )) 583 | ); 584 | assert_eq!( 585 | Loc::parse("\t.loc\t31 26 29 prologue_end"), 586 | Ok(( 587 | "", 588 | Loc { 589 | file: 31, 590 | line: 26, 591 | column: 29, 592 | extra: Some("prologue_end") 593 | } 594 | )) 595 | ); 596 | assert_eq!( 597 | Loc::parse("\t.cv_loc\t9 6 1 0"), 598 | Ok(( 599 | "", 600 | Loc { 601 | file: 6, 602 | line: 1, 603 | column: 0, 604 | extra: None, 605 | } 606 | )) 607 | ); 608 | assert_eq!( 609 | Loc::parse("\t.cv_loc\t9 6 1 0 rest of the line is ignored"), 610 | Ok(( 611 | "", 612 | Loc { 613 | file: 6, 614 | line: 1, 615 | column: 0, 616 | extra: Some("rest of the line is ignored"), 617 | } 618 | )) 619 | ); 620 | } 621 | 622 | #[test] 623 | fn test_parse_file() { 624 | let (rest, file) = File::parse("\t.file\t9 \"/home/ubuntu/buf-test/src/main.rs\"").unwrap(); 625 | assert!(rest.is_empty()); 626 | assert_eq!( 627 | file, 628 | File { 629 | index: 9, 630 | path: FilePath::FullPath("/home/ubuntu/buf-test/src/main.rs".to_owned()), 631 | md5: None 632 | } 633 | ); 634 | assert_eq!( 635 | file.path.as_full_path(), 636 | Path::new("/home/ubuntu/buf-test/src/main.rs") 637 | ); 638 | 639 | let (rest, file) = File::parse("\t.file\t9 \"/home/ubuntu/buf-test\" \"src/main.rs\"").unwrap(); 640 | assert!(rest.is_empty()); 641 | assert_eq!( 642 | file, 643 | File { 644 | index: 9, 645 | path: FilePath::PathAndFileName { 646 | path: "/home/ubuntu/buf-test".to_owned(), 647 | name: "src/main.rs".to_owned() 648 | }, 649 | md5: None, 650 | } 651 | ); 652 | assert_eq!( 653 | file.path.as_full_path(), 654 | Path::new("/home/ubuntu/buf-test/src/main.rs") 655 | ); 656 | 657 | let (rest, file) = File::parse( 658 | "\t.file\t9 \"/home/ubuntu/buf-test\" \"src/main.rs\" 74ab618651b843a815bf806bd6c50c19", 659 | ) 660 | .unwrap(); 661 | assert!(rest.is_empty()); 662 | assert_eq!( 663 | file, 664 | File { 665 | index: 9, 666 | path: FilePath::PathAndFileName { 667 | path: "/home/ubuntu/buf-test".to_owned(), 668 | name: "src/main.rs".to_owned() 669 | }, 670 | md5: Some("74ab618651b843a815bf806bd6c50c19"), 671 | } 672 | ); 673 | assert_eq!( 674 | file.path.as_full_path(), 675 | Path::new("/home/ubuntu/buf-test/src/main.rs") 676 | ); 677 | 678 | let (rest, file) = File::parse( 679 | "\t.file\t9 \"/home/\\000path\\twith\\nlots\\\"of\\runprintable\\147characters\\blike\\\\this\\f\" \"src/main.rs\" 74ab618651b843a815bf806bd6c50c19", 680 | ) 681 | .unwrap(); 682 | assert!(rest.is_empty()); 683 | assert_eq!( 684 | file, 685 | File { 686 | index: 9, 687 | path: FilePath::PathAndFileName { 688 | path: "/home/\x00path\twith\nlots\"of\runprintable\x67characters\x08like\\this\x0c" 689 | .to_owned(), 690 | name: "src/main.rs".to_owned() 691 | }, 692 | md5: Some("74ab618651b843a815bf806bd6c50c19"), 693 | } 694 | ); 695 | assert_eq!( 696 | file.path.as_full_path(), 697 | Path::new("/home/\x00path\twith\nlots\"of\runprintable\x67characters\x08like\\this\x0c/src/main.rs") 698 | ); 699 | 700 | let (rest, file) = File::parse( 701 | "\t.cv_file\t6 \"\\\\?\\\\C:\\\\Foo\\\\Bar\\\\src\\\\main.rs\" \"778FECDE2D48F9B948BA07E6E0B4AB983123B71B\" 2", 702 | ) 703 | .unwrap(); 704 | assert!(rest.is_empty()); 705 | assert_eq!( 706 | file, 707 | File { 708 | index: 6, 709 | path: FilePath::FullPath("\\\\?\\C:\\Foo\\Bar\\src\\main.rs".to_owned()), 710 | md5: None, 711 | } 712 | ); 713 | 714 | let (rest, file) = File::parse( 715 | "\t.cv_file\t6 \"C:\\\\Foo\\\\Bar\\\\src\\\\main.rs\" \"778FECDE2D48F9B948BA07E6E0B4AB98\" 1", 716 | ) 717 | .unwrap(); 718 | assert!(rest.is_empty()); 719 | assert_eq!( 720 | file, 721 | File { 722 | index: 6, 723 | path: FilePath::FullPath("C:\\Foo\\Bar\\src\\main.rs".to_owned()), 724 | md5: Some("778FECDE2D48F9B948BA07E6E0B4AB98"), 725 | } 726 | ); 727 | } 728 | 729 | #[test] 730 | fn parse_function_alias() { 731 | assert_eq!( 732 | parse_statement("\t.type\ttwo,@function\n").unwrap().1, 733 | Statement::Directive(Directive::SymIsFun("two")) 734 | ); 735 | 736 | assert_eq!( 737 | parse_statement(".set\ttwo,\tone_plus_one\n").unwrap().1, 738 | Statement::Directive(Directive::SetValue("two", "one_plus_one")) 739 | ) 740 | } 741 | 742 | #[test] 743 | fn parse_data_decl() { 744 | assert_eq!( 745 | parse_statement(" .asciz \"sample_merged\"\n").unwrap().1, 746 | Statement::Directive(Directive::Data("asciz", "\"sample_merged\"")) 747 | ); 748 | assert_eq!( 749 | parse_statement(" .byte 0\n").unwrap().1, 750 | Statement::Directive(Directive::Data("byte", "0")) 751 | ); 752 | assert_eq!( 753 | parse_statement("\t.long .Linfo_st\n").unwrap().1, 754 | Statement::Directive(Directive::Data("long", ".Linfo_st")) 755 | ); 756 | } 757 | 758 | #[derive(Clone, Debug, Eq, PartialEq)] 759 | pub enum Directive<'a> { 760 | File(File<'a>), 761 | Loc(Loc<'a>), 762 | Global(&'a str), 763 | Generic(GenericDirective<'a>), 764 | SymIsFun(&'a str), 765 | SetValue(&'a str, &'a str), 766 | SubsectionsViaSym, 767 | SectionStart(&'a str), 768 | Data(&'a str, &'a str), 769 | } 770 | 771 | #[derive(Clone, Debug, Eq, PartialEq)] 772 | pub struct GenericDirective<'a>(pub &'a str); 773 | 774 | pub fn parse_statement(input: &str) -> IResult<&str, Statement> { 775 | let label = map(Label::parse, Statement::Label); 776 | 777 | let file = map(File::parse, Directive::File); 778 | 779 | let loc = map(Loc::parse, Directive::Loc); 780 | 781 | let section = map( 782 | preceded(tag("\t.section"), take_while1(|c| c != '\n')), 783 | |s: &str| Directive::SectionStart(s.trim()), 784 | ); 785 | let generic = map(preceded(tag("\t."), take_while1(|c| c != '\n')), |s| { 786 | Directive::Generic(GenericDirective(s)) 787 | }); 788 | let set = map( 789 | ( 790 | tag(".set"), 791 | space1, 792 | take_while1(good_for_label), 793 | tag(","), 794 | space0, 795 | take_while1(|c| c != '\n'), 796 | ), 797 | |(_, _, name, _, _, val)| Directive::SetValue(name, val), 798 | ); 799 | let ssvs = map(tag(".subsections_via_symbols"), |_| { 800 | Directive::SubsectionsViaSym 801 | }); 802 | 803 | let dunno = map(take_while1(|c| c != '\n'), Statement::Dunno); 804 | // let dunno = |input: &str| todo!("{:?}", &input[..100]); 805 | 806 | let instr = map(Instruction::parse, Statement::Instruction); 807 | let nothing = map(verify(not_line_ending, str::is_empty), |_| { 808 | Statement::Nothing 809 | }); 810 | 811 | let typ = map( 812 | ( 813 | tag("\t.type"), 814 | space1, 815 | take_while1(good_for_label), 816 | tag(",@function"), 817 | ), 818 | |(_, _, id, _)| Directive::SymIsFun(id), 819 | ); 820 | 821 | let global = map( 822 | ( 823 | space0, 824 | alt((tag(".globl"), tag(".global"))), 825 | space1, 826 | take_while1(|c| good_for_label(c) || c == '@'), 827 | ), 828 | |(_, _, _, name)| Directive::Global(name), 829 | ); 830 | let dir = map( 831 | alt(( 832 | file, 833 | global, 834 | loc, 835 | set, 836 | ssvs, 837 | section, 838 | typ, 839 | parse_data_dec, 840 | generic, 841 | )), 842 | Statement::Directive, 843 | ); 844 | 845 | // use terminated on the subparsers so that if the subparser doesn't consume the whole line, it's discarded 846 | // we assume that each label/instruction/directive will only take one line 847 | terminated(alt((label, dir, instr, nothing, dunno)), newline).parse(input) 848 | } 849 | 850 | fn good_for_label(c: char) -> bool { 851 | c == '.' || c == '$' || c == '_' || c.is_ascii_alphanumeric() 852 | } 853 | impl Statement<'_> { 854 | /// Is this a label that starts with ".Lfunc_end"? 855 | pub(crate) fn is_end_of_fn(&self) -> bool { 856 | let check_id = |id: &str| id.strip_prefix('.').unwrap_or(id).starts_with("Lfunc_end"); 857 | matches!(self, Statement::Label(Label { id, .. }) if check_id(id)) 858 | } 859 | 860 | /// Is this a .section directive? 861 | pub(crate) fn is_section_start(&self) -> bool { 862 | matches!(self, Statement::Directive(Directive::SectionStart(_))) 863 | } 864 | 865 | /// Is this a .global directive? 866 | pub(crate) fn is_global(&self) -> bool { 867 | matches!(self, Statement::Directive(Directive::Global(_))) 868 | } 869 | } 870 | -------------------------------------------------------------------------------- /src/cached_lines.rs: -------------------------------------------------------------------------------- 1 | use line_span::LineSpans; 2 | use std::ops::{Index, Range}; 3 | 4 | pub struct CachedLines { 5 | pub content: String, 6 | pub splits: Vec>, 7 | } 8 | 9 | impl CachedLines { 10 | #[must_use] 11 | pub fn without_ending(content: String) -> Self { 12 | let splits = content.line_spans().map(|s| s.range()).collect::>(); 13 | Self { content, splits } 14 | } 15 | 16 | #[must_use] 17 | pub fn iter(&self) -> LineIter { 18 | LineIter { 19 | payload: self, 20 | current: 0, 21 | } 22 | } 23 | #[must_use] 24 | pub fn get(&self, index: usize) -> Option<&str> { 25 | let range = self.splits.get(index)?.clone(); 26 | Some(&self.content[range]) 27 | } 28 | } 29 | 30 | impl Index for CachedLines { 31 | type Output = str; 32 | 33 | fn index(&self, index: usize) -> &Self::Output { 34 | &self.content[self.splits[index].clone()] 35 | } 36 | } 37 | 38 | pub struct LineIter<'a> { 39 | payload: &'a CachedLines, 40 | current: usize, 41 | } 42 | 43 | impl<'a> IntoIterator for &'a CachedLines { 44 | type Item = &'a str; 45 | 46 | type IntoIter = LineIter<'a>; 47 | 48 | fn into_iter(self) -> Self::IntoIter { 49 | self.iter() 50 | } 51 | } 52 | 53 | impl<'a> Iterator for LineIter<'a> { 54 | type Item = &'a str; 55 | 56 | fn next(&mut self) -> Option { 57 | self.current += 1; 58 | self.payload.get(self.current - 1) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/demangle.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_pub_self)] // default is wrong, I want to mark some items as explicitly private 2 | // otherwise pub(self) makes no sense 3 | 4 | use crate::{color, opts::NameDisplay}; 5 | use owo_colors::OwoColorize; 6 | use regex::{Regex, RegexSet, Replacer}; 7 | use rustc_demangle::Demangle; 8 | use std::{borrow::Cow, sync::OnceLock}; 9 | 10 | #[must_use] 11 | pub fn name(input: &str) -> Option { 12 | Some(format!("{:#?}", demangled(input)?)) 13 | } 14 | 15 | #[must_use] 16 | pub fn demangled(input: &str) -> Option { 17 | let name = if input.starts_with("__") { 18 | #[allow(clippy::string_slice)] 19 | rustc_demangle::try_demangle(&input[1..]).ok()? 20 | } else { 21 | rustc_demangle::try_demangle(input).ok()? 22 | }; 23 | Some(name) 24 | } 25 | 26 | pub(self) const GLOBAL_LABELS_REGEX: &str = r"\b_?(_[a-zA-Z0-9_$\.]+)"; 27 | 28 | // This regex is two parts 29 | // 1. \.L[a-zA-Z0-9_$\.]+ 30 | // 2. LBB[0-9_]+ 31 | // Label kind 1. is a standard label format for GCC and Clang (LLVM) 32 | // Label kinds 2. was detected in the wild, and don't seem to be a normal label format 33 | // however it's important to detect them, so they can be colored and possibly removed 34 | // 35 | // Note on `(?:[^\w\d\$\.]|^)`. This is to prevent the label from matching in the middle of some other word 36 | // since \b doesn't match before a `.` we can't use \b. So instead we're using a negation of any character 37 | // that could come up in the label OR the beginning of the text. It's not matching because we don't care what's 38 | // there as long as it doesn't look like a label. 39 | // 40 | // Note: this rejects "labels" like `H.Lfoo` but accepts `.Lexception` and `[some + .Label]` 41 | pub(self) const LOCAL_LABELS_REGEX: &str = r"(?:[^\w\d\$\.]|^)(\.L[a-zA-Z0-9_\$\.]+|\bLBB[0-9_]+)"; 42 | 43 | // temporary labels 44 | pub(self) const TEMP_LABELS_REGEX: &str = r"\b(Ltmp[0-9]+)\b"; 45 | 46 | pub(self) fn global_labels_reg() -> &'static Regex { 47 | static GLOBAL_LABELS: OnceLock = OnceLock::new(); 48 | GLOBAL_LABELS.get_or_init(|| Regex::new(GLOBAL_LABELS_REGEX).expect("regexp should be valid")) 49 | } 50 | 51 | pub(self) fn local_labels_reg() -> &'static Regex { 52 | static LOCAL_LABELS: OnceLock = OnceLock::new(); 53 | LOCAL_LABELS.get_or_init(|| Regex::new(LOCAL_LABELS_REGEX).expect("regexp should be valid")) 54 | } 55 | 56 | pub(self) fn label_kinds_reg() -> &'static RegexSet { 57 | static LABEL_KINDS: OnceLock = OnceLock::new(); 58 | LABEL_KINDS.get_or_init(|| { 59 | RegexSet::new([LOCAL_LABELS_REGEX, GLOBAL_LABELS_REGEX, TEMP_LABELS_REGEX]) 60 | .expect("regexp should be valid") 61 | }) 62 | } 63 | 64 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 65 | pub enum LabelKind { 66 | Global, 67 | Local, 68 | Temp, 69 | Unknown, 70 | } 71 | 72 | #[test] 73 | fn local_labels_works() { 74 | let s0 = "vmovaps xmm0, xmmword ptr [rip + .LCPI0_0]"; 75 | assert_eq!(local_labels(s0).collect::>(), [".LCPI0_0"]); 76 | 77 | let s1 = "vmovaps xmm0, xmmword ptr [rip + B.LCPI0_0]"; 78 | assert_eq!(local_labels(s1).collect::>(), [] as [&str; 0]); 79 | 80 | let s2 = ".Lexception"; 81 | assert_eq!(local_labels(s2).collect::>(), [".Lexception"]); 82 | } 83 | 84 | pub(crate) fn local_labels(input: &str) -> impl Iterator { 85 | local_labels_reg() 86 | .captures_iter(input) 87 | .filter_map(|c| Some(c.get(1)?.as_str())) 88 | } 89 | 90 | #[must_use] 91 | pub fn label_kind(input: &str) -> LabelKind { 92 | match label_kinds_reg().matches(input).into_iter().next() { 93 | Some(1) => LabelKind::Global, 94 | Some(0) => LabelKind::Local, 95 | Some(2) => LabelKind::Temp, 96 | _ => LabelKind::Unknown, 97 | } 98 | } 99 | 100 | struct LabelColorizer; 101 | impl Replacer for LabelColorizer { 102 | fn replace_append(&mut self, caps: ®ex::Captures<'_>, dst: &mut String) { 103 | use std::fmt::Write; 104 | write!(dst, "{}", color!(&caps[0], OwoColorize::bright_yellow)).unwrap(); 105 | } 106 | } 107 | 108 | pub fn color_local_labels(input: &str) -> Cow<'_, str> { 109 | local_labels_reg().replace_all(input, LabelColorizer) 110 | } 111 | 112 | struct Demangler { 113 | display: NameDisplay, 114 | } 115 | impl Replacer for Demangler { 116 | fn replace_append(&mut self, cap: ®ex::Captures<'_>, dst: &mut String) { 117 | if let Ok(dem) = rustc_demangle::try_demangle(&cap[1]) { 118 | use std::fmt::Write; 119 | match self.display { 120 | NameDisplay::Full => { 121 | write!(dst, "{:?}", color!(dem, OwoColorize::green)).unwrap(); 122 | } 123 | NameDisplay::Short => { 124 | write!(dst, "{:#?}", color!(dem, OwoColorize::green)).unwrap(); 125 | } 126 | NameDisplay::Mangled => { 127 | write!(dst, "{}", color!(&cap[1], OwoColorize::green)).unwrap(); 128 | } 129 | } 130 | } else { 131 | dst.push_str(&cap[0]); 132 | } 133 | } 134 | } 135 | 136 | #[must_use] 137 | pub fn contents(input: &str, display: NameDisplay) -> Cow<'_, str> { 138 | global_labels_reg().replace_all(input, Demangler { display }) 139 | } 140 | 141 | #[must_use] 142 | pub fn global_reference(input: &str) -> Option<&str> { 143 | global_labels_reg().find(input).map(|m| m.as_str()) 144 | } 145 | 146 | #[cfg(test)] 147 | mod test { 148 | use owo_colors::set_override; 149 | 150 | use crate::opts::NameDisplay; 151 | 152 | use super::{contents, name}; 153 | const MAC: &str = 154 | "__ZN58_$LT$nom..error..ErrorKind$u20$as$u20$core..fmt..Debug$GT$3fmt17hb98704099c11c31fE"; 155 | const LINUX: &str = 156 | "_ZN58_$LT$nom..error..ErrorKind$u20$as$u20$core..fmt..Debug$GT$3fmt17hb98704099c11c31fE"; 157 | const CALL_M: &str = "[rip + __ZN58_$LT$nom..error..ErrorKind$u20$as$u20$core..fmt..Debug$GT$3fmt17hb98704099c11c31fE]"; 158 | const CALL_L: &str = "[rip + _ZN58_$LT$nom..error..ErrorKind$u20$as$u20$core..fmt..Debug$GT$3fmt17hb98704099c11c31fE]"; 159 | 160 | #[test] 161 | fn linux_demangle() { 162 | assert!(name(LINUX).is_some()); 163 | } 164 | 165 | #[test] 166 | fn mac_demangle() { 167 | assert!(name(MAC).is_some()); 168 | } 169 | 170 | #[test] 171 | fn linux_no_demangle_call() { 172 | set_override(true); 173 | let x = contents(CALL_L, NameDisplay::Mangled); 174 | assert_eq!( 175 | "[rip + \u{1b}[32m_ZN58_$LT$nom..error..ErrorKind$u20$as$u20$core..fmt..Debug$GT$3fmt17hb98704099c11c31fE\u{1b}[39m]", 176 | x 177 | ); 178 | } 179 | 180 | #[test] 181 | fn linux_demangle_call() { 182 | set_override(true); 183 | let x = contents(CALL_L, NameDisplay::Short); 184 | assert_eq!( 185 | "[rip + \u{1b}[32m::fmt\u{1b}[39m]", 186 | x 187 | ); 188 | } 189 | 190 | #[test] 191 | fn mac_demangle_call() { 192 | set_override(true); 193 | let x = contents(CALL_M, NameDisplay::Short); 194 | assert_eq!( 195 | "[rip + \u{1b}[32m::fmt\u{1b}[39m]", 196 | x 197 | ); 198 | } 199 | 200 | #[test] 201 | fn mac_demangle_call2() { 202 | set_override(true); 203 | let x = contents(CALL_M, NameDisplay::Full); 204 | assert_eq!( 205 | "[rip + \u{1b}[32m::fmt::hb98704099c11c31f\u{1b}[39m]", 206 | x 207 | ); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/disasm.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | color, 3 | demangle::{self, demangled}, 4 | opts::{Format, NameDisplay, OutputStyle, ToDump}, 5 | pick_dump_item, safeprintln, Item, 6 | }; 7 | use ar::Archive; 8 | use capstone::{Capstone, Insn}; 9 | use object::{ 10 | Architecture, Object, ObjectSection, ObjectSymbol, Relocation, RelocationTarget, SectionIndex, 11 | SymbolKind, 12 | }; 13 | use owo_colors::OwoColorize; 14 | use std::{ 15 | collections::{BTreeMap, BTreeSet}, 16 | path::Path, 17 | }; 18 | 19 | /// Reference to some other symbol 20 | #[derive(Copy, Clone)] 21 | struct Reference<'a> { 22 | name: &'a str, 23 | name_display: NameDisplay, 24 | } 25 | 26 | impl std::fmt::Display for Reference<'_> { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | write!(f, "{}", demangle::contents(self.name, self.name_display)) 29 | } 30 | } 31 | 32 | struct HexDump<'a> { 33 | max_width: usize, 34 | bytes: &'a [u8], 35 | } 36 | 37 | impl std::fmt::Display for HexDump<'_> { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | if self.bytes.is_empty() { 40 | return Ok(()); 41 | } 42 | for byte in self.bytes.iter() { 43 | write!(f, "{:02x} ", byte)?; 44 | } 45 | for _ in 0..(1 + self.max_width - self.bytes.len()) { 46 | f.write_str(" ")?; 47 | } 48 | Ok(()) 49 | } 50 | } 51 | 52 | /// disassemble rlib or exe, one file at a time 53 | pub fn dump_disasm( 54 | goal: ToDump, 55 | file: &Path, 56 | fmt: &Format, 57 | syntax: OutputStyle, 58 | ) -> anyhow::Result<()> { 59 | if file.extension().is_some_and(|e| e == "rlib") { 60 | let mut slices = Vec::new(); 61 | let mut archive = Archive::new(std::fs::File::open(file)?); 62 | 63 | while let Some(entry) = archive.next_entry() { 64 | let mut entry = entry?; 65 | let name = std::str::from_utf8(entry.header().identifier())?; 66 | if !name.ends_with(".o") { 67 | continue; 68 | } 69 | let mut bytes = Vec::new(); 70 | std::io::Read::read_to_end(&mut entry, &mut bytes)?; 71 | slices.push(bytes); 72 | } 73 | dump_slices(goal, slices.as_slice(), fmt, syntax) 74 | } else { 75 | let binary_data = std::fs::read(file)?; 76 | dump_slices(goal, &[binary_data][..], fmt, syntax) 77 | } 78 | } 79 | 80 | fn pick_item<'a>( 81 | goal: ToDump, 82 | files: &'a [object::File], 83 | fmt: &Format, 84 | ) -> anyhow::Result<(&'a object::File<'a>, SectionIndex, usize, usize)> { 85 | let mut items = BTreeMap::new(); 86 | 87 | for file in files { 88 | let mut addresses: Vec<_> = file 89 | .symbols() 90 | .filter(|s| s.is_definition() && s.kind() == SymbolKind::Text) 91 | .map(|s| s.address() as usize) 92 | .collect(); 93 | addresses.sort_unstable(); 94 | 95 | for (index, symbol) in file 96 | .symbols() 97 | .filter(|s| s.is_definition() && s.kind() == SymbolKind::Text) 98 | .enumerate() 99 | { 100 | let raw_name = symbol.name()?; 101 | let (name, hashed) = match demangled(raw_name) { 102 | Some(dem) => (format!("{dem:#?}"), format!("{dem:?}")), 103 | None => (raw_name.to_owned(), raw_name.to_owned()), 104 | }; 105 | 106 | let Some(section_index) = symbol.section_index() else { 107 | // external symbol? 108 | continue; 109 | }; 110 | 111 | let addr = symbol.address() as usize; 112 | let mut len = symbol.size() as usize; // sorry 32bit platforms, you are not real 113 | if len == 0 { 114 | // Most symbols do not have a size. 115 | // Guess size from the address of the next symbol after it. 116 | let (Ok(idx) | Err(idx)) = addresses.binary_search(&addr); 117 | let next_address = match addresses[idx..].iter().copied().find(|&a| a > addr) { 118 | Some(addr) => addr, 119 | None => { 120 | let section = file.section_by_index(section_index)?; 121 | (section.address() + section.size()) as usize 122 | } 123 | }; 124 | len = next_address - addr; 125 | } 126 | let item = Item { 127 | name, 128 | hashed, 129 | index, 130 | len, 131 | non_blank_len: len, 132 | mangled_name: raw_name.to_owned(), 133 | }; 134 | items.insert(item, (file, section_index, addr, len)); 135 | } 136 | } 137 | 138 | // there are things that can be supported and there are things that I consider useful to 139 | // support. --everything with --disasm is not one of them for now 140 | pick_dump_item(goal, fmt, &items) 141 | .ok_or_else(|| anyhow::anyhow!("no can do --everything with --disasm")) 142 | } 143 | 144 | /// Get printable name from relocation info 145 | fn reloc_info<'a>( 146 | file: &'a object::File, 147 | reloc_map: &'a BTreeMap, 148 | insn: &Insn, 149 | fmt: &Format, 150 | ) -> Option> { 151 | let addr = insn.address(); 152 | let range = addr..addr + insn.len() as u64; 153 | let (_range, relocation) = reloc_map.range(range).next()?; 154 | let name = match relocation.target() { 155 | RelocationTarget::Symbol(sym) => file.symbol_by_index(sym).ok()?.name().ok(), 156 | RelocationTarget::Section(sec) => file.section_by_index(sec).ok()?.name().ok(), 157 | RelocationTarget::Absolute => None, 158 | _ => None, 159 | }?; 160 | Some(Reference { 161 | name, 162 | name_display: fmt.name_display, 163 | }) 164 | } 165 | 166 | fn dump_slices( 167 | goal: ToDump, 168 | binary_data: &[Vec], 169 | fmt: &Format, 170 | syntax: OutputStyle, 171 | ) -> anyhow::Result<()> { 172 | let files = binary_data 173 | .iter() 174 | .map(|data| object::File::parse(data.as_slice())) 175 | .collect::, _>>()?; 176 | let (file, section_index, addr, len) = pick_item(goal, &files, fmt)?; 177 | let mut opcode_cache = BTreeMap::new(); 178 | 179 | let section = file.section_by_index(section_index)?; 180 | let reloc_map = section.relocations().collect::>(); 181 | 182 | // if relocation map is present - addresses are going to be base 0 = useless 183 | // 184 | // For executable files there will be just one section... 185 | let symbol_names = if reloc_map.is_empty() { 186 | files 187 | .iter() 188 | .flat_map(|f| f.symbols()) 189 | .map(|s| { 190 | let name = s.name().unwrap(); 191 | let name = name.split_once('$').map_or(name, |(p, _)| p); 192 | let reloc = Reference { 193 | name, 194 | name_display: fmt.name_display, 195 | }; 196 | (s.address(), reloc) 197 | }) 198 | .collect::>() 199 | } else { 200 | BTreeMap::new() 201 | }; 202 | 203 | // In ARM ELF files, bit zero of the symbol address indicates its encoding. 204 | // It is one for Thumb-v2 (aka "t32") instructions and zero for ARM (aka 205 | // "a32") instructions. See ARM Arch ABI, 2024Q3, ELF, section 5.5.3. 206 | let is_thumb = addr & 1 == 1; 207 | let addr = addr & !1; 208 | let start = addr - section.address() as usize; 209 | let cs = make_capstone(file, syntax, is_thumb)?; 210 | let code = §ion.data()?[start..start + len]; 211 | 212 | if fmt.verbosity >= 2 { 213 | if reloc_map.is_empty() { 214 | safeprintln!("There is no relocation table"); 215 | } else { 216 | safeprintln!("reloc_map {:#?}", reloc_map); 217 | } 218 | } 219 | 220 | let insns = cs.disasm_all(code, addr as u64)?; 221 | if insns.is_empty() { 222 | if fmt.verbosity > 0 { 223 | safeprintln!("No instructions - empty code block?"); 224 | } 225 | return Ok(()); 226 | } 227 | 228 | let max_width = insns.iter().map(|i| i.len()).max().unwrap_or(1); 229 | 230 | // flow control related addresses referred by each instruction 231 | let addrs = insns 232 | .iter() 233 | .map(|insn| { 234 | if *opcode_cache.entry(insn.op_str()).or_insert_with(|| { 235 | cs.insn_detail(insn) 236 | .expect("Can't get instruction info") 237 | .groups() 238 | .iter() 239 | .any(|g| matches!(cs.group_name(*g).as_deref(), Some("call" | "jump"))) 240 | }) { 241 | let r = get_reference(&cs, insn)?; 242 | (r != insn.address() + insn.len() as u64).then_some(r) 243 | } else { 244 | None 245 | } 246 | }) 247 | .collect::>(); 248 | 249 | let local_range = insns[0].address()..insns.last().unwrap().address(); 250 | 251 | let local_labels = addrs 252 | .iter() 253 | .copied() 254 | .flatten() 255 | .filter(|addr| local_range.contains(addr)) 256 | .collect::>(); 257 | let local_labels = local_labels 258 | .into_iter() 259 | .enumerate() 260 | .map(|n| (n.1, n.0)) 261 | .collect::>(); 262 | 263 | let mut buf = String::new(); 264 | for (insn, &maddr) in insns.iter().zip(addrs.iter()) { 265 | let hex = HexDump { 266 | max_width, 267 | bytes: if fmt.simplify { &[] } else { insn.bytes() }, 268 | }; 269 | 270 | let addr = insn.address(); 271 | 272 | // binary code will have pending relocations if we are dealing with disassembling a library 273 | // code or with relocations already applied if we are working with a binary 274 | let mut refn = reloc_info(file, &reloc_map, insn, fmt) 275 | .or_else(|| maddr.and_then(|addr| symbol_names.get(&addr).copied())); 276 | 277 | if let Some(id) = local_labels.get(&addr) { 278 | use owo_colors::OwoColorize; 279 | safeprintln!( 280 | "{}{}:", 281 | crate::color!(".L", OwoColorize::bright_yellow), 282 | crate::color!(id, OwoColorize::bright_yellow), 283 | ); 284 | } 285 | 286 | let i = crate::asm::Instruction { 287 | op: insn.mnemonic().unwrap_or("???"), 288 | args: insn.op_str(), 289 | }; 290 | 291 | if let Some(id) = maddr.and_then(|a| local_labels.get(&a)) { 292 | buf.clear(); 293 | use std::fmt::Write; 294 | write!( 295 | buf, 296 | "{}{}", 297 | color!(".L", OwoColorize::bright_yellow), 298 | color!(id, OwoColorize::bright_yellow) 299 | ) 300 | .unwrap(); 301 | refn = Some(Reference { 302 | name: buf.as_str(), 303 | name_display: fmt.name_display, 304 | }); 305 | } 306 | 307 | if let Some(reloc) = refn { 308 | safeprintln!("{addr:8x}: {hex}{i} # {reloc}"); 309 | } else { 310 | safeprintln!("{addr:8x}: {hex}{i}"); 311 | } 312 | } 313 | 314 | Ok(()) 315 | } 316 | 317 | fn get_reference(cs: &Capstone, insn: &Insn) -> Option { 318 | use capstone::arch::{ 319 | arm64::Arm64OperandType, x86::X86OperandType, ArchDetail, DetailsArchInsn, 320 | }; 321 | let details = cs.insn_detail(insn).unwrap(); 322 | match details.arch_detail() { 323 | ArchDetail::X86Detail(x86) => match x86.operands().next()?.op_type { 324 | X86OperandType::Imm(rel) => Some(rel.try_into().unwrap()), 325 | X86OperandType::Mem(mem) => { 326 | assert_eq!(mem.scale(), 1); 327 | if mem.disp() == 0 { 328 | (insn.address() + insn.len() as u64).checked_add_signed(mem.disp()) 329 | } else { 330 | None 331 | } 332 | } 333 | _ => None, // ¯\_ (ツ)_/¯ 334 | }, 335 | 336 | // I have no idea what I'm doing here :) 337 | ArchDetail::Arm64Detail(arm) => match arm.operands().next()?.op_type { 338 | Arm64OperandType::Imm(rel) => Some(rel.try_into().unwrap()), 339 | Arm64OperandType::Mem(mem) => { 340 | if mem.disp() == 0 { 341 | (insn.address() + insn.len() as u64).checked_add_signed(mem.disp() as i64) 342 | } else { 343 | None 344 | } 345 | } 346 | _ => None, // ¯\_ (ツ)_/¯ 347 | }, 348 | 349 | _ => None, 350 | } 351 | } 352 | 353 | impl From for capstone::Syntax { 354 | fn from(value: OutputStyle) -> Self { 355 | match value { 356 | OutputStyle::Intel => Self::Intel, 357 | OutputStyle::Att => Self::Att, 358 | } 359 | } 360 | } 361 | 362 | fn make_capstone( 363 | file: &object::File, 364 | syntax: OutputStyle, 365 | is_thumb: bool, 366 | ) -> anyhow::Result { 367 | use capstone::{ 368 | arch::{self, BuildsCapstone}, 369 | Endian, 370 | }; 371 | 372 | let endiannes = match file.endianness() { 373 | object::Endianness::Little => Endian::Little, 374 | object::Endianness::Big => Endian::Big, 375 | }; 376 | let x86_width = if file.is_64() { 377 | arch::x86::ArchMode::Mode64 378 | } else { 379 | arch::x86::ArchMode::Mode32 380 | }; 381 | 382 | let mut capstone = match file.architecture() { 383 | Architecture::Aarch64 => Capstone::new() 384 | .arm64() 385 | .mode(arch::arm64::ArchMode::Arm) 386 | .build()?, 387 | Architecture::Arm => { 388 | let mode = if is_thumb { 389 | arch::arm::ArchMode::Thumb 390 | } else { 391 | arch::arm::ArchMode::Arm 392 | }; 393 | Capstone::new().arm().mode(mode).build()? 394 | } 395 | Architecture::X86_64 => Capstone::new().x86().mode(x86_width).build()?, 396 | unknown => anyhow::bail!("Dunno how to decompile {unknown:?}"), 397 | }; 398 | capstone.set_syntax(syntax.into())?; 399 | capstone.set_detail(true)?; 400 | capstone.set_endian(endiannes)?; 401 | Ok(capstone) 402 | } 403 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use opts::{Format, NameDisplay, ToDump}; 4 | use std::{ 5 | array, 6 | collections::{BTreeMap, BTreeSet}, 7 | ops::Range, 8 | path::{Path, PathBuf}, 9 | }; 10 | 11 | pub mod asm; 12 | pub mod cached_lines; 13 | pub mod demangle; 14 | #[cfg(feature = "disasm")] 15 | pub mod disasm; 16 | pub mod llvm; 17 | pub mod mca; 18 | pub mod mir; 19 | pub mod opts; 20 | 21 | #[macro_export] 22 | macro_rules! color { 23 | ($item:expr, $color:expr) => { 24 | owo_colors::OwoColorize::if_supports_color(&$item, owo_colors::Stream::Stdout, $color) 25 | }; 26 | } 27 | 28 | /// Safe version of `print[ln]!` macro 29 | /// By default `print[ln]!` macro panics when print fails. Usually print fails when output 30 | /// stream is disconnected, for purposes of this application disconnected stream means output 31 | /// was piped somewhere and this something was terminated before printing completed. 32 | /// 33 | /// At this point we might as well exit 34 | #[macro_export] 35 | macro_rules! safeprintln { 36 | ($($x:expr),* $(,)?) => {{ 37 | use std::io::Write; 38 | if writeln!(std::io::stdout(), $($x),*).is_err() { 39 | std::process::exit(0); 40 | } 41 | }}; 42 | } 43 | 44 | #[macro_export] 45 | macro_rules! safeprint { 46 | ($($x:expr),* $(,)?) => {{ 47 | use std::io::Write; 48 | if write!(std::io::stdout(), $($x),*).is_err() { 49 | std::process::exit(0); 50 | } 51 | }}; 52 | } 53 | 54 | #[macro_export] 55 | macro_rules! esafeprintln { 56 | ($($x:expr),* $(,)?) => {{ 57 | use std::io::Write; 58 | if writeln!(std::io::stderr(), $($x),*).is_err() { 59 | std::process::exit(0); 60 | } 61 | }}; 62 | } 63 | 64 | #[macro_export] 65 | macro_rules! esafeprint { 66 | ($($x:expr),* $(,)?) => {{ 67 | use std::io::Write; 68 | if write!(std::io::stderr(), $($x),*).is_err() { 69 | std::process::exit(0); 70 | } 71 | }}; 72 | } 73 | 74 | /// read a set of source files to a set of strings 75 | /// 76 | /// perform lossy conversion to utf8 77 | pub fn read_sources(names: &[PathBuf]) -> anyhow::Result> { 78 | names 79 | .iter() 80 | .map(|name| { 81 | let bytes = std::fs::read(name)?; 82 | // For some reason llvm/rustc can produce non utf8 files... 83 | // Also there's no (without unsafe) way to reuse allocation 84 | // from bytes in resulting String... 85 | Ok(String::from_utf8_lossy(&bytes).into_owned()) 86 | }) 87 | .collect() 88 | } 89 | 90 | #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] 91 | pub struct Item { 92 | // name and hashed MUST be first two fields - they are 93 | // used to produce correct Ord/PartialOrd 94 | /// demangled name 95 | pub name: String, 96 | /// demangled name with hash 97 | pub hashed: String, 98 | /// sequential number of demangled name 99 | pub index: usize, 100 | /// number of lines 101 | pub len: usize, 102 | /// number of non-blank lines 103 | pub non_blank_len: usize, 104 | /// mangled name 105 | pub mangled_name: String, 106 | } 107 | 108 | pub fn suggest_name<'a>( 109 | search: &str, 110 | fmt: &Format, 111 | items: impl IntoIterator, 112 | ) -> ! { 113 | let mut count = 0usize; 114 | let names: BTreeMap<&String, Vec> = 115 | items.into_iter().fold(BTreeMap::new(), |mut m, item| { 116 | count += 1; 117 | let entry = match fmt.name_display { 118 | NameDisplay::Full => &item.hashed, 119 | NameDisplay::Short => &item.name, 120 | NameDisplay::Mangled => &item.mangled_name, 121 | }; 122 | m.entry(entry).or_default().push(item.non_blank_len); 123 | m 124 | }); 125 | 126 | if fmt.verbosity > 0 { 127 | if names.is_empty() { 128 | if search.is_empty() { 129 | safeprintln!( 130 | "This target defines no functions (or cargo-show-asm can't find them)" 131 | ); 132 | } else { 133 | safeprintln!("No matching functions, try relaxing your search request"); 134 | } 135 | safeprintln!("You can pass --everything to see the demangled contents of a file"); 136 | } else { 137 | safeprintln!("Try one of those by name or a sequence number"); 138 | } 139 | } 140 | 141 | #[allow(clippy::cast_sign_loss)] 142 | #[allow(clippy::cast_precision_loss)] 143 | let width = (count as f64).log10().ceil() as usize; 144 | 145 | let mut ix = 0; 146 | for (name, lens) in &names { 147 | safeprintln!( 148 | "{ix:width$} {:?} {:?}", 149 | color!(name, owo_colors::OwoColorize::green), 150 | color!(lens, owo_colors::OwoColorize::cyan), 151 | ); 152 | ix += lens.len(); 153 | } 154 | 155 | std::process::exit(1); 156 | } 157 | 158 | /// Pick an item to dump based on a goal 159 | /// 160 | /// Prints suggestions and exits if goal can't be reached or more info is needed 161 | #[must_use] 162 | pub fn pick_dump_item( 163 | goal: ToDump, 164 | fmt: &Format, 165 | items: &BTreeMap, 166 | ) -> Option { 167 | match goal { 168 | // to dump everything just return an empty range 169 | ToDump::Everything => None, 170 | 171 | // By index without filtering 172 | ToDump::ByIndex { value } => { 173 | if let Some(range) = items.values().nth(value) { 174 | Some(range.clone()) 175 | } else { 176 | let actual = items.len(); 177 | esafeprintln!("You asked to display item #{value} (zero based), but there's only {actual} items"); 178 | std::process::exit(1); 179 | } 180 | } 181 | 182 | // By index with filtering 183 | ToDump::Function { function, nth } => { 184 | let filtered = items 185 | .iter() 186 | .filter(|(item, _range)| item.name.contains(&function)) 187 | .collect::>(); 188 | 189 | let range = if nth.is_none() && filtered.len() == 1 { 190 | filtered 191 | .first() 192 | .expect("Must have one item as checked above") 193 | .1 194 | .clone() 195 | } else if let Some(range) = nth.and_then(|nth| filtered.get(nth)) { 196 | range.1.clone() 197 | } else if let Some(value) = nth { 198 | let filtered = filtered.len(); 199 | esafeprintln!("You asked to display item #{value} (zero based), but there's only {filtered} matching items"); 200 | std::process::exit(1); 201 | } else { 202 | if filtered.is_empty() { 203 | esafeprintln!("Can't find any items matching {function:?}"); 204 | } else { 205 | suggest_name(&function, fmt, filtered.iter().map(|x| x.0)); 206 | } 207 | std::process::exit(1); 208 | }; 209 | Some(range) 210 | } 211 | 212 | ToDump::Unspecified => { 213 | let mut items_values = items.values(); 214 | if let [Some(item), None] = array::from_fn(|_| items_values.next()) { 215 | // Automatically pick an item if only one is found 216 | Some(item.clone()) 217 | } else { 218 | // Otherwise, print suggestions and exit 219 | let items = items.keys(); 220 | suggest_name("", fmt, items); 221 | } 222 | } 223 | } 224 | } 225 | 226 | trait RawLines { 227 | fn lines(&self) -> Option<&str>; 228 | } 229 | 230 | impl RawLines for &str { 231 | fn lines(&self) -> Option<&str> { 232 | Some(self) 233 | } 234 | } 235 | 236 | /// Recursively scan for references to global objects 237 | fn get_context_for( 238 | depth: usize, 239 | all_stmts: &[R], 240 | self_range: Range, 241 | items: &BTreeMap>, 242 | ) -> Vec> { 243 | let mut out = Vec::new(); 244 | if depth == 0 { 245 | return out; 246 | } 247 | let items = items 248 | .iter() 249 | .map(|(item, range)| (item.mangled_name.as_str(), range.clone())) 250 | .collect::>(); 251 | let mut pending = vec![(self_range.clone(), depth)]; 252 | let mut processed = BTreeSet::new(); 253 | while let Some((range, depth)) = pending.pop() { 254 | for raw in all_stmts[range] 255 | .iter() 256 | .filter_map(R::lines) 257 | .filter_map(demangle::global_reference) 258 | { 259 | if !processed.insert(raw) { 260 | continue; 261 | } 262 | if let Some(range) = items.get(raw) { 263 | if range == &self_range { 264 | continue; 265 | } 266 | if depth > 0 { 267 | pending.push((range.clone(), depth - 1)); 268 | } 269 | out.push(range.clone()); 270 | } 271 | } 272 | } 273 | out.sort_by_key(|r| r.start); 274 | out 275 | } 276 | 277 | pub trait Dumpable { 278 | type Line<'a>; 279 | /// Split source code into multiple lines, code can do some parsing here 280 | fn split_lines(contents: &str) -> anyhow::Result>>; 281 | 282 | /// Given a set of lines find all the interesting items 283 | fn find_items(lines: &[Self::Line<'_>]) -> BTreeMap>; 284 | 285 | /// Initialize freshly created Dumpable using additional information from the file 286 | fn init(&mut self, _lines: &[Self::Line<'_>]) {} 287 | 288 | /// print all the lines from this range, aplying the required formatting 289 | fn dump_range(&self, fmt: &Format, lines: &[Self::Line<'_>]) -> anyhow::Result<()>; 290 | 291 | /// starting at an initial range find more ranges to include 292 | fn extra_context( 293 | &self, 294 | fmt: &Format, 295 | lines: &[Self::Line<'_>], 296 | range: Range, 297 | items: &BTreeMap>, 298 | ) -> Vec> { 299 | #![allow(unused_variables)] 300 | Vec::new() 301 | } 302 | } 303 | 304 | /// Parse a dumpable item from a file and dump it with all the extra context 305 | pub fn dump_function( 306 | dumpable: &mut T, 307 | goal: ToDump, 308 | path: &Path, 309 | fmt: &Format, 310 | ) -> anyhow::Result<()> { 311 | // first we need to read the data and do a lossy conversion to a string slice 312 | // (files generated by rustc/llvm can have non-utf8 characters in them 313 | let raw_bytes = std::fs::read(path)?; 314 | let contents = String::from_utf8_lossy(&raw_bytes[..]); 315 | 316 | let lines = T::split_lines(&contents)?; 317 | let items = T::find_items(&lines); 318 | dumpable.init(&lines); 319 | 320 | match pick_dump_item(goal, fmt, &items) { 321 | Some(range) => { 322 | let context = T::extra_context(dumpable, fmt, &lines, range.clone(), &items); 323 | dumpable.dump_range(fmt, &lines[range])?; 324 | 325 | if !context.is_empty() { 326 | safeprintln!( 327 | "\n======================= Additional context =========================" 328 | ); 329 | for range in context { 330 | safeprintln!(""); 331 | dumpable.dump_range(fmt, &lines[range])?; 332 | } 333 | } 334 | } 335 | None => { 336 | if fmt.rust { 337 | // for asm files extra_context loads rust sources 338 | T::extra_context(dumpable, fmt, &lines, 0..lines.len(), &items); 339 | } 340 | dumpable.dump_range(fmt, &lines)? 341 | } 342 | } 343 | Ok(()) 344 | } 345 | 346 | /// Mostly the same as Range, but Copy and Ord 347 | #[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] 348 | pub struct URange { 349 | start: usize, 350 | end: usize, 351 | } 352 | 353 | impl From> for URange { 354 | fn from(Range { start, end }: Range) -> Self { 355 | Self { start, end } 356 | } 357 | } 358 | 359 | impl std::ops::Index for [T] { 360 | type Output = [T]; 361 | fn index(&self, index: URange) -> &Self::Output { 362 | &self[index.start..index.end] 363 | } 364 | } 365 | 366 | impl URange { 367 | pub fn fully_contains(&self, other: Self) -> bool { 368 | self.start >= other.start && self.end <= other.end 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/llvm.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::missing_errors_doc)] 2 | use line_span::LineSpans; 3 | // https://llvm.org/docs/LangRef.html 4 | use owo_colors::OwoColorize; 5 | use regex::Regex; 6 | 7 | use crate::Dumpable; 8 | use crate::{ 9 | color, 10 | demangle::{self, contents}, 11 | opts::Format, 12 | safeprintln, Item, 13 | }; 14 | use std::{ 15 | collections::BTreeMap, 16 | fs::File, 17 | io::{BufRead, BufReader}, 18 | ops::Range, 19 | path::Path, 20 | }; 21 | 22 | #[derive(Debug)] 23 | enum State { 24 | Skipping, 25 | Seeking, 26 | Name, 27 | Type, 28 | Define, 29 | } 30 | 31 | pub struct Llvm; 32 | 33 | impl Dumpable for Llvm { 34 | type Line<'a> = &'a str; 35 | fn split_lines(contents: &str) -> anyhow::Result>> { 36 | Ok(contents 37 | .line_spans() 38 | .map(|s| s.as_str()) 39 | .collect::>()) 40 | } 41 | fn find_items(lines: &[&str]) -> BTreeMap> { 42 | struct ItemParseState { 43 | item: Item, 44 | start: usize, 45 | } 46 | let mut res = BTreeMap::new(); 47 | let mut current_item = None::; 48 | let regex = Regex::new("@\"?(_?[a-zA-Z0-9_$.]+)\"?\\(").expect("regexp should be valid"); 49 | 50 | for (ix, &line) in lines.iter().enumerate() { 51 | if line.starts_with("; Module") || line.starts_with("; Function Attrs: ") { 52 | #[allow(clippy::needless_continue)] // silly clippy, readability suffers otherwise 53 | continue; 54 | } else if let (true, Some(name)) = (current_item.is_none(), line.strip_prefix("; ")) { 55 | current_item = Some(ItemParseState { 56 | item: Item { 57 | mangled_name: name.to_owned(), 58 | name: name.to_owned(), 59 | hashed: String::new(), 60 | index: res.len(), 61 | len: 0, 62 | non_blank_len: 0, 63 | }, 64 | start: ix, 65 | }); 66 | } else if line.starts_with("define ") { 67 | if let Some(name) = regex.captures(line).and_then(|c| c.get(1)) { 68 | let name = name.as_str(); 69 | let cur = current_item.get_or_insert_with(|| ItemParseState { 70 | item: Item { 71 | mangled_name: String::new(), 72 | name: name.to_owned(), 73 | hashed: String::new(), 74 | index: res.len(), 75 | len: 0, 76 | non_blank_len: 0, 77 | }, 78 | start: ix, 79 | }); 80 | cur.item.mangled_name = name.to_owned(); 81 | cur.item.hashed = demangle::demangled(name) 82 | .map_or_else(|| name.to_owned(), |hashed| format!("{hashed:?}")); 83 | } 84 | } else if !line_is_blank(line) { 85 | if let Some(cur) = &mut current_item { 86 | cur.item.non_blank_len += 1; 87 | } 88 | } else if line == "}" { 89 | if let Some(mut cur) = current_item.take() { 90 | // go home clippy, you're drunk 91 | #[allow(clippy::range_plus_one)] 92 | let range = cur.start..ix + 1; 93 | cur.item.len = range.len(); 94 | res.insert(cur.item, range); 95 | } 96 | } 97 | } 98 | res 99 | } 100 | 101 | fn dump_range(&self, fmt: &Format, strings: &[&str]) -> anyhow::Result<()> { 102 | for line in strings { 103 | if line.starts_with("; ") { 104 | safeprintln!("{}", color!(line, OwoColorize::bright_cyan)); 105 | } else { 106 | let line = contents(line, fmt.name_display); 107 | safeprintln!("{line}"); 108 | } 109 | } 110 | Ok(()) 111 | } 112 | } 113 | 114 | /// Returns true if the line should not be counted as meaningful for the function definition. 115 | /// 116 | /// LLVM functions can contain whitespace-only lines or lines with labels/comments that are not codegened, 117 | /// thus counted towards function size. 118 | /// llvm-lines uses similar heuristic to drop lines from its counts. 119 | fn line_is_blank(line: &str) -> bool { 120 | // Valid instructions have exactly two spaces as formatting. 121 | // Notable exceptions include comments (lines starting with ';') and 122 | // labels (lines starting with alphanumeric characters). 123 | let is_comment_or_label = !line.starts_with(" "); 124 | // That's not the end of story though. A line can have more than two spaces at the start of line, 125 | // but in that case it is an extension of the instruction started at previous line. 126 | // For example: 127 | // invoke void @_ZN4bpaf7literal17hd39eb03fefd4e354E(ptr sret(%"bpaf::params::ParseAny<()>") align 8 %_5, ptr align 1 %cmd.0, i64 %cmd.1) 128 | // to label %bb1 unwind label %cleanup, !dbg !4048 129 | // 130 | // First line is an instruction, so it should be counted towards function size. 131 | // The second one is a part of the instruction started on the previous line, so we should not 132 | // count that towards the function size. 133 | let is_multiline_instruction_extension = line.starts_with(" "); 134 | is_comment_or_label || is_multiline_instruction_extension 135 | } 136 | 137 | /// try to print `goal` from `path`, collect available items otherwise 138 | pub fn collect_or_dump( 139 | goal: Option<(&str, usize)>, 140 | path: &Path, 141 | fmt: &Format, 142 | items: &mut Vec, 143 | ) -> anyhow::Result { 144 | let mut seen = false; 145 | 146 | let reader = BufReader::new(File::open(path)?); 147 | 148 | let regex = Regex::new("@\"?(_?_[a-zA-Z0-9_$.]+)\"?\\(")?; 149 | let mut state = State::Seeking; 150 | let mut name = String::new(); 151 | let mut attrs = String::new(); 152 | let mut current_item = None::; 153 | let mut names = BTreeMap::new(); 154 | for (ix, line) in reader.lines().enumerate() { 155 | let line = line?; 156 | 157 | // glorious state machine 158 | match state { 159 | State::Skipping => { 160 | current_item = None; 161 | if line.is_empty() { 162 | state = State::Seeking; 163 | } 164 | } 165 | State::Seeking => { 166 | if let Some(name_str) = line.strip_prefix("; ") { 167 | state = State::Name; 168 | name = name_str.to_string(); 169 | } else { 170 | state = State::Skipping; 171 | } 172 | } 173 | State::Name => { 174 | if line.starts_with("; Function Attrs: ") { 175 | state = State::Type; 176 | attrs = line; 177 | } else { 178 | state = State::Skipping; 179 | } 180 | } 181 | State::Type => { 182 | if line.starts_with("define ") { 183 | state = State::Define; 184 | 185 | if let Some((mangled_name, hashed)) = regex 186 | .captures(&line) 187 | .and_then(|c| c.get(1)) 188 | .map(|c| c.as_str()) 189 | .and_then(|c| Some((c.to_owned(), demangle::demangled(c)?))) 190 | { 191 | let hashed = format!("{hashed:?}"); 192 | let name_entry = names.entry(name.clone()).or_insert(0); 193 | seen = goal.is_none_or(|goal| { 194 | (name.as_ref(), *name_entry) == goal || hashed == goal.0 195 | }); 196 | 197 | current_item = Some(Item { 198 | mangled_name, 199 | name: name.clone(), 200 | hashed, 201 | index: *name_entry, 202 | len: ix, 203 | non_blank_len: 0, 204 | }); 205 | *name_entry += 1; 206 | 207 | if seen { 208 | safeprintln!("{}", color!(name, OwoColorize::cyan)); 209 | safeprintln!("{}", color!(attrs, OwoColorize::cyan)); 210 | safeprintln!("{}", contents(&line, fmt.name_display)); 211 | } 212 | } else { 213 | state = State::Skipping; 214 | } 215 | } else { 216 | state = State::Skipping; 217 | } 218 | } 219 | State::Define => { 220 | if seen { 221 | safeprintln!("{}", contents(&line, fmt.name_display)); 222 | } 223 | if line == "}" { 224 | if let Some(mut cur) = current_item.take() { 225 | cur.len = ix - cur.len; 226 | cur.non_blank_len = cur.len; 227 | if goal.is_none_or(|goal| goal.0.is_empty() || cur.name.contains(goal.0)) { 228 | items.push(cur); 229 | } 230 | } 231 | if seen { 232 | return Ok(true); 233 | } 234 | state = State::Skipping; 235 | } 236 | } 237 | } 238 | } 239 | 240 | Ok(seen) 241 | } 242 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use cargo_metadata::{Artifact, Message, MetadataCommand, Package}; 3 | 4 | #[cfg(feature = "disasm")] 5 | use cargo_show_asm::disasm::dump_disasm; 6 | use cargo_show_asm::{ 7 | asm::Asm, 8 | dump_function, esafeprintln, 9 | llvm::Llvm, 10 | mca::Mca, 11 | mir::Mir, 12 | opts::{self, CodeSource, OutputType}, 13 | safeprintln, 14 | }; 15 | use std::{ 16 | io::BufReader, 17 | path::{Path, PathBuf}, 18 | process::{Child, Stdio}, 19 | sync::OnceLock, 20 | }; 21 | 22 | fn cargo_path() -> &'static Path { 23 | static CARGO_PATH: OnceLock = OnceLock::new(); 24 | CARGO_PATH 25 | .get_or_init(|| std::env::var_os("CARGO").map_or_else(|| "cargo".into(), PathBuf::from)) 26 | } 27 | 28 | fn rust_path() -> &'static Path { 29 | static RUSTC_PATH: OnceLock = OnceLock::new(); 30 | RUSTC_PATH 31 | .get_or_init(|| std::env::var_os("RUSTC").map_or_else(|| "rustc".into(), PathBuf::from)) 32 | } 33 | 34 | #[cfg(not(feature = "disasm"))] 35 | macro_rules! no_disasm { 36 | () => {{ 37 | // Sigh, never type... 38 | esafeprintln!("This option requires cargo-show-asm to be compiled with \"disasm\" feature"); 39 | std::process::exit(101) 40 | }}; 41 | } 42 | 43 | fn spawn_cargo( 44 | cargo: &opts::Cargo, 45 | format: &opts::Format, 46 | syntax: opts::Syntax, 47 | target_cpu: Option<&str>, 48 | focus_package: &Package, 49 | focus_artifact: &opts::Focus, 50 | force_single_cgu: bool, 51 | ) -> std::io::Result { 52 | use std::ffi::OsStr; 53 | use std::fmt::Write; 54 | 55 | let mut cmd = std::process::Command::new(cargo_path()); 56 | let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_default(); 57 | 58 | // Cargo flags. 59 | cmd.arg("rustc") 60 | // General. 61 | .args([ 62 | "--message-format=json-render-diagnostics", 63 | "--color", 64 | if format.color { "always" } else { "never" }, 65 | ]) 66 | .args(std::iter::repeat_n( 67 | "-v", 68 | format.verbosity.saturating_sub(1), 69 | )) 70 | // Workspace location. 71 | .arg("--manifest-path") 72 | .arg(&cargo.manifest_path) 73 | .args(["--config", "profile.release.strip=false"]) 74 | // Artifact selectors. 75 | .args(["--package", &focus_package.name]) 76 | .args(focus_artifact.as_cargo_args()) 77 | // Compile options. 78 | .args(cargo.config.iter().flat_map(|c| ["--config", c])) 79 | .args(cargo.dry.then_some("--dry")) 80 | .args(cargo.frozen.then_some("--frozen")) 81 | .args(cargo.locked.then_some("--locked")) 82 | .args(cargo.offline.then_some("--offline")) 83 | .args(cargo.quiet.then_some("--quiet")) 84 | .args(cargo.target.iter().flat_map(|t| ["--target", t])) 85 | .args(cargo.unstable.iter().flat_map(|z| ["-Z", z])) 86 | .args((syntax.output_type == OutputType::Wasm).then_some("--target=wasm32-unknown-unknown")) 87 | .args( 88 | cargo 89 | .target_dir 90 | .iter() 91 | .flat_map(|t| [OsStr::new("--target-dir"), t.as_ref()]), 92 | ) 93 | .args( 94 | cargo 95 | .cli_features 96 | .no_default_features 97 | .then_some("--no-default-features"), 98 | ) 99 | .args(cargo.cli_features.all_features.then_some("--all-features")) 100 | .args( 101 | cargo 102 | .cli_features 103 | .features 104 | .iter() 105 | .flat_map(|feat| ["--features", feat]), 106 | ); 107 | match &cargo.compile_mode { 108 | opts::CompileMode::Dev => {} 109 | opts::CompileMode::Release => { 110 | cmd.arg("--release"); 111 | } 112 | opts::CompileMode::Custom(profile) => { 113 | cmd.args(["--profile", profile]); 114 | } 115 | } 116 | 117 | // Cargo flags terminator. 118 | cmd.arg("--"); 119 | 120 | // Rustc flags. 121 | cmd 122 | // Start with the user-supplied codegen flags, which we might need to override. 123 | .args(cargo.codegen.iter().flat_map(|c| ["-C", c])) 124 | // Next, we care about asm/wasm/llvm-ir/llvm-mac. 125 | .args(syntax.emit().iter().flat_map(|s| ["--emit", s])) 126 | .args(syntax.format().iter().flat_map(|s| ["-C", s])); 127 | 128 | if let Some(cpu) = target_cpu { 129 | write!(rust_flags, " -Ctarget-cpu={cpu}").unwrap(); 130 | } 131 | 132 | { 133 | // None corresponds to disasm 134 | if [Some("asm"), None].contains(&syntax.emit()) { 135 | // Debug info is needed to detect function boundaries in asm (Windows/Mac), and to map asm/wasm 136 | // output to rust source. 137 | cmd.arg("-Cdebuginfo=2"); 138 | } 139 | } 140 | 141 | // current rust does not emit info about generated byproducts, new one will :) 142 | if force_single_cgu { 143 | cmd.arg("-Ccodegen-units=1"); 144 | } 145 | 146 | if !rust_flags.is_empty() { 147 | // `args` from `cargo rustc -- args` are passed only to the final compiler instance. 148 | // `RUSTFLAGS` envvar is useful for passing flags to all compiler instances. 149 | cmd.env("RUSTFLAGS", rust_flags.trim_start()); 150 | } 151 | 152 | if format.verbosity >= 3 { 153 | safeprintln!("Running: {cmd:?}"); 154 | } 155 | 156 | cmd.stdin(Stdio::null()) 157 | .stdout(Stdio::piped()) 158 | .stderr(Stdio::inherit()) 159 | .spawn() 160 | } 161 | 162 | fn sysroot() -> anyhow::Result { 163 | let output = std::process::Command::new(rust_path()) 164 | .arg("--print=sysroot") 165 | .stdin(Stdio::null()) 166 | .stderr(Stdio::inherit()) 167 | .stdout(Stdio::piped()) 168 | .output()?; 169 | if !output.status.success() { 170 | anyhow::bail!( 171 | "Failed to get sysroot. '{:?} --print=sysroot' exited with {}", 172 | rust_path(), 173 | output.status, 174 | ); 175 | } 176 | // `rustc` prints a trailing newline. 177 | Ok(PathBuf::from( 178 | std::str::from_utf8(&output.stdout)?.trim_end(), 179 | )) 180 | } 181 | 182 | #[allow(clippy::too_many_lines)] 183 | fn main() -> anyhow::Result<()> { 184 | let opts = opts::options().run(); 185 | owo_colors::set_override(opts.format.color); 186 | 187 | let cargo = match opts.code_source { 188 | CodeSource::FromCargo { ref cargo } => cargo, 189 | CodeSource::File { ref file } => { 190 | if opts.format.verbosity > 1 { 191 | esafeprintln!("Processing a given single file"); 192 | } 193 | match file.extension() { 194 | Some(ext) if ext == "s" => { 195 | let nope = PathBuf::new(); 196 | let mut asm = Asm::new(&nope, &nope); 197 | let mut format = opts.format; 198 | // For standalone file we don't know the matching 199 | // system root so don't even try to dump it 200 | format.rust = false; 201 | dump_function(&mut asm, opts.to_dump, file, &format)?; 202 | } 203 | _ => { 204 | #[cfg(feature = "disasm")] 205 | { 206 | dump_disasm(opts.to_dump, file, &opts.format, opts.syntax.output_style)? 207 | } 208 | #[cfg(not(feature = "disasm"))] 209 | { 210 | no_disasm!() 211 | } 212 | } 213 | } 214 | return Ok(()); 215 | } 216 | }; 217 | 218 | let sysroot = sysroot()?; 219 | if opts.format.verbosity > 1 { 220 | esafeprintln!("Found sysroot: {}", sysroot.display()); 221 | } 222 | 223 | let unstable = cargo 224 | .unstable 225 | .iter() 226 | .flat_map(|x| ["-Z".to_owned(), x.clone()]) 227 | .collect::>(); 228 | 229 | let metadata = MetadataCommand::new() 230 | .cargo_path(cargo_path()) 231 | .manifest_path(&cargo.manifest_path) 232 | .other_options(unstable) 233 | .no_deps() 234 | .exec()?; 235 | 236 | let focus_package = match opts.select_fragment.package { 237 | Some(ref name) => metadata 238 | .packages 239 | .iter() 240 | .find(|p| p.name == name.as_str()) 241 | .with_context(|| format!("Package '{name}' is not found"))?, 242 | None if metadata.packages.len() == 1 => &metadata.packages[0], 243 | None => { 244 | esafeprintln!( 245 | "{:?} refers to multiple packages, you need to specify which one to use", 246 | cargo.manifest_path 247 | ); 248 | for package in &metadata.packages { 249 | esafeprintln!("\t-p {}", package.name); 250 | } 251 | anyhow::bail!("Multiple packages found") 252 | } 253 | }; 254 | 255 | let focus_artifact = match opts.select_fragment.focus { 256 | Some(ref focus) => focus.clone(), 257 | None => match focus_package.targets.len() { 258 | 0 => anyhow::bail!("No targets found"), 259 | 1 => opts::Focus::try_from(&focus_package.targets[0])?, 260 | _ => { 261 | esafeprintln!( 262 | "{} defines multiple targets, you need to specify which one to use:", 263 | focus_package.name 264 | ); 265 | for target in &focus_package.targets { 266 | if let Ok(focus) = opts::Focus::try_from(target) { 267 | esafeprintln!("\t{}", focus.as_cargo_args().collect::>().join(" ")); 268 | } 269 | } 270 | anyhow::bail!("Multiple targets found") 271 | } 272 | }, 273 | }; 274 | 275 | // Pending on this https://github.com/rust-lang/rust/pull/122597 276 | 277 | #[cfg(feature = "disasm")] 278 | let force_single_cgu = opts.syntax.output_type != OutputType::Disasm; 279 | 280 | #[cfg(not(feature = "disasm"))] 281 | let force_single_cgu = true; 282 | 283 | let cargo_child = spawn_cargo( 284 | cargo, 285 | &opts.format, 286 | opts.syntax, 287 | opts.target_cpu.as_deref(), 288 | focus_package, 289 | &focus_artifact, 290 | force_single_cgu, 291 | )?; 292 | 293 | let asm_path = cargo_to_asm_path(cargo_child, &focus_artifact, &opts)?; 294 | 295 | if opts.format.verbosity > 3 { 296 | safeprintln!("goal: {:?}", opts.to_dump); 297 | } 298 | 299 | match opts.syntax.output_type { 300 | OutputType::Asm | OutputType::Wasm => { 301 | let mut asm = Asm::new(metadata.workspace_root.as_std_path(), &sysroot); 302 | dump_function(&mut asm, opts.to_dump, &asm_path, &opts.format) 303 | } 304 | OutputType::Llvm | OutputType::LlvmInput => { 305 | dump_function(&mut Llvm, opts.to_dump, &asm_path, &opts.format) 306 | } 307 | OutputType::Mir => dump_function(&mut Mir, opts.to_dump, &asm_path, &opts.format), 308 | OutputType::Mca => { 309 | let mut mca = Mca::new( 310 | &opts.mca_arg, 311 | cargo.target.as_deref(), 312 | opts.target_cpu.as_deref(), 313 | ); 314 | dump_function(&mut mca, opts.to_dump, &asm_path, &opts.format) 315 | } 316 | #[cfg(not(feature = "disasm"))] 317 | OutputType::Disasm => no_disasm!(), 318 | 319 | #[cfg(feature = "disasm")] 320 | OutputType::Disasm => dump_disasm( 321 | opts.to_dump, 322 | &asm_path, 323 | &opts.format, 324 | opts.syntax.output_style, 325 | ), 326 | } 327 | } 328 | 329 | fn cargo_to_asm_path( 330 | mut cargo: Child, 331 | focus_artifact: &opts::Focus, 332 | opts: &crate::opts::Options, 333 | ) -> anyhow::Result { 334 | let mut result_artifact = None; 335 | let mut success = false; 336 | for msg in Message::parse_stream(BufReader::new(cargo.stdout.take().unwrap())) { 337 | match msg? { 338 | Message::CompilerArtifact(artifact) if focus_artifact.matches_artifact(&artifact) => { 339 | result_artifact = Some(artifact); 340 | } 341 | Message::BuildFinished(fin) => { 342 | success = fin.success; 343 | break; 344 | } 345 | _ => {} 346 | } 347 | } 348 | // add some spacing between cargo's output and ours 349 | esafeprintln!(); 350 | if !success { 351 | let status = cargo.wait()?; 352 | esafeprintln!("Cargo failed with {status}"); 353 | std::process::exit(101); 354 | } 355 | let artifact = result_artifact.context("No artifact found")?; 356 | 357 | if opts.format.verbosity > 1 { 358 | esafeprintln!("Artifact files: {:?}", artifact.filenames); 359 | } 360 | 361 | let asm_path = match opts.syntax.ext() { 362 | Some(expect_ext) => locate_asm_path_via_artifact(&artifact, expect_ext)?, 363 | None => { 364 | if let Some(executable) = artifact.executable { 365 | executable.into() 366 | } else if let Some(rlib) = artifact 367 | .filenames 368 | .iter() 369 | .find(|f| f.extension() == Some("rlib")) 370 | { 371 | rlib.into() 372 | } else { 373 | todo!("{:?}", artifact); 374 | } 375 | } 376 | }; 377 | if opts.format.verbosity > 1 { 378 | esafeprintln!("Working with file: {}", asm_path.display()); 379 | } 380 | Ok(asm_path) 381 | } 382 | 383 | fn locate_asm_path_via_artifact(artifact: &Artifact, expect_ext: &str) -> anyhow::Result { 384 | // For lib, test, bench, lib-type example, `filenames` hint the file stem of the asm file. 385 | // We could locate asm files precisely. 386 | // 387 | // `filenames`: 388 | // [..]/target/debug/deps/libfoo-01234567.rmeta # lib by-product 389 | // [..]/target/debug/deps/foo-01234567 # test & bench 390 | // [..]/target/debug/deps/example/libfoo-01234567.rmeta # lib-type example by-product 391 | // Asm files: 392 | // [..]/target/debug/deps/foo-01234567.s 393 | // [..]/target/debug/deps/example/foo-01234567.s 394 | if let Some(path) = artifact 395 | .filenames 396 | .iter() 397 | .filter(|path| { 398 | matches!( 399 | path.parent().unwrap().file_name(), 400 | Some("deps" | "examples") 401 | ) 402 | }) 403 | .find_map(|path| { 404 | let path = path.with_extension(expect_ext); 405 | if path.exists() { 406 | return Some(path); 407 | } 408 | let path = path.with_file_name(path.file_name()?.strip_prefix("lib")?); 409 | if path.exists() { 410 | return Some(path); 411 | } 412 | None 413 | }) 414 | { 415 | return Ok(path.into_std_path_buf()); 416 | } 417 | 418 | // then there's rlib with filenames as following: 419 | // `filenames`: 420 | // [..]/target/debug/libfoo.a <+ 421 | // [..]/target/debug/libfoo.rlib | <+ Hard linked. 422 | // Asm files: | | Or same contents at least 423 | // [..]/target/debug/libfoo-01234567.a <+ | 424 | // [..]/target/debug/libfoo-01234567.rlib <+ 425 | // [..]/target/debug/foo-01234567.s 426 | 427 | if let Some(rlib_path) = artifact 428 | .filenames 429 | .iter() 430 | .find(|f| f.extension() == Some("rlib")) 431 | { 432 | let deps_dir = rlib_path.with_file_name("deps"); 433 | 434 | for entry in deps_dir.read_dir()? { 435 | let maybe_origin = entry?.path(); 436 | if same_contents(&rlib_path, &maybe_origin)? { 437 | let name = maybe_origin 438 | .file_name() 439 | .unwrap() 440 | .to_str() 441 | .unwrap() 442 | .strip_prefix("lib") 443 | .unwrap(); 444 | let asm_file = maybe_origin.with_file_name(name).with_extension(expect_ext); 445 | if asm_file.exists() { 446 | return Ok(asm_file); 447 | } 448 | } 449 | } 450 | } 451 | 452 | // for cdylib we have 453 | // [..]/target/debug/deps/xx.d 454 | // [..]/target/debug/deps/libxx.so <+ Hard linked/same contents 455 | // [..]/target/debug/deps/xx.s | <- asm file 456 | // [..]/target/debug/libxx.d | 457 | // [..]/target/debug/libxx.so <+ <- artifact 458 | // 459 | // on windows it's xx.dll / xx.s, on MacOS it's libxx.dylib / xx.s... 460 | // if artifact.target.kind.iter().any(|k| k == "cdylib") { 461 | if let Some(cdylib_path) = artifact.filenames.iter().find(|f| { 462 | f.extension() 463 | .is_some_and(|e| ["so", "dylib", "dll"].contains(&e)) 464 | }) { 465 | let deps_dir = cdylib_path.with_file_name("deps"); 466 | for entry in deps_dir.read_dir()? { 467 | let entry = entry?; 468 | let maybe_origin = entry.path(); 469 | if same_contents(cdylib_path, &maybe_origin)? { 470 | let Some(name) = maybe_origin.file_name() else { 471 | continue; 472 | }; 473 | let Some(name) = name.to_str() else { continue }; 474 | let name = name.strip_prefix("lib").unwrap_or(name); 475 | // on windows this is xx.dll -> xx.s, no lib.... 476 | let asm_file = maybe_origin.with_file_name(name).with_extension(expect_ext); 477 | if asm_file.exists() { 478 | return Ok(asm_file); 479 | } 480 | } 481 | } 482 | } 483 | 484 | // For bin or bin-type example artifacts, `filenames` provide hard-linked paths 485 | // without extra-filename. 486 | // We scan all possible original artifacts by checking hard links, 487 | // in order to retrieve the correct extra-filename, and then locate asm files. 488 | // 489 | // `filenames`, also `executable`: 490 | // [..]/target/debug/foobin <+ 491 | // [..]/target/debug/examples/fooexample | <+ Hard linked. 492 | // Origins: | | 493 | // [..]/target/debug/deps/foobin-01234567 <+ | 494 | // [..]/target/debug/examples/fooexample-01234567 <+ 495 | // Asm files: 496 | // [..]/target/debug/deps/foobin-01234567.s 497 | // [..]/target/debug/examples/fooexample-01234567.s 498 | if let Some(exe_path) = &artifact.executable { 499 | let parent = exe_path.parent().unwrap(); 500 | let deps_dir = if parent.file_name() == Some("examples") { 501 | parent.to_owned() 502 | } else { 503 | exe_path.with_file_name("deps") 504 | }; 505 | 506 | for entry in deps_dir.read_dir()? { 507 | let maybe_origin = entry?.path(); 508 | if same_contents(&exe_path, &maybe_origin)? { 509 | let asm_file = maybe_origin.with_extension(expect_ext); 510 | if asm_file.exists() { 511 | return Ok(asm_file); 512 | } 513 | } 514 | } 515 | } 516 | 517 | anyhow::bail!("Cannot locate the path to the asm file"); 518 | } 519 | 520 | fn same_contents, B: AsRef>(a: &A, b: &B) -> anyhow::Result { 521 | Ok(same_file::is_same_file(a, b)? 522 | || (std::fs::metadata(a)?.len() == std::fs::metadata(b)?.len() 523 | && std::fs::read(a)? == std::fs::read(b)?)) 524 | } 525 | -------------------------------------------------------------------------------- /src/mca.rs: -------------------------------------------------------------------------------- 1 | use crate::{asm::Statement, demangle, esafeprintln, opts::Format, safeprintln, Dumpable}; 2 | use std::{ 3 | io::{BufRead, BufReader}, 4 | process::{Command, Stdio}, 5 | }; 6 | 7 | pub struct Mca<'a> { 8 | /// mca specific arguments 9 | args: &'a [String], 10 | target_triple: Option<&'a str>, 11 | target_cpu: Option<&'a str>, 12 | intel_syntax: bool, 13 | } 14 | impl<'a> Mca<'a> { 15 | pub fn new( 16 | mca_args: &'a [String], 17 | target_triple: Option<&'a str>, 18 | target_cpu: Option<&'a str>, 19 | ) -> Self { 20 | Self { 21 | args: mca_args, 22 | target_triple, 23 | target_cpu, 24 | intel_syntax: false, 25 | } 26 | } 27 | } 28 | 29 | impl Dumpable for Mca<'_> { 30 | type Line<'a> = Statement<'a>; 31 | 32 | fn split_lines(contents: &str) -> anyhow::Result>> { 33 | crate::asm::parse_file(contents) 34 | } 35 | 36 | fn init(&mut self, lines: &[Self::Line<'_>]) { 37 | use crate::asm::{Directive, GenericDirective, Statement}; 38 | for line in lines { 39 | let Statement::Directive(Directive::Generic(GenericDirective(dir))) = line else { 40 | return; 41 | }; 42 | if dir.contains("intel_syntax") { 43 | self.intel_syntax = true; 44 | } 45 | } 46 | } 47 | 48 | fn find_items( 49 | lines: &[Self::Line<'_>], 50 | ) -> std::collections::BTreeMap> { 51 | crate::asm::find_items(lines) 52 | } 53 | 54 | fn dump_range(&self, fmt: &Format, lines: &[Self::Line<'_>]) -> anyhow::Result<()> { 55 | use std::io::Write; 56 | 57 | let mut mca = Command::new("llvm-mca"); 58 | mca.args(self.args) 59 | .args(self.target_triple.iter().flat_map(|t| ["--mtriple", t])) 60 | .args(self.target_cpu.iter().flat_map(|t| ["--mcpu", t])) 61 | .stdin(Stdio::piped()) 62 | .stdout(Stdio::piped()) 63 | .stderr(Stdio::piped()); 64 | 65 | if fmt.verbosity >= 3 { 66 | safeprintln!("running {mca:?}"); 67 | } 68 | let mca = mca.spawn(); 69 | let mut mca = match mca { 70 | Ok(mca) => mca, 71 | Err(err) => { 72 | esafeprintln!("Failed to start llvm-mca, do you have it installed? The error was"); 73 | esafeprintln!("{err}"); 74 | std::process::exit(1); 75 | } 76 | }; 77 | 78 | let mut i = mca.stdin.take().expect("Stdin should be piped"); 79 | let o = mca.stdout.take().expect("Stdout should be piped"); 80 | let e = mca.stderr.take().expect("Stderr should be piped"); 81 | 82 | if self.intel_syntax { 83 | // without that llvm-mca gets confused for some instructions 84 | writeln!(i, ".intel_syntax")? 85 | } 86 | 87 | for line in lines.iter() { 88 | match line { 89 | Statement::Label(l) => writeln!(i, "{}:", l.id)?, 90 | Statement::Directive(_) => {} 91 | Statement::Instruction(instr) => match instr.args { 92 | Some(args) => writeln!(i, "{} {}", instr.op, args)?, 93 | None => writeln!(i, "{}", instr.op)?, 94 | }, 95 | Statement::Nothing => {} 96 | // we couldn't parse it, maybe mca can? 97 | Statement::Dunno(unk) => writeln!(i, "{unk}")?, 98 | } 99 | } 100 | drop(i); 101 | 102 | for line in BufRead::lines(BufReader::new(o)) { 103 | let line = line?; 104 | let line = demangle::contents(&line, fmt.name_display); 105 | safeprintln!("{line}"); 106 | } 107 | 108 | for line in BufRead::lines(BufReader::new(e)) { 109 | let line = line?; 110 | esafeprintln!("{line}"); 111 | } 112 | 113 | Ok(()) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/mir.rs: -------------------------------------------------------------------------------- 1 | use crate::Dumpable; 2 | use crate::{color, opts::Format, safeprintln, Item}; 3 | use line_span::LineSpans; 4 | use owo_colors::OwoColorize; 5 | use std::{collections::BTreeMap, ops::Range}; 6 | 7 | pub struct Mir; 8 | 9 | impl Dumpable for Mir { 10 | type Line<'a> = &'a str; 11 | 12 | fn find_items(lines: &[&str]) -> BTreeMap> { 13 | let mut res = BTreeMap::new(); 14 | let mut current_item = None::; 15 | let mut block_start = None; 16 | 17 | for (ix, &line) in lines.iter().enumerate() { 18 | if line.starts_with("//") { 19 | if block_start.is_none() { 20 | block_start = Some(ix); 21 | } 22 | } else if line == "}" { 23 | if let Some(mut cur) = current_item.take() { 24 | // go home clippy, you're drunk 25 | #[allow(clippy::range_plus_one)] 26 | let range = cur.len..ix + 1; 27 | cur.len = range.len(); 28 | res.insert(cur, range); 29 | } 30 | } else if !(line.starts_with(' ') || line.is_empty()) && current_item.is_none() { 31 | let start = block_start.take().unwrap_or(ix); 32 | let mut name = line; 33 | 'outer: loop { 34 | for suffix in [" {", " =", " -> ()"] { 35 | if let Some(rest) = name.strip_suffix(suffix) { 36 | name = rest; 37 | continue 'outer; 38 | } 39 | } 40 | break; 41 | } 42 | current_item = Some(Item { 43 | mangled_name: name.to_owned(), 44 | name: name.to_owned(), 45 | hashed: name.to_owned(), 46 | index: res.len(), 47 | len: start, 48 | non_blank_len: 0, 49 | }); 50 | } 51 | } 52 | 53 | res 54 | } 55 | 56 | fn dump_range(&self, _fmt: &Format, strings: &[&str]) -> anyhow::Result<()> { 57 | for line in strings { 58 | if let Some(ix) = line.rfind("//") { 59 | safeprintln!("{}{}", &line[..ix], color!(&line[ix..], OwoColorize::cyan)); 60 | } else { 61 | safeprintln!("{line}"); 62 | } 63 | } 64 | Ok(()) 65 | } 66 | 67 | fn split_lines(contents: &str) -> anyhow::Result> { 68 | Ok(contents 69 | .line_spans() 70 | .map(|s| s.as_str()) 71 | .collect::>()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/opts.rs: -------------------------------------------------------------------------------- 1 | use bpaf::{construct, doc::Style, long, short, Bpaf, Parser}; 2 | use cargo_metadata::Artifact; 3 | use std::path::PathBuf; 4 | 5 | fn check_target_dir(path: PathBuf) -> anyhow::Result { 6 | if path.is_dir() { 7 | Ok(path) 8 | } else { 9 | std::fs::create_dir(&path)?; 10 | Ok(std::fs::canonicalize(path)?) 11 | } 12 | } 13 | 14 | #[derive(Clone, Debug, Bpaf)] 15 | #[bpaf(options("asm"), version)] 16 | #[allow(clippy::struct_excessive_bools)] 17 | #[allow(clippy::doc_markdown)] 18 | /// Show the code rustc generates for any function 19 | /// 20 | /// 21 | /// 22 | /// Usage: 23 | /// 1. Focus on a single assembly producing target: 24 | /// % cargo asm -p isin --lib # here we are targeting lib in isin crate 25 | /// 2. Narrow down a function: 26 | /// % cargo asm -p isin --lib from_ # here "from_" is part of the function you are interested intel 27 | /// 3. Get the full results: 28 | /// % cargo asm -p isin --lib isin::base36::from_alphanum 29 | pub struct Options { 30 | // here is the code located 31 | #[bpaf(external)] 32 | pub select_fragment: SelectFragment, 33 | 34 | // how to compile 35 | #[bpaf(external)] 36 | pub code_source: CodeSource, 37 | 38 | // how to display 39 | /// Pass parameter to llvm-mca for mca targets 40 | #[bpaf(short('M'), long)] 41 | pub mca_arg: Vec, 42 | /// Generate code for a specific CPU 43 | #[bpaf(external)] 44 | pub target_cpu: Option, 45 | #[bpaf(external)] 46 | pub format: Format, 47 | #[bpaf(external(syntax_compat))] 48 | pub syntax: Syntax, 49 | 50 | // what to display 51 | #[bpaf(external)] 52 | pub to_dump: ToDump, 53 | } 54 | 55 | #[derive(Debug, Clone, Bpaf)] 56 | pub enum CodeSource { 57 | FromCargo { 58 | #[bpaf(external(cargo))] 59 | cargo: Cargo, 60 | }, 61 | File { 62 | /// Disassemble or process this file instead of calling cargo, 63 | /// requires cargo-show-asm to be compiled with disasm feature 64 | /// 65 | /// You can specify executable, rlib or an object file 66 | #[bpaf(argument("PATH"), hide_usage)] 67 | file: PathBuf, 68 | }, 69 | } 70 | 71 | #[derive(Clone, Debug, Bpaf)] 72 | pub struct SelectFragment { 73 | // what to compile 74 | /// Package to use, defaults to a current one, 75 | /// 76 | /// required for workspace projects, can also point 77 | /// to a dependency 78 | #[bpaf(long, short, argument("SPEC"))] 79 | pub package: Option, 80 | 81 | #[bpaf(external, optional)] 82 | pub focus: Option, 83 | } 84 | 85 | #[derive(Debug, Clone, Bpaf)] 86 | #[allow(clippy::struct_excessive_bools)] 87 | /// Cargo options 88 | #[bpaf(hide_usage)] 89 | pub struct Cargo { 90 | #[bpaf(external, hide_usage)] 91 | pub manifest_path: PathBuf, 92 | 93 | /// Override a cargo configuration value 94 | #[bpaf(argument("KEY=VALUE"))] 95 | pub config: Vec, 96 | 97 | /// Use custom target directory for generated artifacts, create if missing 98 | #[bpaf( 99 | env("CARGO_TARGET_DIR"), 100 | argument("DIR"), 101 | parse(check_target_dir), 102 | optional, 103 | hide_usage 104 | )] 105 | pub target_dir: Option, 106 | /// Produce a build plan instead of actually building 107 | #[bpaf(hide_usage)] 108 | pub dry: bool, 109 | /// Requires Cargo.lock and cache to be up-to-date 110 | #[bpaf(hide_usage)] 111 | pub frozen: bool, 112 | /// Requires Cargo.lock to be up-to-date 113 | #[bpaf(hide_usage)] 114 | pub locked: bool, 115 | /// Run without accessing the network 116 | #[bpaf(hide_usage)] 117 | pub offline: bool, 118 | /// Do not print cargo log messages 119 | #[bpaf(short, long, hide_usage)] 120 | pub quiet: bool, 121 | #[bpaf(external, hide_usage)] 122 | pub cli_features: CliFeatures, 123 | #[bpaf(external)] 124 | pub compile_mode: CompileMode, 125 | /// Build for the target triple 126 | #[bpaf(argument("TRIPLE"))] 127 | pub target: Option, 128 | /// Codegen flags to rustc, see 'rustc -C help' for details 129 | #[bpaf(short('C'), argument("FLAG"))] 130 | pub codegen: Vec, 131 | /// Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details 132 | // OsString would be better but MetadataCommand takes a vector of strings... 133 | #[bpaf(short('Z'), argument("FLAG"))] 134 | pub unstable: Vec, 135 | } 136 | 137 | #[derive(Debug, Clone, Bpaf)] 138 | /// Pick item to display from the artifact 139 | #[bpaf(fallback(ToDump::Unspecified))] 140 | pub enum ToDump { 141 | /// Dump the whole file 142 | Everything, 143 | 144 | #[bpaf(hide)] 145 | ByIndex { 146 | /// Dump name with this index 147 | #[bpaf(positional("ITEM_INDEX"))] 148 | value: usize, 149 | }, 150 | 151 | Function { 152 | /// Dump a function with a given name, filter functions by name 153 | #[bpaf(positional("FUNCTION"))] 154 | function: String, 155 | 156 | /// Select specific function when there's several with the same name 157 | #[bpaf(positional("INDEX"))] 158 | nth: Option, 159 | }, 160 | 161 | #[bpaf(skip)] 162 | Unspecified, 163 | } 164 | 165 | fn target_cpu() -> impl Parser> { 166 | let native = long("native") 167 | .help("Optimize for the CPU running the compiler") 168 | .req_flag("native".to_string()); 169 | let cpu = long("target-cpu") 170 | .help("Optimize code for a specific CPU, see 'rustc --print target-cpus'") 171 | .argument::("CPU"); 172 | construct!([native, cpu]) 173 | .custom_usage(&[("TARGET-CPU", Style::Metavar)]) 174 | .optional() 175 | } 176 | 177 | #[derive(Bpaf, Clone, Debug)] 178 | pub struct CliFeatures { 179 | /// Do not activate `default` feature 180 | pub no_default_features: bool, 181 | 182 | /// Activate all available features 183 | pub all_features: bool, 184 | 185 | /// A feature to activate, can be used multiple times 186 | #[bpaf(short('F'), long("features"), argument("FEATURE"))] 187 | pub features: Vec, 188 | } 189 | 190 | #[derive(Bpaf, Clone, Debug)] 191 | #[bpaf(fallback(CompileMode::Release))] 192 | pub enum CompileMode { 193 | /// Compile in release mode (default) 194 | Release, 195 | /// Compile in dev mode 196 | Dev, 197 | Custom( 198 | /// Build for this specific profile, you can also use `dev` and `release` here 199 | #[bpaf(env("CARGO_SHOW_ASM_PROFILE"), long("profile"), argument("PROFILE"))] 200 | String, 201 | ), 202 | } 203 | 204 | fn verbosity() -> impl Parser { 205 | let verbose = short('v') 206 | .long("verbose") 207 | .help("more verbose output, can be specified multiple times") 208 | .req_flag(()) 209 | .hide_usage() 210 | .count(); 211 | let silent = short('s') 212 | .long("silent") 213 | .help("print less user-forward information to make consumption by tools easier") 214 | .req_flag(()) 215 | .hide_usage() 216 | .count(); 217 | construct!(verbose, silent).map(|(v, q)| (v + 1).saturating_sub(q)) 218 | } 219 | 220 | fn manifest_path() -> impl Parser { 221 | long("manifest-path") 222 | .help("Path to Cargo.toml, defaults to one in current folder") 223 | .argument::("PATH") 224 | .parse(|p| { 225 | if p.is_absolute() { 226 | Ok(p) 227 | } else { 228 | std::env::current_dir() 229 | .map(|d| d.join(p)) 230 | .and_then(|full_path| full_path.canonicalize()) 231 | } 232 | }) 233 | .fallback_with(|| std::env::current_dir().map(|x| x.join("Cargo.toml"))) 234 | } 235 | 236 | #[allow(clippy::struct_excessive_bools)] 237 | #[derive(Debug, Clone, Bpaf, Copy)] 238 | /// Postprocessing options: 239 | pub struct Format { 240 | /// Print interleaved Rust code 241 | pub rust: bool, 242 | 243 | /// Include other called functions, recursively, up to COUNT depth 244 | #[bpaf(short, long, argument("COUNT"), fallback(0), display_fallback)] 245 | pub context: usize, 246 | 247 | #[bpaf(external(color_detection), hide_usage)] 248 | pub color: bool, 249 | 250 | #[bpaf(hide_usage, external)] 251 | pub name_display: NameDisplay, 252 | 253 | #[bpaf(external, hide_usage)] 254 | pub redundant_labels: RedundantLabels, 255 | 256 | /// more verbose output, can be specified multiple times 257 | // default verbosity is 1 258 | #[bpaf(external)] 259 | pub verbosity: usize, 260 | 261 | /// Try to strip some of the non-assembly instruction information 262 | pub simplify: bool, 263 | 264 | /// Include sections containing string literals and other constants 265 | pub include_constants: bool, 266 | 267 | /// Keep blank lines 268 | #[bpaf(short('b'), long, hide_usage)] 269 | pub keep_blank: bool, 270 | 271 | #[bpaf(external)] 272 | pub sources_from: SourcesFrom, 273 | } 274 | 275 | #[derive(Debug, Clone, Copy, Bpaf)] 276 | #[bpaf(fallback(SourcesFrom::AllSources))] 277 | pub enum SourcesFrom { 278 | /// Show rust sources from current workspace only 279 | ThisWorkspace, 280 | /// Show rust sources from current workspace and from rust registry 281 | AllCrates, 282 | /// Show all the rust sources including stdlib and compiler 283 | AllSources, 284 | } 285 | 286 | #[derive(Debug, Clone, Bpaf, Eq, PartialEq, Copy)] 287 | #[bpaf(fallback(RedundantLabels::Strip))] 288 | pub enum RedundantLabels { 289 | /// Keep all the original labels 290 | #[bpaf(short('K'), long("keep-labels"))] 291 | Keep, 292 | /// Strip redundant labels, but keep spaces in their place 293 | #[bpaf(short('B'), long("keep-blanks"))] 294 | Blanks, 295 | /// Strip redundant labels entirely 296 | #[bpaf(short('R'), long("reduce-labels"))] 297 | Strip, 298 | } 299 | 300 | #[derive(Debug, Copy, Clone, Bpaf, Eq, PartialEq)] 301 | #[bpaf(fallback(NameDisplay::Short))] 302 | pub enum NameDisplay { 303 | #[bpaf(long("full-name"))] 304 | /// Include full demangled name instead of just prefix 305 | Full, 306 | 307 | /// Include demangled names without hash suffix (default) 308 | #[bpaf(long("short-name"))] 309 | Short, 310 | 311 | /// Do not demangle symbol names 312 | #[bpaf(long("keep-mangled"))] 313 | Mangled, 314 | } 315 | 316 | #[derive(Debug, Clone, Bpaf, Eq, PartialEq, Copy)] 317 | #[bpaf(fallback(OutputType::Asm))] 318 | pub enum OutputType { 319 | /// Show assembly 320 | Asm, 321 | /// Disassembly binaries or object files 322 | Disasm, 323 | /// Show llvm-ir 324 | Llvm, 325 | /// Show llvm-ir before any LLVM passes 326 | LlvmInput, 327 | /// Show MIR 328 | Mir, 329 | /// Show WASM, needs wasm32-unknown-unknown target installed 330 | Wasm, 331 | /// Show llvm-mca anasysis 332 | Mca, 333 | } 334 | 335 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Bpaf)] 336 | #[bpaf(fallback(OutputStyle::Intel))] 337 | pub enum OutputStyle { 338 | /// Use Intel style for assembly 339 | Intel, 340 | /// Use AT&T style for assembly 341 | Att, 342 | } 343 | 344 | #[derive(Debug, Clone, Copy, Bpaf)] 345 | #[bpaf(custom_usage(&[("OUTPUT-FORMAT", Style::Metavar)]))] 346 | /// Pick output type: 347 | pub struct Syntax { 348 | #[bpaf(external)] 349 | pub output_type: OutputType, 350 | #[bpaf(external)] 351 | pub output_style: OutputStyle, 352 | } 353 | 354 | fn syntax_compat() -> impl Parser { 355 | let mca_att = long("mca-att") 356 | .req_flag(Syntax { 357 | output_type: OutputType::Mca, 358 | output_style: OutputStyle::Att, 359 | }) 360 | .hide(); 361 | let mca_intel = long("mca-intel") 362 | .req_flag(Syntax { 363 | output_type: OutputType::Mca, 364 | output_style: OutputStyle::Intel, 365 | }) 366 | .hide(); 367 | construct!([syntax(), mca_att, mca_intel]) 368 | } 369 | 370 | impl Syntax { 371 | #[must_use] 372 | pub fn format(&self) -> Option<&str> { 373 | match self.output_type { 374 | OutputType::Asm | OutputType::Mca => match self.output_style { 375 | OutputStyle::Intel => Some("llvm-args=-x86-asm-syntax=intel"), 376 | OutputStyle::Att => Some("llvm-args=-x86-asm-syntax=att"), 377 | }, 378 | OutputType::LlvmInput => Some("no-prepopulate-passes"), 379 | OutputType::Llvm | OutputType::Mir | OutputType::Wasm => None, 380 | 381 | OutputType::Disasm => None, 382 | } 383 | } 384 | 385 | #[must_use] 386 | pub fn emit(&self) -> Option<&str> { 387 | match self.output_type { 388 | OutputType::Asm | OutputType::Wasm | OutputType::Mca => Some("asm"), 389 | OutputType::Llvm | OutputType::LlvmInput => Some("llvm-ir"), 390 | OutputType::Mir => Some("mir"), 391 | 392 | OutputType::Disasm => None, 393 | } 394 | } 395 | 396 | #[must_use] 397 | pub fn ext(&self) -> Option<&str> { 398 | match self.output_type { 399 | OutputType::Asm | OutputType::Wasm | OutputType::Mca => Some("s"), 400 | OutputType::Llvm | OutputType::LlvmInput => Some("ll"), 401 | OutputType::Mir => Some("mir"), 402 | 403 | OutputType::Disasm => None, 404 | } 405 | } 406 | } 407 | 408 | fn color_detection() -> impl Parser { 409 | let yes = long("color") 410 | .help("Enable color highlighting") 411 | .req_flag(true); 412 | let no = long("no-color") 413 | .help("Disable color highlighting") 414 | .req_flag(false); 415 | construct!([yes, no]).fallback_with::<_, &str>(|| { 416 | Ok(supports_color::on(supports_color::Stream::Stdout).is_some()) 417 | }) 418 | } 419 | 420 | #[derive(Debug, Clone, Bpaf)] 421 | /// Pick artifact for analysis: 422 | #[bpaf(custom_usage(&[("ARTIFACT", Style::Metavar)]))] 423 | pub enum Focus { 424 | /// Show results from library code 425 | Lib, 426 | 427 | Test( 428 | /// Show results from an integration test 429 | #[bpaf(long("test"), argument("TEST"))] 430 | String, 431 | ), 432 | 433 | // Show available tests (hidden: cargo shows the list as an error) 434 | #[bpaf(long("test"), hide)] 435 | TestList, 436 | 437 | Bench( 438 | /// Show results from a benchmark 439 | #[bpaf(long("bench"), argument("BENCH"))] 440 | String, 441 | ), 442 | 443 | // Show available benchmarks (hidden: cargo shows the list as an error) 444 | #[bpaf(long("bench"), hide)] 445 | BenchList, 446 | 447 | Example( 448 | /// Show results from an example 449 | #[bpaf(long("example"), argument("EXAMPLE"))] 450 | String, 451 | ), 452 | 453 | // Show available examples (hidden: cargo shows the list as an error) 454 | #[bpaf(long("example"), hide)] 455 | ExampleList, 456 | 457 | Bin( 458 | /// Show results from a binary 459 | #[bpaf(long("bin"), argument("BIN"))] 460 | String, 461 | ), 462 | 463 | // Show available binaries (hidden: cargo shows the list as an error) 464 | #[bpaf(long("bin"), hide)] 465 | BinList, 466 | } 467 | 468 | impl TryFrom<&'_ cargo_metadata::Target> for Focus { 469 | type Error = anyhow::Error; 470 | 471 | fn try_from(target: &cargo_metadata::Target) -> Result { 472 | use cargo_metadata::TargetKind as T; 473 | let kind = target 474 | .kind 475 | .first() 476 | .ok_or_else(|| anyhow::anyhow!("No target kinds in target"))?; 477 | let name = target.name.clone(); 478 | match kind { 479 | T::Lib | T::RLib | T::CDyLib => Ok(Focus::Lib), 480 | T::Test => Ok(Focus::Test(name)), 481 | T::Bench => Ok(Focus::Bench(name)), 482 | T::Example => Ok(Focus::Example(name)), 483 | T::Bin => Ok(Focus::Bin(name)), 484 | // don't bother with handling remaining cases since struct is #[non_exhaustive] 485 | _ => anyhow::bail!("Unsupported target kind {kind:?}"), 486 | } 487 | } 488 | } 489 | 490 | impl Focus { 491 | #[must_use] 492 | pub fn as_parts(&self) -> (&str, Option<&str>) { 493 | match self { 494 | Focus::Lib => ("lib", None), 495 | Focus::Test(name) => ("test", Some(name)), 496 | Focus::TestList => ("test", None), 497 | Focus::Bench(name) => ("bench", Some(name)), 498 | Focus::BenchList => ("bench", None), 499 | Focus::Example(name) => ("example", Some(name)), 500 | Focus::ExampleList => ("example", None), 501 | Focus::Bin(name) => ("bin", Some(name)), 502 | Focus::BinList => ("bin", None), 503 | } 504 | } 505 | 506 | pub fn as_cargo_args(&self) -> impl Iterator { 507 | let (kind, name) = self.as_parts(); 508 | std::iter::once(format!("--{kind}")).chain(name.map(ToOwned::to_owned)) 509 | } 510 | 511 | #[must_use] 512 | pub fn matches_artifact(&self, artifact: &Artifact) -> bool { 513 | let (kind, name) = self.as_parts(); 514 | let somewhat_matches = 515 | kind == "lib" && artifact.target.is_rlib() || artifact.target.is_cdylib(); 516 | let kind = ::from_str(kind) 517 | .expect("cargo_metadata made me do it"); 518 | let kind_matches = artifact.target.kind.contains(&kind); 519 | (somewhat_matches || kind_matches) && name.is_none_or(|name| artifact.target.name == *name) 520 | } 521 | } 522 | 523 | #[cfg(unix)] 524 | #[cfg(test)] 525 | fn write_updated(new_val: &str, path: impl AsRef) -> std::io::Result { 526 | use std::io::Read; 527 | use std::io::Seek; 528 | let mut file = std::fs::OpenOptions::new() 529 | .write(true) 530 | .read(true) 531 | .create(true) 532 | .truncate(false) 533 | .open(path)?; 534 | let mut current_val = String::new(); 535 | file.read_to_string(&mut current_val)?; 536 | if current_val != new_val { 537 | file.set_len(0)?; 538 | file.seek(std::io::SeekFrom::Start(0))?; 539 | std::io::Write::write_all(&mut file, new_val.as_bytes())?; 540 | Ok(false) 541 | } else { 542 | Ok(true) 543 | } 544 | } 545 | 546 | #[cfg(unix)] 547 | #[test] 548 | fn docs_are_up_to_date() { 549 | let usage = options().render_markdown("cargo asm"); 550 | let readme = std::fs::read_to_string("README.tpl").unwrap(); 551 | let docs = readme.replacen("", &usage, 1); 552 | assert!(write_updated(&docs, "README.md").unwrap()); 553 | } 554 | --------------------------------------------------------------------------------