mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-18 04:18:10 +00:00
feat(tools): add scripts for firmware.elf file analysis using binsize
[no changelog]
This commit is contained in:
parent
ee81f98708
commit
668bddff17
1
core/tools/size/.gitignore
vendored
Normal file
1
core/tools/size/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.txt
|
44
core/tools/size/README.md
Normal file
44
core/tools/size/README.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Size (binsize)
|
||||
|
||||
Shell and python scripts acting as wrappers around `binsize` tool/command.
|
||||
|
||||
Adding `--help` to any `<command>.sh` will show the help message for that specific command.
|
||||
|
||||
Settings of this specific (`trezor-firmware`) project are forwarded in each command. Specifically, `core/build/firmware/firmware.elf` and `core/build/firmware/firmware.map` (optional) files are used and we are interested in `.flash` and `.flash2` sections.
|
||||
|
||||
`bloaty` and `nm` tools are needed.
|
||||
|
||||
For more info about `binsize` tool, visit [its repository](github.com/trezor/binsize).
|
||||
|
||||
## Available scripts/commands
|
||||
|
||||
### app.py
|
||||
Shows the statistics about each micropython app.
|
||||
|
||||
### build.sh <file_suffix>
|
||||
Builds the firmware with optional renaming of the generated .elf file.
|
||||
|
||||
### checker.py
|
||||
Checks the size of the current firmware against the size limits of its flash sections.
|
||||
|
||||
### commit.sh <commit_id>
|
||||
Gets the size difference introduced by a specified commit
|
||||
|
||||
### compare_master.py
|
||||
Compares the size of the current firmware with the size of the latest master.
|
||||
|
||||
### compare.sh <old_elf_file> <new_elf_file>
|
||||
Compares the size of two firmware binaries.
|
||||
|
||||
### get.sh
|
||||
Gets the size information about the current firmware.
|
||||
|
||||
### groups.py
|
||||
Shows the groupings of all symbols into specific categories.
|
||||
|
||||
### history.sh <commit_amount:int> <step_size:int>
|
||||
Shows the size history of latest `<commit_amount>` with `<step_size>` commits between them.
|
||||
BEWARE: might not always work properly, as it needs to build firmware for each commit. It may happens that some commits are not buildable.
|
||||
|
||||
### tree.sh
|
||||
Shows the tree-size view of all files in the current firmware with their sizes.
|
45
core/tools/size/apps.py
Executable file
45
core/tools/size/apps.py
Executable file
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Showing sizes of individual micropython apps.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from binsize import BinarySize, StatisticsPlugin, DataRow
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
CORE_DIR = HERE.parent.parent
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
BIN_TO_ANALYZE = sys.argv[1]
|
||||
else:
|
||||
BIN_TO_ANALYZE = CORE_DIR / "build/firmware/firmware.elf" # type: ignore
|
||||
|
||||
|
||||
def apps_categories(row: DataRow) -> str | None:
|
||||
pattern = r"^src/apps/(\w+)/" # dir name after apps/
|
||||
match = re.search(pattern, row.module_name)
|
||||
if not match:
|
||||
return None
|
||||
else:
|
||||
return match.group(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
BS = (
|
||||
BinarySize()
|
||||
.load_file(BIN_TO_ANALYZE, sections=(".flash", ".flash2"))
|
||||
.use_map_file(
|
||||
CORE_DIR / "build/firmware/firmware.map", sections=(".flash", ".flash2")
|
||||
)
|
||||
.add_basic_info()
|
||||
.aggregate()
|
||||
.sort()
|
||||
.add_definitions()
|
||||
)
|
||||
StatisticsPlugin(BS, apps_categories).show()
|
7
core/tools/size/build.sh
Executable file
7
core/tools/size/build.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
CURR_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
CORE_DIR=$(realpath "${CURR_DIR}/../..")
|
||||
|
||||
export BINSIZE_ROOT_DIR="${CORE_DIR}"
|
||||
binsize build $@
|
56
core/tools/size/checker.py
Executable file
56
core/tools/size/checker.py
Executable file
@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Checking the size of the flash sections in firmware binary.
|
||||
|
||||
Prints the info and fails if there is not enough free space.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from binsize import get_sections_sizes, set_root_dir
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
CORE_DIR = HERE.parent.parent
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
BIN_TO_ANALYZE = sys.argv[1]
|
||||
else:
|
||||
BIN_TO_ANALYZE = CORE_DIR / "build/firmware/firmware.elf" # type: ignore
|
||||
|
||||
# Comes from `core/embed/firmware/memory_T.ld`
|
||||
FLASH_SIZE_KB = 768
|
||||
FLASH_2_SIZE_KB = 896
|
||||
|
||||
MIN_KB_FREE_TO_SUCCEED = 15
|
||||
|
||||
EXIT_CODE = 0
|
||||
|
||||
|
||||
def report_section(name: str, size: int, max_size: int) -> None:
|
||||
percentage = 100 * size / max_size
|
||||
free = max_size - size
|
||||
|
||||
print(f"{name}: {size}K / {max_size}K ({percentage:.2f}%) - {free}K free")
|
||||
|
||||
global EXIT_CODE
|
||||
if free < MIN_KB_FREE_TO_SUCCEED:
|
||||
print(
|
||||
f"Less free space in {name} ({free}K) than expected ({MIN_KB_FREE_TO_SUCCEED}K). Failing"
|
||||
)
|
||||
EXIT_CODE = 1 # type: ignore
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Analyzing {BIN_TO_ANALYZE}")
|
||||
|
||||
set_root_dir(str(CORE_DIR))
|
||||
|
||||
sizes = get_sections_sizes(BIN_TO_ANALYZE, sections=(".flash", ".flash2"))
|
||||
|
||||
report_section(".flash", sizes[".flash"] // 1024, FLASH_SIZE_KB)
|
||||
report_section(".flash2", sizes[".flash2"] // 1024, FLASH_2_SIZE_KB)
|
||||
|
||||
sys.exit(EXIT_CODE)
|
7
core/tools/size/commit.sh
Executable file
7
core/tools/size/commit.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
CURR_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
CORE_DIR=$(realpath "${CURR_DIR}/../..")
|
||||
|
||||
export BINSIZE_ROOT_DIR="${CORE_DIR}"
|
||||
binsize commit -s ".flash" -s ".flash2" $@
|
7
core/tools/size/compare.sh
Executable file
7
core/tools/size/compare.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
CURR_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
CORE_DIR=$(realpath "${CURR_DIR}/../..")
|
||||
|
||||
export BINSIZE_ROOT_DIR="${CORE_DIR}"
|
||||
binsize compare -s ".flash" -s ".flash2" $@
|
97
core/tools/size/compare_master.py
Executable file
97
core/tools/size/compare_master.py
Executable file
@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Compares the current firmware build with a master one
|
||||
and prints the differences.
|
||||
|
||||
Fails if the current changes are increasing the size by a lot.
|
||||
|
||||
Also generates a thorough report of the current state of the binary
|
||||
with all the functions and their definitions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import shutil
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
import requests
|
||||
import click
|
||||
|
||||
from binsize import BinarySize, get_sections_sizes, show_binaries_diff, set_root_dir
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
CORE_DIR = HERE.parent.parent
|
||||
|
||||
FIRMWARE_ELF = CORE_DIR / "build/firmware/firmware.elf"
|
||||
|
||||
MAX_KB_ADDITION_TO_SUCCEED = 5
|
||||
|
||||
|
||||
def download_and_get_latest_master_firmware_elf() -> Path:
|
||||
url = "https://gitlab.com/satoshilabs/trezor/trezor-firmware/-/jobs/artifacts/master/download?job=core%20fw%20regular%20build"
|
||||
req = requests.get(url)
|
||||
tmp_dir = HERE / "tmp_for_master_elf"
|
||||
zip_file = ZipFile(BytesIO(req.content))
|
||||
zip_file.extractall(tmp_dir)
|
||||
|
||||
atexit.register(lambda: shutil.rmtree(tmp_dir))
|
||||
|
||||
return tmp_dir / "firmware.elf"
|
||||
|
||||
|
||||
def generate_report_file(fw_location: str, report_file: str | Path) -> None:
|
||||
BinarySize().load_file(
|
||||
fw_location, sections=(".flash", ".flash2")
|
||||
).add_basic_info().aggregate().sort(lambda row: row.size, reverse=True).show(
|
||||
report_file
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("fw_location", required=False, default=FIRMWARE_ELF)
|
||||
@click.option("-r", "--report-file", help="Report file")
|
||||
def compare_master(fw_location: str, report_file: str | None) -> None:
|
||||
print(f"Analyzing {fw_location}")
|
||||
set_root_dir(str(CORE_DIR))
|
||||
|
||||
if report_file:
|
||||
print(f"Generating report file under {report_file}")
|
||||
generate_report_file(fw_location, report_file)
|
||||
|
||||
sections = (".flash", ".flash2")
|
||||
|
||||
master_bin = download_and_get_latest_master_firmware_elf()
|
||||
if not master_bin.exists():
|
||||
print("Master firmware not found")
|
||||
sys.exit(1)
|
||||
|
||||
show_binaries_diff(old=master_bin, new=fw_location, sections=sections)
|
||||
|
||||
curr = get_sections_sizes(fw_location, sections)
|
||||
curr_flash = curr[".flash"] // 1024
|
||||
curr_flash_2 = curr[".flash2"] // 1024
|
||||
|
||||
master = get_sections_sizes(master_bin, sections)
|
||||
master_flash = master[".flash"] // 1024
|
||||
master_flash_2 = master[".flash2"] // 1024
|
||||
|
||||
print()
|
||||
print(f"Current: flash={curr_flash}K flash2={curr_flash_2}K")
|
||||
print(f"Master: flash={master_flash}K flash2={master_flash_2}K")
|
||||
|
||||
size_diff = (curr_flash + curr_flash_2) - (master_flash + master_flash_2)
|
||||
print(f"Size_diff: {size_diff} K")
|
||||
if size_diff > MAX_KB_ADDITION_TO_SUCCEED:
|
||||
print(f"Size of flash sections increased by {size_diff} K.")
|
||||
print(f"More than allowed {MAX_KB_ADDITION_TO_SUCCEED} K. Failing.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
compare_master()
|
10
core/tools/size/get.sh
Executable file
10
core/tools/size/get.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
CURR_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
CORE_DIR=$(realpath "${CURR_DIR}/../..")
|
||||
|
||||
FIRMWARE_ELF="${CORE_DIR}/build/firmware/firmware.elf"
|
||||
MAP_FILE="${CORE_DIR}/build/firmware/firmware.map"
|
||||
|
||||
export BINSIZE_ROOT_DIR="${CORE_DIR}"
|
||||
binsize get "${FIRMWARE_ELF}" -m "${MAP_FILE}" -s ".flash" -s ".flash2" $@
|
171
core/tools/size/groups.py
Executable file
171
core/tools/size/groups.py
Executable file
@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Grouping symbols in binary into coherent categories.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from binsize import BinarySize, DataRow, StatisticsPlugin, set_root_dir
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
CORE_DIR = HERE.parent.parent
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
BIN_TO_ANALYZE = sys.argv[1]
|
||||
else:
|
||||
BIN_TO_ANALYZE = CORE_DIR / "build/firmware/firmware.elf" # type: ignore
|
||||
FILE_TO_SAVE = HERE / "size_binary_firmware_elf_results.txt"
|
||||
|
||||
|
||||
def _categories_func(row: DataRow) -> str | None:
|
||||
# Defined inside the function so it can be seen in the function definition
|
||||
# (which is optionally printed)
|
||||
CATEGORIES: dict[str, Callable[[DataRow], bool]] = {
|
||||
"UI": lambda row: (
|
||||
row.source_definition.startswith(
|
||||
("src/trezor/ui/", "embed/extmod/modtrezorui/")
|
||||
)
|
||||
),
|
||||
"Crypto": lambda row: (
|
||||
row.source_definition.startswith(
|
||||
(
|
||||
"vendor/trezor-crypto/",
|
||||
"src/trezor/crypto/",
|
||||
"embed/extmod/modtrezorcrypto/",
|
||||
)
|
||||
)
|
||||
),
|
||||
"Secp256": lambda row: (
|
||||
row.source_definition.startswith("vendor/secp256k1-zkp/")
|
||||
),
|
||||
"Storage": lambda row: (
|
||||
row.source_definition.startswith(("src/storage/", "vendor/trezor-storage/"))
|
||||
),
|
||||
"Micropython": lambda row: row.source_definition.startswith(
|
||||
"vendor/micropython/"
|
||||
),
|
||||
"Bitcoin app": lambda row: row.source_definition.startswith(
|
||||
"src/apps/bitcoin/"
|
||||
),
|
||||
"Ethereum app": lambda row: row.source_definition.startswith(
|
||||
"src/apps/ethereum/"
|
||||
),
|
||||
"Monero app": lambda row: row.source_definition.startswith("src/apps/monero/"),
|
||||
"Cardano app": lambda row: row.source_definition.startswith(
|
||||
"src/apps/cardano/"
|
||||
),
|
||||
"Management app": lambda row: row.source_definition.startswith(
|
||||
"src/apps/management/"
|
||||
),
|
||||
"Common apps": lambda row: row.source_definition.startswith("src/apps/common/"),
|
||||
"Webauthn app": lambda row: row.source_definition.startswith(
|
||||
"src/apps/webauthn/"
|
||||
),
|
||||
"Altcoin apps": lambda row: (
|
||||
row.source_definition.startswith(
|
||||
(
|
||||
"src/apps/nem/",
|
||||
"src/apps/stellar/",
|
||||
"src/apps/eos/",
|
||||
"src/apps/tezos/",
|
||||
"src/apps/ripple/",
|
||||
"src/apps/zcash/",
|
||||
"src/apps/binance/",
|
||||
)
|
||||
)
|
||||
),
|
||||
"Other apps": lambda row: row.source_definition.startswith("src/apps/"),
|
||||
"Rest of src/": lambda row: row.source_definition.startswith("src/"),
|
||||
"Fonts": lambda row: row.source_definition.startswith("embed/lib/fonts/"),
|
||||
"Embed firmware": lambda row: row.source_definition.startswith(
|
||||
"embed/firmware/"
|
||||
),
|
||||
"Trezorhal": lambda row: row.source_definition.startswith("embed/trezorhal/"),
|
||||
"Trezorio": lambda row: row.source_definition.startswith(
|
||||
"embed/extmod/modtrezorio/"
|
||||
),
|
||||
"Trezorconfig": lambda row: row.source_definition.startswith(
|
||||
"embed/extmod/modtrezorconfig/"
|
||||
),
|
||||
"Trezorutils": lambda row: row.source_definition.startswith(
|
||||
"embed/extmod/modtrezorutils/"
|
||||
),
|
||||
"Embed extmod": lambda row: row.source_definition.startswith("embed/extmod/"),
|
||||
"Embed lib": lambda row: row.source_definition.startswith("embed/lib/"),
|
||||
"Rust": lambda row: (
|
||||
row.language == "Rust"
|
||||
or row.source_definition.startswith(("embed/rust/", "/cargo/registry"))
|
||||
or row.symbol_name.startswith(("trezor_tjpgdec", "qrcodegen"))
|
||||
or ".cargo/registry" in row.symbol_name
|
||||
),
|
||||
"Frozen modules": lambda row: row.symbol_name.startswith("frozen_module"),
|
||||
".bootloader": lambda row: row.symbol_name == ".bootloader",
|
||||
".rodata + qstr + misc": lambda row: (
|
||||
row.symbol_name.startswith(
|
||||
(".rodata", "mp_qstr", "str1", "*fill*", ".text", "OUTLINED_FUNCTION")
|
||||
)
|
||||
or _has_32_hex(row.symbol_name)
|
||||
),
|
||||
}
|
||||
|
||||
for category, func in CATEGORIES.items():
|
||||
if func(row):
|
||||
return category
|
||||
return None
|
||||
|
||||
|
||||
def _has_32_hex(text: str) -> bool:
|
||||
if "." in text:
|
||||
text = text.split(".")[0]
|
||||
return len(text) == 32 and all(c in "0123456789abcdef" for c in text)
|
||||
|
||||
|
||||
def show_categories_statistics(
|
||||
STATS: StatisticsPlugin, include_categories_func: bool = False
|
||||
) -> None:
|
||||
STATS.show(include_none=True, include_categories_func=include_categories_func)
|
||||
|
||||
|
||||
def show_data_with_categories(
|
||||
STATS: StatisticsPlugin, file_to_save: str | Path | None = None
|
||||
) -> None:
|
||||
STATS.show_data_with_categories(file_to_save, include_none=True)
|
||||
|
||||
|
||||
def show_only_one_category(
|
||||
BS: BinarySize, category: str | None, file_to_save: str | Path | None = None
|
||||
) -> None:
|
||||
BS.filter(lambda row: _categories_func(row) == category).show(
|
||||
file_to_save, debug=True
|
||||
)
|
||||
|
||||
|
||||
def show_raw_bloaty_data() -> None:
|
||||
BinarySize().load_file(BIN_TO_ANALYZE, sections=(".flash", ".flash2")).show(
|
||||
HERE / "size_binary_firmware_elf_results_no_aggregation.txt"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
set_root_dir(str(CORE_DIR))
|
||||
|
||||
BS = (
|
||||
BinarySize()
|
||||
.load_file(BIN_TO_ANALYZE, sections=(".flash", ".flash2"))
|
||||
.use_map_file(
|
||||
CORE_DIR / "build/firmware/firmware.map", sections=(".flash", ".flash2")
|
||||
)
|
||||
.add_basic_info()
|
||||
.aggregate()
|
||||
.sort()
|
||||
.add_definitions()
|
||||
)
|
||||
STATS = StatisticsPlugin(BS, _categories_func)
|
||||
|
||||
show_categories_statistics(STATS, include_categories_func=True)
|
||||
show_data_with_categories(STATS, FILE_TO_SAVE)
|
||||
show_only_one_category(BS, None, HERE / "size_binary_firmware_elf_results_None.txt")
|
7
core/tools/size/history.sh
Executable file
7
core/tools/size/history.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
CURR_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
CORE_DIR=$(realpath "${CURR_DIR}/../..")
|
||||
|
||||
export BINSIZE_ROOT_DIR="${CORE_DIR}"
|
||||
binsize history -s ".flash" -s ".flash2" $@
|
10
core/tools/size/tree.sh
Executable file
10
core/tools/size/tree.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
CURR_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
CORE_DIR=$(realpath "${CURR_DIR}/../..")
|
||||
|
||||
FIRMWARE_ELF="${CORE_DIR}/build/firmware/firmware.elf"
|
||||
MAP_FILE="${CORE_DIR}/build/firmware/firmware.map"
|
||||
|
||||
export BINSIZE_ROOT_DIR="${CORE_DIR}"
|
||||
binsize tree "${FIRMWARE_ELF}" -m "${MAP_FILE}" -s ".flash" -s ".flash2" $@
|
Loading…
Reference in New Issue
Block a user