Skip to content

Commit 02c5a34

Browse files
committed
feat: add support for ADR-based vector table jump
lint: minor lints and fixes
1 parent efb6f41 commit 02c5a34

4 files changed

Lines changed: 229 additions & 40 deletions

File tree

‎app/elf_arm_thumbv6m/functions/s04_instructions_effect/_program_counter_effect.py‎

Lines changed: 222 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
Instruction,
1010
InstructionAddRegisterT2,
1111
InstructionAddSpPlusRegisterT1,
12+
InstructionAdrT1,
1213
InstructionBlT1,
1314
InstructionBlxRegisterT1,
1415
InstructionBT1,
1516
InstructionBT2,
1617
InstructionBxT1,
1718
InstructionLdrLiteralT1,
19+
InstructionLdrRegisterT1,
1820
InstructionLslImmediateT1,
1921
InstructionMovRegisterT1,
2022
InstructionPopT1,
@@ -85,21 +87,21 @@ def resolve(
8587

8688
match instruction:
8789
case InstructionAddRegisterT2():
88-
if instruction.dn is Register4.PC:
89-
# PC = PC + register
90-
effect_branch = resolve_add_register_t2_pc(cursor_function_region_instructions)
90+
if instruction.dn is not Register4.PC:
91+
return None
9192

92-
return effect_branch
93+
# PC = PC + register
94+
effect_branch = resolve_add_register_t2_pc(cursor_function_region_instructions)
9395

94-
return None
96+
return effect_branch
9597
case InstructionAddSpPlusRegisterT1():
96-
if instruction.dm is Register4.PC:
97-
# PC = SP + PC ???
98-
99-
# not seen in the wild
100-
raise ResolveUnsupportedInstructionException(instruction, address)
101-
else:
98+
if instruction.dm is not Register4.PC:
10299
return None
100+
101+
# PC = SP + PC ???
102+
# not seen in the wild
103+
raise ResolveUnsupportedInstructionException(instruction, address)
104+
103105
case InstructionBT1():
104106
# current position + branch-pc offset (4) + instruction immediate
105107
target_function_offset = cursor_function_region_instructions.function_offset() + 4 + instruction.imm * 2
@@ -139,23 +141,26 @@ def resolve(
139141
# other patterns not seen in the wild
140142
raise ResolveUnsupportedInstructionException(instruction, address)
141143
case InstructionMovRegisterT1():
142-
if instruction.d is Register4.PC:
143-
# PC = register
144+
if instruction.d is not Register4.PC:
145+
return None
144146

145-
# special case - MOV PC, LR is technically a return
146-
if instruction.m is Register4.LR:
147-
return EffectReturn()
147+
# PC = register
148148

149-
# other patterns not seen in the wild
150-
raise ResolveUnsupportedInstructionException(instruction, address)
151-
else:
152-
return None
153-
case InstructionPopT1():
154-
if instruction.pc:
155-
# POP PC
149+
# special case - MOV PC, LR is technically a return
150+
if instruction.m is Register4.LR:
156151
return EffectReturn()
157152

158-
return None
153+
# PC = register (non-LR)
154+
# requires non-trivial handling (eg. jump table via ADR + LDR register)
155+
effect_branch = resolve_mov_register_t1_pc(cursor_function_region_instructions)
156+
157+
return effect_branch
158+
case InstructionPopT1():
159+
if not instruction.pc:
160+
return None
161+
162+
# POP PC
163+
return EffectReturn()
159164
case InstructionUdfT1():
160165
return EffectInvalid()
161166
case InstructionUdfT2():
@@ -208,8 +213,8 @@ def resolve_add_register_t2_pc(cursor_function_region_instructions: CursorFuncti
208213

209214
effects_branch = {
210215
effect_branch
211-
for effect_branch in (resolver(cursor_function_region_instructions) for resolver in RESOLVERS)
212-
if effect_branch is not None
216+
for resolver in RESOLVERS
217+
if (effect_branch := resolver(cursor_function_region_instructions)) is not None
213218
}
214219
match list(effects_branch):
215220
case []:
@@ -365,12 +370,10 @@ def resolve_add_register_t2_pc_offset_table(
365370
# data region must not be empty and we started from the beginning
366371
assert target_instruction_offsets
367372

368-
instruction_function_offset = cursor_function_region_instructions.function_offset()
369-
370373
# convert to function-relative offsets
371374
# use current instruction offset + 4 (pc on next instruction) + offset from current instruction
372375
target_function_offsets = {
373-
instruction_function_offset + 4 + target_instruction_offset
376+
cursor_function_region_instructions.function_offset() + 4 + target_instruction_offset
374377
for target_instruction_offset in target_instruction_offsets
375378
}
376379

@@ -404,8 +407,8 @@ def resolve_blx_register_t1(
404407
# they may return the same effect (will be flattened by set comprehensions)
405408
effects_call = {
406409
effect_call
407-
for effect_call in (resolver(cursor_function_region_instructions) for resolver in RESOLVERS)
408-
if effect_call is not None
410+
for resolver in RESOLVERS
411+
if (effect_call := resolver(cursor_function_region_instructions)) is not None
409412
}
410413
match list(effects_call):
411414
case []:
@@ -494,14 +497,14 @@ def resolve_blx_register_t1_pc_relative_load(
494497
)
495498

496499
# resolve data region containing target address
497-
cursor_function_region_data_target = (
498-
cursor_function_region_instructions_modifying.cursor_function.region_data(data_function_offset)
500+
cursor_function_region_data = cursor_function_region_instructions_modifying.cursor_function.region_data(
501+
data_function_offset
499502
)
500-
if cursor_function_region_data_target is None:
503+
if cursor_function_region_data is None:
501504
return None
502505

503506
# get the address
504-
target_address, _ = cursor_function_region_data_target.read_word_unsigned()
507+
target_address, _ = cursor_function_region_data.read_word_unsigned()
505508

506509
# lsb bit must always be set to signify thumb execution
507510
if target_address & 1 != 1:
@@ -519,3 +522,187 @@ def resolve_blx_register_t1_pc_relative_load(
519522
case _:
520523
# unsupported instruction
521524
return None
525+
526+
527+
def resolve_mov_register_t1_pc(
528+
cursor_function_region_instructions: CursorFunctionRegionInstructions,
529+
) -> EffectBranch:
530+
RESOLVERS = [
531+
resolve_mov_register_t1_pc_adr_table,
532+
]
533+
534+
effects_branch = {
535+
effect_branch
536+
for resolver in RESOLVERS
537+
if (effect_branch := resolver(cursor_function_region_instructions)) is not None
538+
}
539+
match list(effects_branch):
540+
case []:
541+
# no resolver resolved anything
542+
address = cursor_function_region_instructions.address()
543+
544+
raise ResolveMovRegisterT1PcUnknownException(address)
545+
case [effect_branch]:
546+
# one or many resolver resolved our target effect
547+
548+
return effect_branch
549+
case _:
550+
# multiple resolvers resolved multiple effects
551+
assert False
552+
553+
554+
def resolve_mov_register_t1_pc_adr_table(
555+
cursor_function_region_instructions: CursorFunctionRegionInstructions,
556+
) -> EffectBranch | None:
557+
# register contains jump table index [0 - ?]
558+
# lsls register_index, register_source, #0x2 # multiplies jump table index by 4 (word size)
559+
# adr register_base, #imm # loads word-aligned address pointing to the data table
560+
# ldr register_index, [register_base, register_index] # loads absolute address from jump table
561+
# mov pc, register_index # branches to the loaded address
562+
# data region containing absolute addresses (with thumb bit set)
563+
#
564+
# solve this by examining the pattern backwards from mov pc and extracting absolute addresses from data region
565+
566+
# parse the mov pc, register - this should always be true as it was what caused entry to this block
567+
instruction = cursor_function_region_instructions.instruction()
568+
match instruction:
569+
case InstructionMovRegisterT1():
570+
assert instruction.d is Register4.PC
571+
572+
# extract the register, cast it to Register3 (all other instructions operate on 3-bit only)
573+
register_index = instruction.m.to_register3()
574+
if register_index is None:
575+
# is not R0-R7?
576+
return None
577+
case _:
578+
# so who and why called us?
579+
assert False
580+
581+
# parse ldr register_index, [register_base, register_index]
582+
cursor_function_region_instructions_ldr = cursor_function_region_instructions.previous()
583+
if cursor_function_region_instructions_ldr is None:
584+
# function ended prematurely?
585+
return None
586+
instruction_ldr = cursor_function_region_instructions_ldr.instruction()
587+
match instruction_ldr:
588+
case InstructionLdrRegisterT1():
589+
# we've got LDR register, [register, register]
590+
591+
# target register must be our index register
592+
if instruction_ldr.t is not register_index:
593+
return None
594+
# offset register must be our index register
595+
if instruction_ldr.m is not register_index:
596+
return None
597+
598+
# extract the base register
599+
register_base = instruction_ldr.n
600+
case _:
601+
# other instruction
602+
return None
603+
604+
# parse adr register_base, #imm
605+
cursor_function_region_instructions_adr = cursor_function_region_instructions_ldr.previous()
606+
if cursor_function_region_instructions_adr is None:
607+
# function ended prematurely?
608+
return None
609+
instruction_adr = cursor_function_region_instructions_adr.instruction()
610+
match instruction_adr:
611+
case InstructionAdrT1():
612+
# we've got ADR
613+
614+
# destination register must be our base register
615+
if instruction_adr.d is not register_base:
616+
return None
617+
case _:
618+
# other instruction
619+
return None
620+
621+
# parse lsls register_index, register_source, #0x2
622+
cursor_function_region_instructions_lsls = cursor_function_region_instructions_adr.previous()
623+
if cursor_function_region_instructions_lsls is None:
624+
# function ended prematurely?
625+
return None
626+
instruction_lsls = cursor_function_region_instructions_lsls.instruction()
627+
match instruction_lsls:
628+
case InstructionLslImmediateT1():
629+
# we've got LSLS
630+
631+
# destination must be our index register
632+
if instruction_lsls.d is not register_index:
633+
return None
634+
# shift must be 2 (multiply by 4 for word-sized entries)
635+
if instruction_lsls.imm != 2:
636+
return None
637+
case _:
638+
# other instruction
639+
return None
640+
641+
# instruction pattern matches, now resolve the data table address from ADR
642+
data_function_offset = (
643+
(cursor_function_region_instructions_adr.function_offset() + 4) & ~0b11
644+
) + instruction_adr.imm * 4 # Align(PC, 4)
645+
646+
# resolve data region containing the jump table
647+
cursor_function_region_data = cursor_function_region_instructions.cursor_function.region_data(data_function_offset)
648+
if cursor_function_region_data is None:
649+
# region covering this address does not exist?
650+
return None
651+
if cursor_function_region_data.function_region_data_offset != 0:
652+
# is not at the beginning of the data region?
653+
return None
654+
655+
# extract all branch targets (absolute addresses)
656+
target_addresses = set[Address]()
657+
cursor_function_region_data_item: CursorFunctionRegionData | None = cursor_function_region_data
658+
while True:
659+
# we reached end of data region
660+
if cursor_function_region_data_item is None:
661+
break
662+
663+
# load absolute address and advance cursor
664+
target_address, cursor_function_region_data_item = cursor_function_region_data_item.read_word_unsigned()
665+
666+
# skip zero entries (padding / sentinel)
667+
if target_address == 0:
668+
continue
669+
670+
# thumb bit (LSB) must be set for valid branch targets
671+
if target_address & 1 != 1:
672+
raise ValueError(
673+
f"Thumb bit not set for jump table entry at {cursor_function_region_instructions}? "
674+
"This would cause a HardFault..."
675+
)
676+
677+
# clear it to align to the address
678+
target_address &= ~1
679+
680+
target_addresses.add(target_address)
681+
682+
# data region must not be empty
683+
assert target_addresses
684+
685+
# convert absolute target addresses to function-relative-addresses
686+
target_function_offsets = {
687+
target_address
688+
- cursor_function_region_instructions.address()
689+
+ cursor_function_region_instructions.function_offset()
690+
for target_address in target_addresses
691+
}
692+
693+
return EffectBranch(
694+
conditional=False,
695+
target_function_offsets=frozenset(target_function_offsets),
696+
)
697+
698+
699+
class ResolveMovRegisterT1PcUnknownException(ResolveException):
700+
def __init__(self, address: Address) -> None:
701+
super().__init__(address, self._format_message(address))
702+
703+
@classmethod
704+
def _format_message(cls, address: Address) -> str:
705+
return (
706+
f"Unable to automatically resolve target of MOV PC, Register instruction at 0x{address:04X}.\n"
707+
"Please open an issue to help us improve this tool, providing as many details as possible."
708+
)

‎app/elf_arm_thumbv6m/functions/s04_instructions_effect/_stack_pointer_effect.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Instruction,
66
InstructionAddSpPlusImmediateT2,
77
InstructionAddSpPlusRegisterT1,
8+
InstructionAddSpPlusRegisterT2,
89
InstructionMovRegisterT1,
910
InstructionMsrRegisterT1,
1011
InstructionPopT1,
@@ -47,6 +48,9 @@ def resolve(cursor_function_region_instructions: CursorFunctionRegionInstruction
4748
raise ResolveUnsupportedInstructionException(instruction, address)
4849

4950
return None
51+
case InstructionAddSpPlusRegisterT2():
52+
# SP = SP + m
53+
raise ResolveUnsupportedInstructionException(instruction, address)
5054
case InstructionMovRegisterT1():
5155
if instruction.d is Register4.SP:
5256
# SP = m

‎app/elf_arm_thumbv6m_cortex_m0/entrypoints/parse.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,8 @@ def resolve_default_handler_function(functions: Functions, config: ConfigDefault
241241
# get all functions matching our predefined names list
242242
default_handler_functions = [
243243
function
244-
for function in (
245-
functions.by_name.get(default_handler_name) for default_handler_name in _DEFAULT_HANDLER_NAMES
246-
)
247-
if function is not None
244+
for default_handler_name in _DEFAULT_HANDLER_NAMES
245+
if (function := functions.by_name.get(default_handler_name)) is not None
248246
]
249247

250248
# we should have exactly one match

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "stack-depth-analyzer"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
authors = [{ name = "Paweł Kubrak", email = "peku33@gmail.com" }]
55
requires-python = "~3.13"
66
dynamic = ["dependencies"]

0 commit comments

Comments
 (0)