diff --git a/.gitignore b/.gitignore index aaa1e38..dfe5d47 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ core *.log docs/source/start.md /src/copapy/_version.py +sketch*.py diff --git a/src/copapy/_binwrite.py b/src/copapy/_binwrite.py index ff77918..a12d82b 100644 --- a/src/copapy/_binwrite.py +++ b/src/copapy/_binwrite.py @@ -6,7 +6,9 @@ ByteOrder = Literal['little', 'big'] Command = Enum('Command', [('ALLOCATE_DATA', 1), ('COPY_DATA', 2), ('ALLOCATE_CODE', 3), ('COPY_CODE', 4), - ('PATCH_FUNC', 0x1000), ('PATCH_OBJECT', 0x2000), + ('PATCH_FUNC', 0x1000), + ('PATCH_FUNC_ARM32_THM', 0x1005), + ('PATCH_OBJECT', 0x2000), ('PATCH_OBJECT_HI21', 0x2001), ('PATCH_OBJECT_ABS', 0x2002), ('PATCH_OBJECT_REL', 0x2003), diff --git a/src/copapy/_compiler.py b/src/copapy/_compiler.py index cbade28..cd42d41 100644 --- a/src/copapy/_compiler.py +++ b/src/copapy/_compiler.py @@ -393,13 +393,14 @@ def compile_to_dag(node_list: Iterable[Node], sdb: stencil_database) -> tuple[bi # assemble stencils to main program and patch stencils data = sdb.get_function_code('entry_function_shell', 'start') data_list.append(data) + print(f"* entry_function_shell (0) " + ' '.join(f'{d:02X}' for d in data)) offset = aux_func_len + len(data) for associated_net, node in extended_output_ops: assert node.name in sdb.stencil_definitions, f"- Warning: {node.name} stencil not found" data = sdb.get_stencil_code(node.name) data_list.append(data) - #print(f"* {node.name} ({offset}) " + ' '.join(f'{d:02X}' for d in data)) + print(f"* {node.name} ({offset}) " + ' '.join(f'{d:02X}' for d in data)) for reloc in sdb.get_relocations(node.name, stencil=True): if reloc.target_symbol_info in ('STT_OBJECT', 'STT_NOTYPE', 'STT_SECTION'): @@ -451,10 +452,8 @@ def compile_to_dag(node_list: Iterable[Node], sdb: stencil_database) -> tuple[bi #print('--> ', name, list(sdb.get_relocations(name))) for reloc in sdb.get_relocations(name): - #assert reloc.target_symbol_info != 'STT_FUNC', "Not tested yet!" - if not reloc.target_section_index: - assert reloc.pelfy_reloc.type == 'R_ARM_V4BX' + assert reloc.pelfy_reloc.type == 'R_ARM_V4BX', (reloc.pelfy_reloc.type, name) elif reloc.target_symbol_info in {'STT_OBJECT', 'STT_NOTYPE', 'STT_SECTION'}: # Patch constants/variable addresses on heap @@ -489,6 +488,6 @@ def compile_to_dag(node_list: Iterable[Node], sdb: stencil_database) -> tuple[bi dw.write_int(patch.value, signed=True) dw.write_com(binw.Command.ENTRY_POINT) - dw.write_int(aux_func_len) + dw.write_int(aux_func_len + sdb.thumb_mode) return dw, variables diff --git a/src/copapy/_stencils.py b/src/copapy/_stencils.py index b7864c0..f464b3e 100644 --- a/src/copapy/_stencils.py +++ b/src/copapy/_stencils.py @@ -121,6 +121,7 @@ class stencil_database(): var_size (dict[str, int]): dictionary of object names and their sizes byteorder (ByteOrder): byte order of the ELF file elf (elf_file): the loaded ELF file + thumb_mode (int): 1 if ARM in thumb mode, 0 otherwise """ def __init__(self, obj_file: str | bytes): @@ -147,6 +148,11 @@ class stencil_database(): # if s.info == 'STT_OBJECT'} self.byteorder: ByteOrder = self.elf.byteorder + self.arm = '.ARM.attributes' in self.elf.sections + + # Returns 1 for ARM in thumb mode, 0 otherwise + self.thumb_mode = self.elf.symbols['entry_function_shell'].fields['st_value'] & 1 + #for name in self.function_definitions.keys(): # sym = self.elf.symbols[name] # sym.relocations @@ -314,6 +320,13 @@ class stencil_database(): patch_value = symbol_address + pr.fields['r_addend'] symbol_type = symbol_type + 0x03 # Relative to data section + elif pr.type.endswith('_THM_JUMP24') or pr.type.endswith('_THM_CALL'): + # R_ARM_THM_JUMP24 + # S + A - P + #assert pr.fields['r_addend'] == 0, pr.fields['r_addend'] + patch_value = symbol_address - (patch_offset + 4) #+ pr.fields['r_addend'] + symbol_type = symbol_type + 0x05 # PATCH_FUNC_ARM32_THM + else: raise NotImplementedError(f"Relocation type {pr.type} in {relocation.pelfy_reloc.target_section.name} pointing to {relocation.pelfy_reloc.symbol.name} not implemented") @@ -334,7 +347,11 @@ class stencil_database(): func = self.elf.symbols[name] start_stencil, end_stencil = get_stencil_position(func) assert func.section - start_index = func.section['sh_offset'] + func['st_value'] + start_stencil + + # For arm functions, mask out the thumb mode bit + function_offset = func.fields['st_value'] & ~int(self.arm) + + start_index = func.section['sh_offset'] + function_offset + start_stencil lengths = end_stencil - start_stencil self._stencil_cache[name] = (start_index, lengths) @@ -403,6 +420,13 @@ class stencil_database(): func = self.elf.symbols[name] assert func.info == 'STT_FUNC', f"{name} is not a function" + # For arm functions, mask out the thumb mode bit + function_offset = func.fields['st_value'] & ~int(self.arm) + + start_index = func.section['sh_offset'] + function_offset + lengths = end_stencil - start_stencil + + #return self.elf.read_bytes(start_index, func.fields['st_size']) if part == 'start': index = get_last_call_in_function(func) return func.data[:index] diff --git a/src/coparun/runmem.c b/src/coparun/runmem.c index 1165956..e5180e5 100644 --- a/src/coparun/runmem.c +++ b/src/coparun/runmem.c @@ -50,6 +50,41 @@ void patch_arm32_abs(uint8_t *patch_addr, uint32_t imm16) *((uint32_t *)patch_addr) = instr; } +void patch_arm_thm_jump24(uint8_t *patch_addr, int32_t imm24) +{ + // Read the 32-bit instruction (two halfwords) + uint16_t *instr16 = (uint16_t *)patch_addr; + uint16_t first_half = instr16[0]; + uint16_t second_half = instr16[1]; + + // Thumb branch instructions always have LSB = 0 (halfword aligned) + // The imm24 offset in Thumb is shifted right by 1 when encoded + int32_t offset = imm24 >> 1; + + // Split into S, J1, J2, imm10, imm11 + uint32_t S = (offset >> 23) & 0x1; + uint32_t I1 = (offset >> 22) & 0x1; + uint32_t I2 = (offset >> 21) & 0x1; + uint32_t imm10 = (offset >> 11) & 0x3FF; + uint32_t imm11 = offset & 0x7FF; + + // Re-encode J1 and J2 + uint32_t J1 = (~(I1 ^ S)) & 0x1; + uint32_t J2 = (~(I2 ^ S)) & 0x1; + + // Clear old imm fields + first_half &= 0xF800; // Keep upper 5 bits + second_half &= 0xD000; // Keep upper 5 bits + + // Set new imm fields + first_half |= (S << 10) | imm10; + second_half |= (J1 << 13) | (J2 << 11) | imm11; + + // Write back + instr16[0] = first_half; + instr16[1] = second_half; +} + void free_memory(runmem_t *context) { deallocate_memory(context->executable_memory, context->executable_memory_len); deallocate_memory(context->data_memory, context->data_memory_len); @@ -180,6 +215,16 @@ int parse_commands(runmem_t *context, uint8_t *bytes) { patch_arm32_abs(context->executable_memory + offs, (uint32_t)((uintptr_t)(context->data_memory + value) & patch_mask) / (uint32_t)patch_scale); break; + case PATCH_FUNC_ARM32_THM: + offs = *(uint32_t*)bytes; bytes += 4; + patch_mask = *(uint32_t*)bytes; bytes += 4; + patch_scale = *(int32_t*)bytes; bytes += 4; + value = *(int32_t*)bytes; bytes += 4; + LOG("PATCH_FUNC_ARM32_THM patch_offs=%i patch_mask=%#08x scale=%i value=%i\n", + offs, patch_mask, patch_scale, value); + patch_arm_thm_jump24(context->executable_memory + offs, value); + break; + case ENTRY_POINT: rel_entr_point = *(uint32_t*)bytes; bytes += 4; context->entr_point = (entry_point_t)(context->executable_memory + rel_entr_point); diff --git a/src/coparun/runmem.h b/src/coparun/runmem.h index 31e9905..371f2f3 100644 --- a/src/coparun/runmem.h +++ b/src/coparun/runmem.h @@ -20,6 +20,7 @@ #define ALLOCATE_CODE 3 #define COPY_CODE 4 #define PATCH_FUNC 0x1000 +#define PATCH_FUNC_ARM32_THM 0x1005 #define PATCH_OBJECT 0x2000 #define PATCH_OBJECT_HI21 0x2001 #define PATCH_OBJECT_ABS 0x2002 diff --git a/tests/test_ops_armv7thunb.py b/tests/test_ops_armv7thunb.py new file mode 100644 index 0000000..ac67e51 --- /dev/null +++ b/tests/test_ops_armv7thunb.py @@ -0,0 +1,171 @@ +from copapy import NumLike, iif, value +from copapy.backend import Store, compile_to_dag, add_read_value_remote +import subprocess +from copapy import _binwrite +import copapy.backend as backend +import os +import warnings +import re +import struct +import pytest +import copapy as cp + +if os.name == 'nt': + # On Windows wsl and qemu-user is required: + # sudo apt install qemu-user + qemu_command = ['wsl', 'qemu-arm'] +else: + qemu_command = ['qemu-arm'] + + +def parse_results(log_text: str) -> dict[int, bytes]: + regex = r"^READ_DATA offs=(\d*) size=(\d*) data=(.*)$" + matches = re.finditer(regex, log_text, re.MULTILINE) + var_dict: dict[int, bytes] = {} + + for match in matches: + value_str: list[str] = match.group(3).strip().split(' ') + #print('--', value_str) + value = bytes(int(v, base=16) for v in value_str) + if len(value) <= 8: + var_dict[int(match.group(1))] = value + + return var_dict + + +def run_command(command: list[str]) -> str: + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8', check=False) + assert result.returncode != 11, f"SIGSEGV (segmentation fault)\n -Error occurred: {result.stderr}\n -Output: {result.stdout}" + assert result.returncode == 0, f"\n -Error occurred: {result.stderr}\n -Output: {result.stdout}" + return result.stdout + + +def check_for_qemu() -> bool: + command = qemu_command + ['--version'] + try: + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + except Exception: + return False + return result.returncode == 0 + + +def function1(c1: NumLike) -> list[NumLike]: + return [c1 / 4, c1 / -4, c1 // 4, c1 // -4, (c1 * -1) // 4, + c1 * 4, c1 * -4, + c1 + 4, c1 - 4, + c1 > 2, c1 > 100, c1 < 4, c1 < 100] + + +def function2(c1: NumLike) -> list[NumLike]: + return [c1 * 4.44, c1 * -4.44] + + +def function3(c1: NumLike) -> list[NumLike]: + return [c1 / 4] + + +def function4(c1: NumLike) -> list[NumLike]: + return [c1 == 9, c1 == 4, c1 != 9, c1 != 4] + + +def function5(c1: NumLike) -> list[NumLike]: + return [c1 == True, c1 == False, c1 != True, c1 != False, c1 / 2, c1 + 2] + + +def function6(c1: NumLike) -> list[NumLike]: + return [c1 == True] + + +def iiftests(c1: NumLike) -> list[NumLike]: + return [iif(c1 > 5, 8, 9), + iif(c1 < 5, 8.5, 9.5), + iif(1 > 5, 3.3, 8.8) + c1, + iif(1 < 5, c1 * 3.3, 8.8), + iif(c1 < 5, c1 * 3.3, 8.8)] + + +@pytest.mark.runner +def test_compile(): + c_i = value(9) + c_f = value(1.111) + c_b = value(True) + + #ret_test = function1(c_i) + function1(c_f) + function2(c_i) + function2(c_f) + function3(c_i) + function4(c_i) + function5(c_b) + [value(9) % 2] + iiftests(c_i) + iiftests(c_f) + [cp.asin(c_i/10)] + #ret_ref = function1(9) + function1(1.111) + function2(9) + function2(1.111) + function3(9) + function4(9) + function5(True) + [9 % 2] + iiftests(9) + iiftests(1.111) + [cp.asin(9/10)] + + ret_test = (c_i * 100 + 5,) + ret_ref = (9 * 100 + 5,) + + out = [Store(r) for r in ret_test] + + sdb = backend.stencil_db_from_package('armv7thumb') + dw, variables = compile_to_dag(out, sdb) + + #dw.write_com(_binwrite.Command.READ_DATA) + #dw.write_int(0) + #dw.write_int(28) + + # run program command + #dw.write_com(_binwrite.Command.RUN_PROG) + dw.write_com(_binwrite.Command.DUMP_CODE) + + for v in ret_test: + assert isinstance(v, value) + add_read_value_remote(dw, variables, v.net) + + #dw.write_com(_binwrite.Command.READ_DATA) + #dw.write_int(0) + #dw.write_int(28) + + dw.write_com(_binwrite.Command.END_COM) + + #print('* Data to runner:') + #dw.print() + + dw.to_file('build/runner/test-armv7thumb.copapy') + + if not check_for_qemu(): + warnings.warn("qemu-armv7 not found, armv7 test skipped!", UserWarning) + return + if not os.path.isfile('build/runner/coparun-armv7thumb'): + warnings.warn("armv7thumb runner not found, armv7thumb test skipped!", UserWarning) + return + + command = qemu_command + ['build/runner/coparun-armv7thumb', 'build/runner/test-armv7thumb.copapy'] + ['build/runner/test.copapy-armv7thumb.bin'] + #try: + result = run_command(command) + #except FileNotFoundError: + # warnings.warn(f"Test skipped, executable not found.", UserWarning) + # return + + print('* Output from runner:\n--') + print(result) + print('--') + + assert 'Return value: 1' in result + + result_data = parse_results(result) + + for test, ref in zip(ret_test, ret_ref): + assert isinstance(test, value) + address = variables[test.net][0] + data = result_data[address] + if test.dtype == 'int': + val = int.from_bytes(data, sdb.byteorder, signed=True) + elif test.dtype == 'bool': + val = bool.from_bytes(data, sdb.byteorder) + elif test.dtype == 'float': + en = {'little': '<', 'big': '>'}[sdb.byteorder] + val = struct.unpack(en + 'f', data)[0] + assert isinstance(val, float) + else: + raise Exception(f"Unknown type: {test.dtype}") + print('+', val, ref, test.dtype, f" addr={address}") + for t in (int, float, bool): + assert isinstance(val, t) == isinstance(ref, t), f"Result type does not match for {val} and {ref}" + assert val == pytest.approx(ref, 1e-5), f"Result does not match: {val} and reference: {ref}" # pyright: ignore[reportUnknownMemberType] + + +if __name__ == "__main__": + #test_example() + test_slow_31bit_int_list_hash() diff --git a/tools/build.sh b/tools/build.sh index b7e3cd6..fca6f7e 100644 --- a/tools/build.sh +++ b/tools/build.sh @@ -4,10 +4,10 @@ set -eux ARCH=${1:-x86_64} case "$ARCH" in - (x86_64|arm-v6|arm-v7|all) + (x86_64|arm-v6|arm-v7|arm-v7-thumb|all) ;; (*) - echo "Usage: $0 [x86_64|arm-v6|arm-v7|all]" + echo "Usage: $0 [x86_64|arm-v6|arm-v7|arm-v6-thumb|all]" exit 1 ;; esac @@ -107,3 +107,36 @@ if [[ "$ARCH" == "arm-v7" || "$ARCH" == "all" ]]; then src/coparun/mem_man.c \ -o build/runner/coparun-armv7 fi + +####################################### +# ARM v7 thumb +####################################### +if [[ "$ARCH" == "arm-v7-thumb" || "$ARCH" == "all" ]]; then + echo "--------------arm-v7-thumb 32 bit----------------" + + LIBGCC=$(arm-none-eabi-gcc -print-libgcc-file-name) + + arm-none-eabi-gcc -fno-pic -ffunction-sections \ + -march=armv7e-m -mfpu=fpv4-sp-d16 -mfloat-abi=hard -mthumb \ + -c $SRC -O3 -o build/stencils/stencils.o + + arm-none-eabi-ld -r \ + build/stencils/stencils.o \ + build/musl/musl_objects_armv7thumb.o \ + $LIBGCC \ + -o $DEST/stencils_armv7thumb_O3.o + + arm-none-eabi-objdump -d -x \ + $DEST/stencils_armv7thumb_O3.o \ + > build/stencils/stencils_armv7thumb_O3.asm + + arm-linux-gnueabihf-gcc \ + -march=armv7-a -mfpu=neon-vfpv3 -mfloat-abi=hard -mthumb -static \ + -Wall -Wextra -Wconversion -Wsign-conversion \ + -Wshadow -Wstrict-overflow -O3 \ + -DENABLE_LOGGING \ + src/coparun/runmem.c \ + src/coparun/coparun.c \ + src/coparun/mem_man.c \ + -o build/runner/coparun-armv7thumb +fi \ No newline at end of file diff --git a/tools/inspect.sh b/tools/inspect.sh index c25e898..42fba43 100644 --- a/tools/inspect.sh +++ b/tools/inspect.sh @@ -14,3 +14,5 @@ objdump -D -b binary -m i386:x86-64 --adjust-vma=0x1000 build/runner/test.copapy build/runner/coparun-armv7 build/runner/test-armv7.copapy build/runner/test.copapy-armv7.bin arm-none-eabi-objdump -D -b binary -marm --adjust-vma=0x50000 build/runner/test.copapy-armv7.bin > build/runner/test.copapy-armv7.asm + +# arm-none-eabi-objdump -D -b binary -marm -M force-thumb --adjust-vma=0x50001 build/runner/test.copapy-armv7thumb.bin > build/runner/test.copapy-armv7thumb.asm