diff --git a/config/function_extents.tsv b/config/function_extents.tsv index c612ac3..1f42ef0 100644 --- a/config/function_extents.tsv +++ b/config/function_extents.tsv @@ -2263,9 +2263,9 @@ 0x80103D60 - - - - standalone grades=prologue 0x80103F84 0x80103FA8 36 0x80103FB4 12 exact term=jr_ra 0x80103FB4 0x80103FCC 24 0x80103FCC 0 indirect jr_reg=0x80103FB8;lower_bound=0x80103FC0 -0x80103FCC 0x80103FD4 8 0x80103FDC 8 exact term=syscall +0x80103FCC 0x80103FDC 16 0x80103FDC 0 exact term=jr_ra 0x80103FDC 0x80103FEC 16 0x80103FEC 0 indirect jr_reg=0x80103FE0;lower_bound=0x80103FE8 -0x80103FEC 0x80103FF4 8 0x80103FFC 8 exact term=syscall +0x80103FEC 0x80103FFC 16 0x80103FFC 0 exact term=jr_ra 0x80103FFC 0x8010400C 16 0x8010400C 0 indirect jr_reg=0x80104000;lower_bound=0x80104008 0x8010400C 0x80104038 44 0x80104230 504 exact term=jr_ra 0x80104040 - - - - standalone grades=prologue @@ -2664,10 +2664,10 @@ 0x8016E504 0x8016E60C 264 0x8016E60C 0 fallthrough hit=0x8016E60C 0x8016E60C 0x8016E93C 816 0x8016E93C 0 fallthrough hit=0x8016E93C 0x8016E93C 0x8017D5C0 60548 0x8017D5C0 0 indirect jr_reg=0x8017075C;lower_bound=0x80170764 -0x8017D5C0 0x8017D75C 412 0x801800C4 10600 exact term=syscall -0x801800C4 0x801801B8 244 0x80180808 1616 exact term=syscall +0x8017D5C0 0x801800C4 11012 0x801800C4 0 outside pc=0x8FFC23FC +0x801800C4 0x80180808 1860 0x80180808 0 fallthrough hit=0x80180808 0x80180808 0x8018080C 4 0x8018080C 0 fallthrough hit=0x8018080C -0x8018080C 0x80180984 376 0x801BB450 240332 exact term=syscall +0x8018080C 0x801BB450 240708 0x801BB450 0 indirect jr_reg=0x801818AC;lower_bound=0x801818B4 0x801AF338 - - - - standalone grades=prologue 0x801AF704 - - - - standalone grades=prologue 0x801AF99C - - - - standalone grades=prologue diff --git a/tools/sf3_extents b/tools/sf3_extents index 4ed766e..74070a5 100755 --- a/tools/sf3_extents +++ b/tools/sf3_extents @@ -22,21 +22,28 @@ records the highest reachable instruction. The walk stops at: * a return (`jr ra`) -- the delay slot is included; * a register jump (`jr $rs`, rs != ra), i.e. a switch/jump table, whose targets cannot be followed statically; - * `syscall` / `break`; + * `break`, which traps; * a tail `j` whose target is outside this function (behind its start, or a known start); * another **hard** start, which is a boundary the walk may not cross. +`syscall` is deliberately **not** a terminator: on this target it is the BIOS +call instruction and it returns to the instruction after it. Treating it as a +return truncated the two 16-byte BIOS stubs at `0x80103FCC` and `0x80103FEC` to +8 bytes, which is unreachable from C because `cc1` always appends an epilogue +(found by worker C in Phase 8; the stub's four `jal` callers and the absence of +any reference to the following address are the confirming evidence). + The extent end is the first byte after the last reachable instruction. For a call this is the return address; for a return, the instruction after the delay slot. ## Grades - exact the walk terminated at a return / `syscall` / `break` / tail - jump and stayed inside its own region. This is the grade a match - candidate wants: the end is the function's own last byte + 1. - All 12 regions registered in Phase 6 reproduce at this grade. + exact the walk terminated at a return / `break` / tail jump and stayed + inside its own region. This is the grade a match candidate wants: + the end is the function's own last byte + 1. All 115 regions + registered in Phases 6-8 reproduce at this grade. fallthrough no terminal was reachable; the walk ran into the next hard start. The extent ends there. Typically a function that ends in a call to something that does not return. @@ -332,9 +339,12 @@ def walk(payload: bytes, text_address: int, start: int, hard: frozenset[int], pending.append(pc + 4) pc += 8 continue - if opcode == 0 and funct in (0x0C, 0x0D): # syscall / break - terminals.append(("syscall" if funct == 0x0C else "break", pc, None)) + if opcode == 0 and funct == 0x0D: # break (traps) + terminals.append(("break", pc, None)) break + if opcode == 0 and funct == 0x0C: # syscall (returns) + pc += 4 + continue if opcode == 0x02: # j target = jump_target(pc, word) # The delay slot executes and is part of the body, but control diff --git a/tools/tests/test_sf3_extents.py b/tools/tests/test_sf3_extents.py index 1114d4f..8be9bfa 100644 --- a/tools/tests/test_sf3_extents.py +++ b/tools/tests/test_sf3_extents.py @@ -114,7 +114,8 @@ def _payload() -> bytes: # No terminal: the body runs into the next hard start. put(FALLTHROUGH, _jal(PAYLOAD + PLAIN)) put(PLAIN, _jr()) - put(SYSCALL_FN, 0x0C) + put(SYSCALL_FN, 0x0C) # syscall (returns to the next word) + put(SYSCALL_FN + 4, _jr()) # ... so the body ends with a return # A branch whose target jumps over the next hard start. put(ESCAPE, _beq(6)) put(ESCAPE + 4, _jr()) @@ -253,10 +254,21 @@ class ExtentTests(unittest.TestCase): self.assertEqual(extent.grade, "fallthrough") self.assertEqual(extent.end, PAYLOAD + PLAIN) - def test_syscall_is_a_terminal(self) -> None: + def test_syscall_is_not_a_terminal_because_it_returns(self) -> None: + """A BIOS `syscall` returns to the next instruction, so the body continues.""" extent = self._extent(SYSCALL_FN) self.assertEqual(extent.grade, "exact") - self.assertIn("term=syscall", extent.evidence) + self.assertNotIn("term=syscall", extent.evidence) + # the walk ran past the syscall to the following `jr ra` and its delay slot + self.assertEqual(extent.end, PAYLOAD + SYSCALL_FN + 12) + self.assertIn("term=jr_ra", extent.evidence) + + def test_break_still_terminates(self) -> None: + payload = bytearray(_payload()) + struct.pack_into(" None: extent = self._extent(ESCAPE)