├── .gitignore ├── firmware ├── .gitignore ├── main.c └── Makefile ├── .gitmodules ├── openocd.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /firmware/.gitignore: -------------------------------------------------------------------------------- 1 | firmware.* 2 | *.o 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "firmware/libopencm3"] 2 | path = firmware/libopencm3 3 | url = https://github.com/libopencm3/libopencm3 4 | branch = 78c23ba5 5 | -------------------------------------------------------------------------------- /firmware/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 Marc Schink 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | #include 19 | 20 | #include 21 | #include 22 | 23 | #define LED2_GPIO GPIOA 24 | #define LED2_PIN GPIO5 25 | 26 | char *text = "This is some secret data stored in the flash memory together with the firmware. Exception(al) failure...!"; 27 | 28 | int main(void) 29 | { 30 | rcc_periph_clock_enable(RCC_GPIOA); 31 | 32 | gpio_set_mode(LED2_GPIO, GPIO_MODE_OUTPUT_2_MHZ, 33 | GPIO_CNF_OUTPUT_PUSHPULL, LED2_PIN); 34 | gpio_clear(LED2_GPIO, LED2_PIN); 35 | 36 | while (true) { 37 | for (uint32_t i = 0; i < (1 << 18); i++) { 38 | __asm__("nop"); 39 | } 40 | 41 | gpio_toggle(LED2_GPIO, LED2_PIN); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /firmware/Makefile: -------------------------------------------------------------------------------- 1 | ## 2 | ## Copyright (C) 2020 Marc Schink 3 | ## 4 | ## This program is free software: you can redistribute it and/or modify 5 | ## it under the terms of the GNU General Public License as published by 6 | ## the Free Software Foundation, either version 3 of the License, or 7 | ## (at your option) any later version. 8 | ## 9 | ## This program is distributed in the hope that it will be useful, 10 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | ## GNU General Public License for more details. 13 | ## 14 | ## You should have received a copy of the GNU General Public License 15 | ## along with this program. If not, see . 16 | ## 17 | 18 | PREFIX ?= arm-none-eabi- 19 | CC = $(PREFIX)gcc 20 | OBJCOPY = $(PREFIX)objcopy 21 | 22 | OPENOCD ?= openocd 23 | 24 | FP_FLAGS = -msoft-float 25 | ARCH_FLAGS = -mcpu=cortex-m3 -mthumb $(FP_FLAGS) 26 | 27 | LD_SCRIPT = libopencm3/lib/stm32/f1/stm32f100xb.ld 28 | 29 | LDFLAGS = -L./libopencm3/lib 30 | LDFLAGS += --static -nostartfiles 31 | LDFLAGS += -T$(LD_SCRIPT) 32 | 33 | CFLAGS = $(ARCH_FLAGS) 34 | CFLAGS += -std=c11 -g3 -O0 35 | CFLAGS += -I./libopencm3/include 36 | CFLAGS += -fno-common -ffunction-sections -fdata-sections 37 | CFLAGS += -DSTM32F1 38 | 39 | LDLIBS = -Wl,--start-group -lc -lgcc -lnosys -Wl,--end-group 40 | LDLIBS += -lopencm3_stm32f1 41 | 42 | FIRMWARE_ELF = firmware.elf 43 | FIRMWARE_BIN = firmware.bin 44 | FIRMWARE_HEX = firmware.hex 45 | 46 | all: $(FIRMWARE_ELF) $(FIRMWARE_BIN) $(FIRMWARE_HEX) 47 | 48 | $(FIRMWARE_ELF): main.c libopencm3/lib/libopencm3_stm32f1.a 49 | $(CC) $(CFLAGS) $(LDFLAGS) main.c $(LDLIBS) -o firmware.elf 50 | 51 | libopencm3/Makefile: 52 | git submodule update --init 53 | 54 | libopencm3/lib/libopencm3_%.a: libopencm3/Makefile 55 | $(MAKE) -C libopencm3 TARGETS=stm32/f1 56 | 57 | $(FIRMWARE_BIN): $(FIRMWARE_ELF) 58 | $(OBJCOPY) -O binary $< $@ 59 | 60 | $(FIRMWARE_HEX): $(FIRMWARE_ELF) 61 | $(OBJCOPY) -O ihex $< $@ 62 | 63 | flash: $(FIRMWARE_ELF) 64 | $(OPENOCD) -f interface/jlink.cfg -c "transport select swd" -f target/stm32f1x.cfg -c "program $(FIRMWARE_ELF) verify reset exit" 65 | 66 | clean: 67 | $(RM) $(FIRMWARE_ELF) $(FIRMWARE_BIN) $(FIRMWARE_HEX) 68 | 69 | distclean: clean 70 | $(MAKE) -C libopencm3 clean 71 | 72 | .PHONY: all flash clean distclean 73 | -------------------------------------------------------------------------------- /openocd.py: -------------------------------------------------------------------------------- 1 | ## 2 | ## OpenOCD remote procedure call (RPC) library. 3 | ## 4 | ## Copyright (C) 2014 Andreas Ortmann 5 | ## Copyright (C) 2019 Marc Schink 6 | ## 7 | ## This program is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## This program is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with this program. If not, see . 19 | ## 20 | 21 | import socket 22 | 23 | class OpenOcd: 24 | COMMAND_TOKEN = '\x1a' 25 | 26 | def __init__(self, host='localhost', port=6666, verbose=False): 27 | self._verbose = verbose 28 | self._tcl_rpc_host = host 29 | self._tcl_rpc_port = port 30 | self._buffer_size = 4096 31 | 32 | self._tcl_variable = 'python_tcl' 33 | 34 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 35 | 36 | def __enter__(self): 37 | self.connect() 38 | return self 39 | 40 | def __exit__(self, type, value, traceback): 41 | try: 42 | self.exit() 43 | finally: 44 | self.close() 45 | 46 | def connect(self): 47 | self._socket.connect((self._tcl_rpc_host, self._tcl_rpc_port)) 48 | 49 | def close(self): 50 | self._socket.close() 51 | 52 | def send(self, cmd): 53 | data = (cmd + OpenOcd.COMMAND_TOKEN).encode('utf-8') 54 | if self._verbose: 55 | print("<-", data) 56 | self._socket.send(data) 57 | 58 | return self._recv() 59 | 60 | def _recv(self): 61 | data = bytes() 62 | 63 | while True: 64 | tmp = self._socket.recv(self._buffer_size) 65 | data += tmp 66 | 67 | if bytes(OpenOcd.COMMAND_TOKEN, encoding='utf-8') in tmp: 68 | break 69 | if self._verbose: 70 | print("->", data) 71 | data = data.decode('utf-8').strip() 72 | 73 | # Strip trailing command token. 74 | data = data[:-1] 75 | 76 | return data 77 | 78 | def exit(self): 79 | self.send('exit') 80 | 81 | def step(self): 82 | self.send('step') 83 | 84 | def resume(self, address=None): 85 | if address is None: 86 | self.send('resume') 87 | else: 88 | self.send('resume 0x%x' % address) 89 | 90 | def halt(self): 91 | self.send('halt') 92 | 93 | def wait_halt(self, timeout=5000): 94 | self.send('wait_halt %u' % timeout) 95 | 96 | def write_memory(self, address, data, word_length=32): 97 | array = ' '.join(['%d 0x%x' % (i, v) for (i, v) in enumerate(data)]) 98 | 99 | # Clear the array before using it. 100 | self.send('array unset %s' % self._tcl_variable) 101 | 102 | self.send('array set %s { %s }' % (self._tcl_variable, array)) 103 | self.send('array2mem %s 0x%x %s %d' % (self._tcl_variable, 104 | word_length, address, len(data))) 105 | 106 | def read_memory(self, address, count, word_length=32): 107 | # Clear the array before using it. 108 | self.send('array unset %s' % self._tcl_variable) 109 | 110 | self.send('mem2array %s %d 0x%x %d' % (self._tcl_variable, 111 | word_length, address, count)) 112 | 113 | raw = self.send('return $%s' % self._tcl_variable).split(' ') 114 | 115 | order = [int(raw[2 * i]) for i in range(len(raw) // 2)] 116 | values = [int(raw[2 * i + 1]) for i in range(len(raw) // 2)] 117 | 118 | # Sort the array because it may not be sorted by the memory address. 119 | result = [0] * len(values) 120 | for (i, pos) in enumerate(order): 121 | result[pos] = values[i] 122 | 123 | return result 124 | 125 | def read_register(self, register): 126 | if issubclass(type(register), int): 127 | raw = self.send('capture "reg %u"' % register).split(': ') 128 | else: 129 | raw = self.send('capture "reg %s"' % register).split(': ') 130 | 131 | if len(raw) < 2: 132 | return None 133 | 134 | return int(raw[1], 16) 135 | 136 | def read_registers(self, registers): 137 | result = dict() 138 | 139 | for register in registers: 140 | value = self.read_register(register) 141 | 142 | if value is None: 143 | return None 144 | 145 | result[register] = value 146 | 147 | return result 148 | 149 | def read_register_list(self, registers): 150 | register_list = [None] * len(registers) 151 | result = self.read_registers(registers) 152 | 153 | # Preserve register order. 154 | for reg in result: 155 | register_list[registers.index(reg)] = result[reg] 156 | 157 | return register_list 158 | 159 | def write_register(self, register, value): 160 | if issubclass(type(register), int): 161 | self.send('reg %u 0x%x' % (register, value)) 162 | else: 163 | self.send('reg %s 0x%x' % (register, value)) 164 | 165 | def write_registers(self, registers): 166 | for reg in registers: 167 | self.write_register(reg, registers[reg]) 168 | 169 | def set_breakpoint(self, address, length=2, hardware=True): 170 | if hardware: 171 | self.send('bp 0x%x %u hw' % (address, length)) 172 | else: 173 | self.send('bp 0x%x %u' % (address, length)) 174 | 175 | def remove_breakpoint(self, address): 176 | self.send('rbp 0x%x' % address) 177 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## 4 | ## Copyright (C) 2019 Marc Schink 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | import sys 21 | import math 22 | import argparse 23 | import struct 24 | import enum 25 | 26 | from openocd import OpenOcd 27 | 28 | class Register(enum.IntEnum): 29 | R0 = 0 30 | R1 = 1 31 | R2 = 2 32 | R3 = 3 33 | R4 = 4 34 | R5 = 5 35 | R6 = 6 36 | R7 = 7 37 | R8 = 8 38 | R9 = 9 39 | R10 = 10 40 | R11 = 11 41 | R12 = 12 42 | SP = 13 43 | LR = 14 44 | PC = 15 45 | PSR = 16 46 | 47 | WORD_SIZE = 4 48 | 49 | # Initial stack pointer (SP) value. 50 | INITIAL_SP = 0x20000200 51 | 52 | # Vector Table Offset Register (VTOR). 53 | VTOR_ADDR = 0xe000ed08 54 | # Interrupt Control and State Register (ICSR). 55 | ICSR_ADDR = 0xe000ed04 56 | # System Handler Control and State Register (SHCSR). 57 | SHCSR_ADDR = 0xe000ed24 58 | # NVIC Interrupt Set-Enable Registers (ISER). 59 | NVIC_ISER0_ADDR = 0xe000e100 60 | # NVIC Interrupt Set-Pending Registers (ISPR). 61 | NVIC_ISPR0_ADDR = 0xe000e200 62 | # Debug Exception and Monitor Control Register (DEMCR). 63 | DEMCR_ADDR = 0xe000edfc 64 | # Memory region with eXecute Never (XN) property. 65 | MEM_XN_ADDR = 0xe0000000 66 | 67 | SVC_INST_ADDR = 0x20000000 68 | NOP_INST_ADDR = 0x20000002 69 | LDR_INST_ADDR = 0x20000004 70 | UNDEF_INST_ADDR = 0x20000006 71 | 72 | # Inaccessible exception numbers. 73 | INACCESSIBLE_EXC_NUMBERS = [0, 1, 7, 8, 9, 10, 13] 74 | 75 | def generate_exception(openocd, vt_address, exception_number): 76 | openocd.send('reset halt') 77 | 78 | # Relocate vector table. 79 | openocd.write_memory(VTOR_ADDR, [vt_address]) 80 | 81 | registers = dict() 82 | 83 | if exception_number == 2: 84 | # Generate a non-maskable interrupt. 85 | openocd.write_memory(ICSR_ADDR, [1 << 31]) 86 | registers[Register.PC] = NOP_INST_ADDR 87 | elif exception_number == 3: 88 | # Generate a HardFault exception due to priority escalation. 89 | registers[Register.PC] = UNDEF_INST_ADDR 90 | elif exception_number == 4: 91 | # Generate a MemFault exception by executing memory with 92 | # eXecute-Never (XN) property. 93 | registers[Register.PC] = MEM_XN_ADDR 94 | # Enable MemFault exceptions. 95 | openocd.write_memory(SHCSR_ADDR, [0x10000]) 96 | elif exception_number == 5: 97 | # Generate a BusFault exception by executing a load instruction that 98 | # accesses invalid memory. 99 | registers[Register.PC] = LDR_INST_ADDR 100 | # Enable BusFault exceptions. 101 | openocd.write_memory(SHCSR_ADDR, [0x20000]) 102 | registers[Register.R6] = 0xffffff00 103 | elif exception_number == 6: 104 | # Generate an UsageFault by executing an undefined instruction. 105 | registers[Register.PC] = UNDEF_INST_ADDR 106 | # Enable UsageFault exceptions. 107 | openocd.write_memory(SHCSR_ADDR, [0x40000]) 108 | elif exception_number == 11: 109 | # Generate a Supervisor Call (SVCall) exception. 110 | registers[Register.PC] = SVC_INST_ADDR 111 | elif exception_number == 12: 112 | # Generate a DebugMonitor exception. 113 | registers[Register.PC] = NOP_INST_ADDR 114 | openocd.write_memory(DEMCR_ADDR, [1 << 17]) 115 | elif exception_number == 14: 116 | # Generate a PendSV interrupt. 117 | openocd.write_memory(ICSR_ADDR, [1 << 28]) 118 | registers[Register.PC] = NOP_INST_ADDR 119 | elif exception_number == 15: 120 | # Generate a SysTick interrupt. 121 | openocd.write_memory(ICSR_ADDR, [1 << 26]) 122 | registers[Register.PC] = NOP_INST_ADDR 123 | elif exception_number >= 16: 124 | # Generate an external interrupt. 125 | ext_interrupt_number = exception_number - 16 126 | 127 | register_offset = (ext_interrupt_number // 32) * WORD_SIZE 128 | value = (1 << (ext_interrupt_number % 32)) 129 | 130 | # Enable and make interrupt pending. 131 | openocd.write_memory(NVIC_ISER0_ADDR + register_offset, [value]) 132 | openocd.write_memory(NVIC_ISPR0_ADDR + register_offset, [value]) 133 | 134 | registers[Register.PC] = NOP_INST_ADDR 135 | else: 136 | sys.exit('Exception number %u not handled' % exception_number) 137 | 138 | # Ensure that the processor operates in Thumb mode. 139 | registers[Register.PSR] = 0x01000000 140 | registers[Register.SP] = INITIAL_SP 141 | 142 | for reg in registers: 143 | openocd.write_register(reg, registers[reg]) 144 | 145 | # Perform a single step to generate the exception. 146 | openocd.send('step') 147 | 148 | def recover_pc(openocd): 149 | (pc, xpsr) = openocd.read_register_list([Register.PC, Register.PSR]) 150 | 151 | # Recover LSB of the PC from the EPSR.T bit. 152 | t_bit = (xpsr >> 24) & 0x1 153 | 154 | return pc | t_bit 155 | 156 | def align(address, base): 157 | return address - (address % base) 158 | 159 | def determine_num_ext_interrupts(openocd): 160 | count = 0 161 | 162 | # The ARMv7-M architecture supports up to 496 external interrupts. 163 | for i in range(0, 496): 164 | openocd.send('reset init') 165 | 166 | register_offset = (i // 32) * WORD_SIZE 167 | value = (1 << (i % 32)) 168 | 169 | # Enable and make interrupt pending. 170 | openocd.write_memory(NVIC_ISER0_ADDR + register_offset, [value]) 171 | openocd.write_memory(NVIC_ISPR0_ADDR + register_offset, [value]) 172 | 173 | openocd.write_register(Register.PC, NOP_INST_ADDR) 174 | # Ensure that the processor operates in Thumb mode. 175 | openocd.write_register(Register.PSR, 0x01000000) 176 | openocd.write_register(Register.SP, INITIAL_SP) 177 | 178 | openocd.step() 179 | xpsr = openocd.read_register(Register.PSR) 180 | exception_number = xpsr & 0x1ff 181 | 182 | if exception_number != (i + 16): 183 | break 184 | 185 | count += 1 186 | 187 | return count 188 | 189 | def calculate_vtor_exc(address, num_exceptions): 190 | # The vector table size must be a power of two. 191 | table_size = 2 ** int(math.log(num_exceptions, 2)) 192 | vtor_address = align(address, table_size * WORD_SIZE) 193 | 194 | exception_number = (address - vtor_address) // WORD_SIZE 195 | 196 | if exception_number not in INACCESSIBLE_EXC_NUMBERS: 197 | return (vtor_address, exception_number) 198 | 199 | # Use the wrap-around behaviour to generate an exception for an 200 | # inaccessible vector table entry. 201 | # This is only possible when the vector table is not aligned to its size 202 | # and the device has enough exceptions. 203 | if (vtor_address % (table_size * 2 * WORD_SIZE)) != 0 \ 204 | and (exception_number + table_size) < num_exceptions: 205 | exception_number += table_size 206 | 207 | return (vtor_address, exception_number) 208 | 209 | if __name__ == '__main__': 210 | parser = argparse.ArgumentParser() 211 | parser.add_argument('address', help='Extraction start address') 212 | parser.add_argument('length', help='Number of words to extract') 213 | parser.add_argument('--value', default='0xffffffff', 214 | help=('Value to be used for non-extractable memory words. ' 215 | 'Use "skip" to ignore them')) 216 | parser.add_argument('--binary', action='store_true', 217 | help='Output binary') 218 | parser.add_argument('--host', default='localhost', 219 | help='OpenOCD Tcl interface host') 220 | parser.add_argument('--port', type=int, default=6666, 221 | help='OpenOCD Tcl interface port') 222 | args = parser.parse_args() 223 | 224 | start_address = int(args.address, 0) 225 | length = int(args.length, 0) 226 | skip_value = args.value 227 | binary_output = args.binary 228 | 229 | if skip_value != 'skip': 230 | skip_value = int(skip_value, 0) 231 | 232 | oocd = OpenOcd(args.host, args.port) 233 | 234 | try: 235 | oocd.connect() 236 | except Exception as e: 237 | sys.exit('Failed to connect to OpenOCD') 238 | 239 | # Disable exception masking by OpenOCD. The target must be halted before 240 | # the masking behaviour can be changed. 241 | oocd.halt() 242 | oocd.send('cortex_m maskisr off') 243 | 244 | # Write 'svc #0', 'nop', 'ldr r0, [r1, #0]' and an undefined instruction 245 | # to the SRAM. We use them later to generate exceptions. 246 | oocd.write_memory(SVC_INST_ADDR, [0xdf00], word_length=16) 247 | oocd.write_memory(NOP_INST_ADDR, [0xbf00], word_length=16) 248 | oocd.write_memory(LDR_INST_ADDR, [0x7b75], word_length=16) 249 | oocd.write_memory(UNDEF_INST_ADDR, [0xffff], word_length=16) 250 | 251 | num_exceptions = 16 + determine_num_ext_interrupts(oocd) 252 | end_address = start_address + (length * WORD_SIZE) 253 | 254 | for address in range(start_address, end_address, WORD_SIZE): 255 | (vtor_address, exception_number) = calculate_vtor_exc( 256 | address, num_exceptions) 257 | 258 | if address == 0x00000000: 259 | oocd.send('reset halt') 260 | recovered_value = oocd.read_register(Register.SP) 261 | elif address == 0x00000004: 262 | oocd.send('reset halt') 263 | recovered_value = recover_pc(oocd) 264 | elif exception_number in INACCESSIBLE_EXC_NUMBERS: 265 | recovered_value = None 266 | else: 267 | generate_exception(oocd, vtor_address, exception_number) 268 | recovered_value = recover_pc(oocd) 269 | 270 | if recovered_value is None and skip_value == 'skip': 271 | continue 272 | 273 | if recovered_value is None: 274 | recovered_value = skip_value 275 | 276 | if binary_output: 277 | output_value = struct.pack('