diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 89363a849..39fdc11e3 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -75,9 +75,12 @@ jobs: - name: Build base image # Always rebuild the base image when the scheduled workflow runs if: inputs.flush_cache || steps.cache-image.outputs.cache-hit != 'true' || github.event_name == 'schedule' + env: + NXP_EMAIL: ${{ secrets.NXP_EMAIL }} + NXP_PASSWORD: ${{ secrets.NXP_PASSWORD }} run: | python3 -m pip install PyYAML - DOCKER_BUILDKIT=1 python3 build_image.py --config ofrak-core-dev.yml --base + DOCKER_BUILDKIT=1 python3 build_image.py --config ofrak-core-dev.yml --base --nxp-email "${NXP_EMAIL}" --nxp-password "${NXP_PASSWORD}" - name: Export base image if: inputs.flush_cache || steps.cache-image.outputs.cache-hit != 'true' || github.event_name == 'schedule' run: | @@ -113,6 +116,9 @@ jobs: | docker load docker images - name: Build Ghidra image + env: + NXP_EMAIL: ${{ secrets.NXP_EMAIL }} + NXP_PASSWORD: ${{ secrets.NXP_PASSWORD }} run: | python3 -m pip install PyYAML DOCKER_BUILDKIT=1 \ @@ -120,7 +126,9 @@ jobs: --config ofrak-ghidra.yml \ --base \ --finish \ - --cache-from redballoonsecurity/ofrak/core-dev-base:latest + --cache-from redballoonsecurity/ofrak/core-dev-base:latest \ + --nxp-email "${NXP_EMAIL}" \ + --nxp-password "${NXP_PASSWORD}" - name: Test documentation run: | docker run \ diff --git a/build_image.py b/build_image.py index 1ebe4d70d..8c6e9ec9e 100644 --- a/build_image.py +++ b/build_image.py @@ -1,3 +1,4 @@ +import tempfile from dataclasses import dataclass from enum import Enum from typing import List, Optional @@ -35,6 +36,8 @@ class OfrakImageConfig: install_target: InstallTarget cache_from: List[str] entrypoint: Optional[str] + nxp_email: Optional[str] + nxp_password: Optional[str] def validate_serial_txt_existence(self): """ @@ -81,11 +84,27 @@ def main(): for cache in config.cache_from: cache_args.append("--cache-from") cache_args.append(cache) + nxp_args = [] + email_file = password_file = None + if config.nxp_email and config.nxp_password: + email_file = tempfile.NamedTemporaryFile(suffix=".txt", mode="w+") + email_file.write(config.nxp_email) + email_file.flush() + password_file = tempfile.NamedTemporaryFile(suffix=".txt", mode="w+") + password_file.write(config.nxp_password) + password_file.flush() + nxp_args = [ + "--secret", + f"id=nxp_email,src={email_file.name}", + "--secret", + f"id=nxp_password,src={password_file.name}", + ] base_command = [ "docker", "build", "--build-arg", "BUILDKIT_INLINE_CACHE=1", + *nxp_args, "--cache-from", f"{full_base_image_name}:master", *cache_args, @@ -108,6 +127,9 @@ def main(): print(f"Error running command: '{' '.join(error.cmd)}'") print(f"Exit status: {error.returncode}") sys.exit(error.returncode) + if email_file and password_file: + email_file.close() + password_file.close() if config.build_finish: full_image_name = "/".join((config.registry, config.image_name)) @@ -146,7 +168,12 @@ def parse_args() -> OfrakImageConfig: default=InstallTarget.DEVELOP.value, ) parser.add_argument("--cache-from", action="append") + parser.add_argument("--nxp-email") + parser.add_argument("--nxp-password") args = parser.parse_args() + if (not not args.nxp_email) ^ (not not args.nxp_password): + raise RuntimeError("Must include the NXP email and password!") + with open(args.config) as file_handle: config_dict = yaml.safe_load(file_handle) image_config = OfrakImageConfig( @@ -161,6 +188,8 @@ def parse_args() -> OfrakImageConfig: InstallTarget(args.target), args.cache_from, config_dict.get("entrypoint"), + args.nxp_email, + args.nxp_password, ) image_config.validate_serial_txt_existence() return image_config diff --git a/disassemblers/ofrak_ghidra/ofrak_ghidra/components/blocks/bb_unpacker.py b/disassemblers/ofrak_ghidra/ofrak_ghidra/components/blocks/bb_unpacker.py index 9f2adfe88..3e588896f 100644 --- a/disassemblers/ofrak_ghidra/ofrak_ghidra/components/blocks/bb_unpacker.py +++ b/disassemblers/ofrak_ghidra/ofrak_ghidra/components/blocks/bb_unpacker.py @@ -5,7 +5,7 @@ from typing import Tuple, Dict, Union, List, Iterable from ofrak.core.architecture import ProgramAttributes -from ofrak_type.architecture import InstructionSet, InstructionSetMode +from ofrak_type.architecture import InstructionSet, InstructionSetMode, SubInstructionSet from ofrak.core.basic_block import BasicBlockUnpacker, BasicBlock from ofrak.core.instruction import Instruction from ofrak.resource import ResourceFactory, Resource @@ -174,6 +174,18 @@ def _asm_fixups( operand = re.sub(r"a([0-7])", r"%A\1", operand) operand = re.sub(r"d([0-7])[bw]?", r"%D\1", operand) operands += operand + elif program_attrs.sub_isa is SubInstructionSet.PPCVLE: + # in Ghidra, offsets from a register like in `se_stw r0,0x9(r1)` are expressed in words. + # so in this example r0 is stored at r1+0x9*4=r1+0x24 + # But it is more natural to express it in bytes, to get the instruction `se_stw r0,0x24(r1)` + # (this is also the convention used by the VLE assembler) + + mnemonic = base_mnemonic + operands = re.sub( + r"(.*, )(0x[0-9]+)(\(r[0-9]+\))", + lambda match: match.group(1) + f"0x{int(match.group(2), 0)*4:x}" + match.group(3), + operands, + ) else: mnemonic = base_mnemonic return mnemonic, operands diff --git a/disassemblers/ofrak_ghidra/ofrak_ghidra/components/ghidra_analyzer.py b/disassemblers/ofrak_ghidra/ofrak_ghidra/components/ghidra_analyzer.py index c596785ef..d95cb75c8 100644 --- a/disassemblers/ofrak_ghidra/ofrak_ghidra/components/ghidra_analyzer.py +++ b/disassemblers/ofrak_ghidra/ofrak_ghidra/components/ghidra_analyzer.py @@ -8,7 +8,7 @@ from typing import Optional, List from ofrak import ResourceFilter -from ofrak.core import CodeRegion +from ofrak.core import CodeRegion, ProgramAttributes, Elf, ElfUnpacker from ofrak.component.analyzer import Analyzer from ofrak.component.modifier import Modifier from ofrak.model.component_model import ComponentConfig @@ -33,6 +33,7 @@ OfrakGhidraMixin, GhidraComponentException, ) +from ofrak_type import InstructionSet, SubInstructionSet LOGGER = logging.getLogger(__name__) @@ -106,7 +107,16 @@ async def analyze( ghidra_project = f"{GHIDRA_REPOSITORY_HOST}:{GHIDRA_REPOSITORY_PORT}/ofrak" - program_name = await self._do_ghidra_import(ghidra_project, full_fname) + program_attributes = None + if resource.has_tag(Elf): + await resource.run(ElfUnpacker) + program_attributes = await resource.analyze(ProgramAttributes) + else: + logging.warning( + f"Could not get ProgramAttributes for resource {resource.get_id()} because it doesn't have the Elf tag." + ) + + program_name = await self._do_ghidra_import(ghidra_project, full_fname, program_attributes) await self._do_ghidra_analyze_and_serve( ghidra_project, program_name, skip_analysis=config is not None ) @@ -116,7 +126,7 @@ async def analyze( return GhidraProject(ghidra_project, f"{GHIDRA_SERVER_HOST}:{GHIDRA_SERVER_PORT}") - async def _do_ghidra_import(self, ghidra_project: str, full_fname: str): + async def _do_ghidra_import(self, ghidra_project: str, full_fname: str, program_attributes): args = [ ghidra_project, "-connect", @@ -127,6 +137,14 @@ async def _do_ghidra_import(self, ghidra_project: str, full_fname: str): "-overwrite", ] + if ( + program_attributes is not None + and program_attributes.isa == InstructionSet.PPC + and program_attributes.sub_isa == SubInstructionSet.PPCVLE + ): + args.extend(["-scriptPath", "'" + (";".join(self._script_directories)) + "'"]) + args.extend(["-preScript", "PreAnalyzePPCVLE.java"]) + cmd_str = " ".join([GHIDRA_HEADLESS_EXEC] + args) LOGGER.debug(f"Running command: {cmd_str}") ghidra_proc = await asyncio.create_subprocess_exec( diff --git a/disassemblers/ofrak_ghidra/ofrak_ghidra/ghidra_scripts/PreAnalyzePPCVLE.java b/disassemblers/ofrak_ghidra/ofrak_ghidra/ghidra_scripts/PreAnalyzePPCVLE.java new file mode 100644 index 000000000..472a9472d --- /dev/null +++ b/disassemblers/ofrak_ghidra/ofrak_ghidra/ghidra_scripts/PreAnalyzePPCVLE.java @@ -0,0 +1,51 @@ +import ghidra.app.script.GhidraScript; +import ghidra.program.model.mem.*; +import ghidra.program.model.lang.*; +import ghidra.program.model.pcode.*; +import ghidra.program.model.util.*; +import ghidra.program.model.reloc.*; +import ghidra.program.model.data.*; +import ghidra.program.model.block.*; +import ghidra.program.model.symbol.*; +import ghidra.program.model.scalar.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.address.*; + +import java.math.BigInteger; + +public class PreAnalyzePPCVLE extends GhidraScript { + + @Override + public void run() throws Exception { + try { + // Set the language to PPC VLE + Language language = (Language) getLanguage(new LanguageID("PowerPC:BE:64:VLE-32addr")); + Program p = currentProgram; + p.setLanguage(language, language.getDefaultCompilerSpec().getCompilerSpecID(), false, monitor); + ProgramContext programContext = p.getProgramContext(); + // Set the vle bit (Ghidra has a "vle" register for that, but on real devices, the VLE + // bit is defined per memory page, as a page attribute bit) so that instructions are + // decoded correctly. + for (Register register : programContext.getContextRegisters()) { + if (register.getName().equals("vle")){ + RegisterValue newValue = new RegisterValue(programContext.getBaseContextRegister()); + BigInteger value = BigInteger.ONE; + newValue = setRegisterValue(newValue, register, value); + programContext.setDefaultDisassemblyContext(newValue); + println("Set the vle bit."); + } + } + } catch(Exception e) { + println(e.toString()); + e.printStackTrace(System.out); + throw e; + } + } + + private RegisterValue setRegisterValue(RegisterValue registerValue, Register register, + BigInteger value) { + RegisterValue newValue = new RegisterValue(register, value); + return registerValue.combineValues(newValue); + } + +} diff --git a/ofrak_core/ofrak/core/elf/analyzer.py b/ofrak_core/ofrak/core/elf/analyzer.py index c4f9a38cf..fccd44a79 100644 --- a/ofrak_core/ofrak/core/elf/analyzer.py +++ b/ofrak_core/ofrak/core/elf/analyzer.py @@ -370,6 +370,7 @@ class ElfProgramAttributesAnalyzer(Analyzer[None, ProgramAttributes]): async def analyze( self, resource: Resource, config: Optional[ComponentConfig] = None ) -> ProgramAttributes: + elf_resource = await resource.view_as(Elf) elf_header = await resource.get_only_descendant_as_view( ElfHeader, r_filter=ResourceFilter.with_tags(ElfHeader) ) @@ -379,7 +380,7 @@ async def analyze( return ProgramAttributes( elf_header.get_isa(), - None, + await elf_resource.get_sub_isa(), elf_basic_header.get_bitwidth(), elf_basic_header.get_endianness(), None, diff --git a/ofrak_core/ofrak/core/elf/model.py b/ofrak_core/ofrak/core/elf/model.py index 313fdb069..43e71a430 100644 --- a/ofrak_core/ofrak/core/elf/model.py +++ b/ofrak_core/ofrak/core/elf/model.py @@ -5,7 +5,7 @@ from ofrak.model.viewable_tag_model import AttributesType -from ofrak_type.architecture import InstructionSet +from ofrak_type.architecture import InstructionSet, SubInstructionSet from ofrak.core.program import Program from ofrak.core.program_section import NamedProgramSection, ProgramSegment from ofrak.model.resource_model import index @@ -863,5 +863,28 @@ async def get_program_header_by_index(self, index: int) -> ElfProgramHeader: ), ) + async def get_sub_isa(self) -> Optional[SubInstructionSet]: + elf_header = await self.get_header() + isa = elf_header.get_isa() + if isa == InstructionSet.PPC: + # We can detect whether the elf is PPC VLE by looking at the section header flags, as described here: https://reverseengineering.stackexchange.com/questions/20863/powerpc-elf32-detecting-vle + PF_PPC_VLE = 0x10000000 + SHF_PPC_VLE = 0x10000000 + ppc_vle = False + program_headers = await self.get_program_headers() + for program_header in program_headers: + if program_header.p_flags & PF_PPC_VLE != 0: + ppc_vle = True + break + if not ppc_vle: + section_headers = await self.get_section_headers() + for section_header in section_headers: + if section_header.sh_flags & SHF_PPC_VLE != 0: + ppc_vle = True + break + if ppc_vle: + return SubInstructionSet.PPCVLE + return None + MagicDescriptionIdentifier.register(Elf, lambda s: s.startswith("ELF ")) diff --git a/ofrak_patch_maker/Dockerstub b/ofrak_patch_maker/Dockerstub index c6d3ac340..217db529a 100644 --- a/ofrak_patch_maker/Dockerstub +++ b/ofrak_patch_maker/Dockerstub @@ -75,3 +75,23 @@ RUN apt-get update && apt-get install -y gcc-avr binutils-avr avr-libc RUN if [ "$TARGETARCH" = "amd64" ]; then \ apt-get update && apt-get install -y gcc-10-powerpc-linux-gnu; \ fi; + +#PPCVLE 4 NXP GCC Fork +# Only runs if the NXP email and password to log in and download the toolchain are passed to build_image.py via the CLI flags +ARG OFRAK_DIR=. +COPY $OFRAK_DIR/ofrak_patch_maker/download_ppcvle.py /tmp/ +RUN --mount=type=secret,id=nxp_email,dst=/tmp/nxp_email.txt \ + --mount=type=secret,id=nxp_password,dst=/tmp/nxp_password.txt \ + test -f /tmp/nxp_email.txt && \ + test -f /tmp/nxp_password.txt && \ + python3 -m pip install playwright && \ + playwright install --with-deps chromium && \ + python3 /tmp/download_ppcvle.py "$(cat /tmp/nxp_email.txt)" "$(cat /tmp/nxp_password.txt)" && \ + cd /tmp && \ + unzip -q gcc-4.9.4-Ee200-eabivle-x86_64-linux-g2724867.zip && \ + mv powerpc-eabivle-4_9 /opt/rbs/toolchain/ && \ + rm gcc-4.9.4-Ee200-eabivle-x86_64-linux-g2724867.zip && \ + dpkg --add-architecture i386 && \ + apt-get update && \ + apt-get install -y libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 \ + || true diff --git a/ofrak_patch_maker/download_ppcvle.py b/ofrak_patch_maker/download_ppcvle.py new file mode 100644 index 000000000..08b0ae863 --- /dev/null +++ b/ofrak_patch_maker/download_ppcvle.py @@ -0,0 +1,53 @@ +import argparse +import os + +from playwright.sync_api import sync_playwright + + +START_URL = "https://www.nxp.com/design/software/development-software/s32-design-studio-ide/s32-design-studio-for-power-architecture:S32DS-PA" + + +def run(page, email: str, password: str, outfile: str) -> None: + print(f"Going to page {START_URL}", flush=True) + page.goto(START_URL) + page.get_by_role("listitem").filter( + has_text="Build Tools NXP Embedded GCC for Power Architecture" + ).filter(has_text="Linux").get_by_role("link", name="Download", exact=True).click() + + print("Signing in", flush=True) + page.locator("#username").click() + page.keyboard.type(email) + page.locator("#password").click() + page.keyboard.type(password) + page.get_by_role("button", name="SIGN IN").click() + + print("Accepting terms and conditions", flush=True) + page.get_by_role("button", name="I Accept").click() + + print("Waiting for download", flush=True) + with page.expect_download() as download_info: + # Download begins when the page is loaded + pass + os.rename(download_info.value.path(), outfile) + + print(f"Complete! Saved to {outfile}", flush=True) + + +def main(args): + with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True) + context = browser.new_context() + page = context.new_page() + run(page, args.email, args.password, args.outfile) + context.close() + browser.close() + + +if __name__ == "__main__": + argument_parser = argparse.ArgumentParser() + argument_parser.add_argument("email") + argument_parser.add_argument("password") + argument_parser.add_argument( + "-o", "--outfile", default="/tmp/gcc-4.9.4-Ee200-eabivle-x86_64-linux-g2724867.zip" + ) + main(argument_parser.parse_args()) diff --git a/ofrak_patch_maker/ofrak_patch_maker/toolchain.conf b/ofrak_patch_maker/ofrak_patch_maker/toolchain.conf index bac54c64b..1b86c0041 100644 --- a/ofrak_patch_maker/ofrak_patch_maker/toolchain.conf +++ b/ofrak_patch_maker/ofrak_patch_maker/toolchain.conf @@ -50,6 +50,12 @@ COMPILER = /usr/bin/powerpc-linux-gnu-gcc-10 LINKER = /usr/bin/powerpc-linux-gnu-ld BIN_PARSER = /usr/bin/powerpc-linux-gnu-objdump +[GNU_PPCVLE_4] +PREPROCESSOR = /opt/rbs/toolchain/powerpc-eabivle-4_9/bin/powerpc-eabivle-gcc +COMPILER = /opt/rbs/toolchain/powerpc-eabivle-4_9/bin/powerpc-eabivle-gcc +LINKER = /opt/rbs/toolchain/powerpc-eabivle-4_9/bin/powerpc-eabivle-ld +BIN_PARSER = /opt/rbs/toolchain/powerpc-eabivle-4_9/bin/powerpc-eabivle-objdump + [ASM] ARM_ASM_PATH = /opt/rbs/toolchain/gcc-arm-none-eabi-10-2020-q4-major/bin/arm-none-eabi-as X86_64_ASM_PATH = /opt/rbs/toolchain/binutils-2.34/gas/as-new @@ -57,3 +63,4 @@ M68K_ASM_PATH = /usr/bin/m68k-linux-gnu-as AARCH64_ASM_PATH = /usr/bin/aarch64-linux-gnu-as AVR_ASM_PATH = /usr/bin/avr-as PPC_ASM_PATH = /usr/bin/powerpc-linux-gnu-as +PPCVLE_ASM_PATH = /opt/rbs/toolchain/powerpc-eabivle-4_9/bin/powerpc-eabivle-as diff --git a/ofrak_patch_maker/ofrak_patch_maker/toolchain/abstract.py b/ofrak_patch_maker/ofrak_patch_maker/toolchain/abstract.py index 61553bb32..a14c07d72 100644 --- a/ofrak_patch_maker/ofrak_patch_maker/toolchain/abstract.py +++ b/ofrak_patch_maker/ofrak_patch_maker/toolchain/abstract.py @@ -13,7 +13,7 @@ from ofrak_patch_maker.binary_parser.abstract import AbstractBinaryFileParser from ofrak_patch_maker.toolchain.model import Segment, ToolchainConfig from ofrak_patch_maker.toolchain.utils import get_repository_config -from ofrak_type.architecture import InstructionSet +from ofrak_type.architecture import InstructionSet, SubInstructionSet from ofrak_type.bit_width import BitWidth from ofrak_type.memory_permissions import MemoryPermissions from ofrak_type.symbol_type import LinkableSymbolType @@ -156,6 +156,11 @@ def _assembler_path(self) -> str: and self._processor.bit_width == BitWidth.BIT_64 ): assembler_path = "X86_64_ASM_PATH" + elif ( + self._processor.isa == InstructionSet.PPC + and self._processor.sub_isa == SubInstructionSet.PPCVLE + ): + assembler_path = "PPCVLE_ASM_PATH" else: assembler_path = f"{self._processor.isa.value.upper()}_ASM_PATH" return get_repository_config("ASM", assembler_path) diff --git a/ofrak_patch_maker/ofrak_patch_maker/toolchain/gnu_ppc.py b/ofrak_patch_maker/ofrak_patch_maker/toolchain/gnu_ppc.py index a92ef7dc3..853015133 100644 --- a/ofrak_patch_maker/ofrak_patch_maker/toolchain/gnu_ppc.py +++ b/ofrak_patch_maker/ofrak_patch_maker/toolchain/gnu_ppc.py @@ -2,9 +2,9 @@ from typing import Optional from ofrak_patch_maker.binary_parser.gnu import GNU_V10_ELF_Parser -from ofrak_patch_maker.toolchain.gnu import GNU_10_Toolchain +from ofrak_patch_maker.toolchain.gnu import GNU_10_Toolchain, Abstract_GNU_Toolchain from ofrak_patch_maker.toolchain.model import ToolchainConfig -from ofrak_type import ArchInfo, InstructionSet, MemoryPermissions +from ofrak_type import ArchInfo, InstructionSet, MemoryPermissions, SubInstructionSet class GNU_PPC_LINUX_10_Toolchain(GNU_10_Toolchain): @@ -83,3 +83,81 @@ def ld_generate_placeholder_reloc_sections(self): regions.append(got_region) sections.append(self._ld_generate_got_section(got_name)) return regions, sections + + +class GNU_PPCVLE_4_Toolchain(Abstract_GNU_Toolchain): + binary_file_parsers = [GNU_V10_ELF_Parser()] + + def __init__( + self, + processor: ArchInfo, + toolchain_config: ToolchainConfig, + logger: logging.Logger = logging.getLogger(__name__), + ): + super().__init__(processor, toolchain_config, logger=logger) + if self._config.hard_float: + self._compiler_flags.append("-mhard-float") + else: + self._compiler_flags.append("-msoft-float") + self._assembler_flags.append("-mvle") + + @property # pragma: no cover + def segment_alignment(self) -> int: + return 4 + + @property + def name(self) -> str: + return "GNU_PPCVLE_4" + + def _get_assembler_target(self, processor: ArchInfo) -> Optional[str]: + if processor.isa != InstructionSet.PPC or processor.sub_isa != SubInstructionSet.PPCVLE: + raise ValueError( + f"The GNU PPCVLE toolchain does not support ISAs that are not PPCVLE. " + f"(Got: {processor.isa.name})" + ) + if self._config.assembler_target: + return self._config.assembler_target + + # PPCVLE GNU 4 does not implement -march + return None + + @staticmethod # pragma: no cover + def _ld_generate_rel_dyn_section( + memory_region_name: str, + ) -> str: + rel_dyn_section_name = ".rela.dyn" + return ( + f" {rel_dyn_section_name} : {{\n" + f" *.o({rel_dyn_section_name})\n" + f" }} > {memory_region_name}" + ) + + @staticmethod # pragma: no cover + def _ld_generate_got_plt_section( + memory_region_name: str, + ) -> str: + got_plt_section_name = ".got2" + return ( + f" {got_plt_section_name} : {{\n" + f" *.o({got_plt_section_name})\n" + f" }} > {memory_region_name}" + ) + + def _ld_generate_got_region(self, vm_address, length): # pragma: no cover + region_name = '".got_mem"' + perms_string = self._ld_perm2str(MemoryPermissions.R) + return ( + f" {region_name} ({perms_string}) : ORIGIN = {hex(vm_address)}, " + f"LENGTH = {hex(length)}", + region_name, + ) + + def ld_generate_placeholder_reloc_sections(self): # pragma: no cover + regions, sections = super().ld_generate_placeholder_reloc_sections() + ( + got_region, + got_name, + ) = self._ld_generate_got_region(0xDEADBEEF + 0x30000, 0x1000) + regions.append(got_region) + sections.append(self._ld_generate_got_section(got_name)) + return regions, sections diff --git a/ofrak_patch_maker/ofrak_patch_maker_test/example_2/bom1.vle.S b/ofrak_patch_maker/ofrak_patch_maker_test/example_2/bom1.vle.S new file mode 100644 index 000000000..f418190de --- /dev/null +++ b/ofrak_patch_maker/ofrak_patch_maker_test/example_2/bom1.vle.S @@ -0,0 +1,7 @@ +.global _main1 +_main1: + add 1, 1, 2 + add 1, 1, 2 + add 1, 1, 2 + add 1, 1, 2 + add 1, 1, 2 diff --git a/ofrak_patch_maker/ofrak_patch_maker_test/example_2/bom2.vle.S b/ofrak_patch_maker/ofrak_patch_maker_test/example_2/bom2.vle.S new file mode 100644 index 000000000..be797fe14 --- /dev/null +++ b/ofrak_patch_maker/ofrak_patch_maker_test/example_2/bom2.vle.S @@ -0,0 +1,7 @@ +.global _main2 +_main2: + add 2, 2, 3 + add 2, 2, 3 + add 2, 2, 3 + add 2, 2, 3 + add 2, 2, 3 diff --git a/ofrak_patch_maker/ofrak_patch_maker_test/example_2/bom3.vle.S b/ofrak_patch_maker/ofrak_patch_maker_test/example_2/bom3.vle.S new file mode 100644 index 000000000..641a56e04 --- /dev/null +++ b/ofrak_patch_maker/ofrak_patch_maker_test/example_2/bom3.vle.S @@ -0,0 +1,7 @@ +.global _main3 +_main3: + add 3, 3, 4 + add 3, 3, 4 + add 3, 3, 4 + add 3, 3, 4 + add 3, 3, 4 diff --git a/ofrak_patch_maker/ofrak_patch_maker_test/test_alignment/patch_vle.as b/ofrak_patch_maker/ofrak_patch_maker_test/test_alignment/patch_vle.as new file mode 100644 index 000000000..412bc43ce --- /dev/null +++ b/ofrak_patch_maker/ofrak_patch_maker_test/test_alignment/patch_vle.as @@ -0,0 +1,2 @@ +.align 1 +se_mflr 0 diff --git a/ofrak_patch_maker/ofrak_patch_maker_test/test_ppc_vle_toolchain.py b/ofrak_patch_maker/ofrak_patch_maker_test/test_ppc_vle_toolchain.py new file mode 100644 index 000000000..c5d74fa73 --- /dev/null +++ b/ofrak_patch_maker/ofrak_patch_maker_test/test_ppc_vle_toolchain.py @@ -0,0 +1,164 @@ +import os +import tempfile +from warnings import warn + +import pytest + +from ofrak_patch_maker.model import PatchRegionConfig +from ofrak_patch_maker.patch_maker import PatchMaker +from ofrak_patch_maker.toolchain.model import ( + ToolchainConfig, + BinFileType, + CompilerOptimizationLevel, + Segment, +) +from ofrak_patch_maker.toolchain.utils import get_file_format +from ofrak_patch_maker_test import ToolchainUnderTest, CURRENT_DIRECTORY + +from ofrak_patch_maker.toolchain.gnu_ppc import GNU_PPCVLE_4_Toolchain +from ofrak_type import ( + ArchInfo, + InstructionSet, + BitWidth, + Endianness, + SubInstructionSet, + MemoryPermissions, +) + +from ofrak_patch_maker_test.toolchain_asm import ( + run_monkey_patch_test, +) + +from ofrak_patch_maker_test.toolchain_c import ( + run_bounds_check_test, + run_hello_world_test, +) + +PPC_EXTENSION = ".vle" + + +INSTALL_TOOLCHAIN_MESSAGE = f""" +The NXP PPC VLE toolchain was not installed as part af the container build, because it requires signing-up and manually downloading the toolchain. +Download the toolchain into your OFRAK directory from here: +https://www.nxp.com/design/software/development-software/s32-design-studio-ide/s32-design-studio-for-power-architecture:S32DS-PA +Then rebuild the docker container, or refer to the Dockerfile for installation instructions. +""" + + +def check_toolchain_installed(toolchain_under_test: ToolchainUnderTest) -> bool: + tc_config = ToolchainConfig( + file_format=BinFileType.ELF, + force_inlines=True, + relocatable=False, + no_std_lib=True, + no_jump_tables=True, + no_bss_section=True, + compiler_optimization_level=CompilerOptimizationLevel.FULL, + ) + compiler_path_exist = os.path.exists( + toolchain_under_test.toolchain(toolchain_under_test.proc, tc_config)._compiler_path + ) + if not compiler_path_exist: + warn(INSTALL_TOOLCHAIN_MESSAGE) + return compiler_path_exist + + +@pytest.fixture( + params=[ + ToolchainUnderTest( + GNU_PPCVLE_4_Toolchain, + ArchInfo( + InstructionSet.PPC, + SubInstructionSet.PPCVLE, + BitWidth.BIT_32, + Endianness.BIG_ENDIAN, + None, + ), + PPC_EXTENSION, + ), + ] +) +def toolchain_under_test(request) -> ToolchainUnderTest: + return request.param + + +# ASM Tests +# def test_challenge_3_reloc_toy_example(toolchain_under_test: ToolchainUnderTest): +# # TODO +# pass + + +def test_monkey_patch(toolchain_under_test: ToolchainUnderTest): + if not check_toolchain_installed(toolchain_under_test): + pytest.skip(INSTALL_TOOLCHAIN_MESSAGE) + run_monkey_patch_test(toolchain_under_test) + + +# C Tests +def test_bounds_check(toolchain_under_test: ToolchainUnderTest): + if not check_toolchain_installed(toolchain_under_test): + pytest.skip(INSTALL_TOOLCHAIN_MESSAGE) + run_bounds_check_test(toolchain_under_test) + + +def test_hello_world(toolchain_under_test: ToolchainUnderTest): + if not check_toolchain_installed(toolchain_under_test): + pytest.skip(INSTALL_TOOLCHAIN_MESSAGE) + run_hello_world_test(toolchain_under_test) + + +def test_vle_alignment(toolchain_under_test: ToolchainUnderTest): + if not check_toolchain_installed(toolchain_under_test): + pytest.skip(INSTALL_TOOLCHAIN_MESSAGE) + tc_config = ToolchainConfig( + file_format=BinFileType.ELF, + force_inlines=True, + relocatable=False, + no_std_lib=True, + no_jump_tables=True, + no_bss_section=True, + create_map_files=True, + compiler_optimization_level=CompilerOptimizationLevel.NONE, + debug_info=True, + check_overlap=False, + hard_float=True, + ) + + build_dir = tempfile.mkdtemp() + + patch_maker = PatchMaker( + toolchain=toolchain_under_test.toolchain(toolchain_under_test.proc, tc_config), + build_dir=build_dir, + ) + patch_source = os.path.join(CURRENT_DIRECTORY, "test_alignment/patch_vle.as") + patch_bom = patch_maker.make_bom("patch", [patch_source], [], []) + + # Grab the resulting object paths and re-map them to the segments we chose for each source file. + patch_object = patch_bom.object_map[patch_source] + text_segment_patch = Segment( + segment_name=".text", + vm_address=0x51A, + offset=0, + is_entry=False, + length=2, + access_perms=MemoryPermissions.RX, + ) + segment_dict = { + patch_object.path: (text_segment_patch,), + } + + exec_path = os.path.join(build_dir, "patch_exec") + # Generate a PatchRegionConfig from your segment Dict. + # This data structure informs ld script generation which regions to create for every segment. + p = PatchRegionConfig(patch_bom.name + "_patch", segment_dict) + fem = patch_maker.make_fem([(patch_bom, p)], exec_path) + assert os.path.exists(exec_path) + assert get_file_format(exec_path) == tc_config.file_format + code_segments = [s for s in fem.executable.segments if s.access_perms == MemoryPermissions.RX] + assert len(code_segments) == 1 + assert code_segments[0].vm_address == 0x51A + assert code_segments[0].length == 2 + with open(exec_path, "rb") as f: + dat = f.read() + code_offset = code_segments[0].offset + assert dat[code_offset : code_offset + 2] == b"\x00\x80" diff --git a/ofrak_patch_maker/ofrak_patch_maker_test/test_symbol_parsing.py b/ofrak_patch_maker/ofrak_patch_maker_test/test_symbol_parsing.py index c1794fed7..220bbb6b4 100644 --- a/ofrak_patch_maker/ofrak_patch_maker_test/test_symbol_parsing.py +++ b/ofrak_patch_maker/ofrak_patch_maker_test/test_symbol_parsing.py @@ -9,7 +9,7 @@ from ofrak_patch_maker.toolchain.gnu_aarch64 import GNU_AARCH64_LINUX_10_Toolchain from ofrak_patch_maker.toolchain.gnu_arm import GNU_ARM_NONE_EABI_10_2_1_Toolchain from ofrak_patch_maker.toolchain.gnu_avr import GNU_AVR_5_Toolchain -from ofrak_patch_maker.toolchain.gnu_ppc import GNU_PPC_LINUX_10_Toolchain +from ofrak_patch_maker.toolchain.gnu_ppc import GNU_PPC_LINUX_10_Toolchain, GNU_PPCVLE_4_Toolchain from ofrak_patch_maker.toolchain.gnu_x64 import GNU_X86_64_LINUX_EABI_10_3_0_Toolchain from ofrak_patch_maker.toolchain.llvm_12 import LLVM_12_0_1_Toolchain from ofrak_patch_maker.toolchain.model import ( @@ -89,6 +89,16 @@ def full_label(self) -> str: GNU_PPC_LINUX_10_Toolchain, ArchInfo(InstructionSet.PPC, None, BitWidth.BIT_32, Endianness.BIG_ENDIAN, None), ), + ( + GNU_PPCVLE_4_Toolchain, + ArchInfo( + InstructionSet.PPC, + SubInstructionSet.PPCVLE, + BitWidth.BIT_32, + Endianness.BIG_ENDIAN, + None, + ), + ), ( GNU_AVR_5_Toolchain, ArchInfo( diff --git a/ofrak_patch_maker/ofrak_patch_maker_test/toolchain_c.py b/ofrak_patch_maker/ofrak_patch_maker_test/toolchain_c.py index 7ced85369..1b9050a56 100644 --- a/ofrak_patch_maker/ofrak_patch_maker_test/toolchain_c.py +++ b/ofrak_patch_maker/ofrak_patch_maker_test/toolchain_c.py @@ -2,6 +2,7 @@ import os import tempfile from ofrak_patch_maker.toolchain.gnu_avr import GNU_AVR_5_Toolchain +from ofrak_patch_maker.toolchain.gnu_ppc import GNU_PPCVLE_4_Toolchain from ofrak_patch_maker.toolchain.gnu_x64 import GNU_X86_64_LINUX_EABI_10_3_0_Toolchain from ofrak_patch_maker_test import ToolchainUnderTest from ofrak_type.architecture import InstructionSet @@ -95,6 +96,9 @@ def run_hello_world_test(toolchain_under_test: ToolchainUnderTest): if toolchain_under_test.toolchain == GNU_AVR_5_Toolchain: relocatable = False base_symbols = {"__mulhi3": 0x1234} # Dummy address to fix missing symbol + elif toolchain_under_test.toolchain == GNU_PPCVLE_4_Toolchain: + relocatable = True + base_symbols = {"__eabi": 0x1234} else: relocatable = True base_symbols = None diff --git a/ofrak_type/ofrak_type/architecture.py b/ofrak_type/ofrak_type/architecture.py index 4a34b42bc..fa9eca94f 100644 --- a/ofrak_type/ofrak_type/architecture.py +++ b/ofrak_type/ofrak_type/architecture.py @@ -87,6 +87,7 @@ class SubInstructionSet(Enum): AVRXMEGA7 = "avrxmega7" AVRTINY = "avrtiny" AVR1 = "avr1" # assembler only + PPCVLE = "ppc-vle" # PowerPC VLE (Variable Length Encoding) class InstructionSetMode(Enum): diff --git a/ofrak_type/ofrak_type/memory_permissions.py b/ofrak_type/ofrak_type/memory_permissions.py index 92af59123..a24f46e7d 100644 --- a/ofrak_type/ofrak_type/memory_permissions.py +++ b/ofrak_type/ofrak_type/memory_permissions.py @@ -13,6 +13,10 @@ class MemoryPermissions(Enum): RW = R + W RX = R + X RWX = R + W + X + VLE = 0x10000000 + X_VLE = X + VLE + RX_VLE = R + X + VLE + RWX_VLE = R + W + X + VLE def as_str(self) -> str: string = ""