| #!/usr/bin/env python3 |
| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import argparse |
| import sys |
| from typing import List |
| |
| from shared.check import CheckResult |
| from shared.decode import OTBNProgram, decode_elf |
| from shared.section import CodeSection |
| |
| |
| def _get_pcs_for_mnemonics(program: OTBNProgram, |
| mnems: List[str]) -> List[int]: |
| '''Gets all PCs in the program holding the given instruction.''' |
| return [ |
| pc for (pc, (insn, _)) in program.insns.items() |
| if insn.mnemonic in mnems |
| ] |
| |
| |
| def _get_branches(program: OTBNProgram) -> List[int]: |
| '''Gets the PCs of all branch instructions (BEQ and BNE) in the program.''' |
| return _get_pcs_for_mnemonics(program, ['bne', 'beq']) |
| |
| |
| def _get_loop_starts(program: OTBNProgram) -> List[int]: |
| '''Gets the start PCs of all loops (LOOP and LOOPI) in the program.''' |
| return _get_pcs_for_mnemonics(program, ['loop', 'loopi']) |
| |
| |
| def _get_loops(program: OTBNProgram) -> List[CodeSection]: |
| '''Gets the PC ranges of all loops (LOOP and LOOPI) in the program.''' |
| loop_starts = _get_loop_starts(program) |
| loops = [] |
| for pc in loop_starts: |
| operands = program.get_operands(pc) |
| end_pc = pc + operands['bodysize'] * 4 |
| loops.append(CodeSection(pc + 4, end_pc)) |
| return loops |
| |
| |
| def _check_loop_iterations(program: OTBNProgram, |
| loops: List[CodeSection]) -> CheckResult: |
| '''Checks number of iterations for loopi. |
| |
| If the number of iterations is 0, this check fails; `loopi` requires at |
| least one iteration and will raise a LOOP error otherwise. The `loop` |
| instruction also has this requirement, but since the number of loop |
| iterations comes from a register it's harder to check statically and is not |
| considered here. |
| ''' |
| out = CheckResult() |
| for loop in loops: |
| insn = program.get_insn(loop.start) |
| operands = program.get_operands(loop.start) |
| if insn.mnemonic == 'loopi' and operands['iterations'] <= 0: |
| out.err( |
| 'Bad number of loop iterations ({}) at PC {:#x}: {}'.format( |
| operands['iterations'], loop.start, |
| insn.disassemble(loop.start, operands))) |
| return out |
| |
| |
| def _check_loop_end_insns(program: OTBNProgram, |
| loops: List[CodeSection]) -> CheckResult: |
| '''Checks that loops do not end in control flow instructions. |
| |
| Such instructions can cause LOOP software errors during execution. |
| ''' |
| out = CheckResult() |
| for loop in loops: |
| loop_end_insn = program.get_insn(loop.end) |
| if not loop_end_insn.straight_line: |
| out.err('Control flow instruction ({}) at end of loop at PC {:#x} ' |
| '(loop starting at PC {:#x})'.format( |
| loop_end_insn.mnemonic, loop.end, loop.start)) |
| return out |
| |
| |
| def _check_loop_inclusion(program: OTBNProgram, |
| loops: List[CodeSection]) -> CheckResult: |
| '''Checks that inner loops are fully contained within outer loops. |
| |
| When a loop starts within the body of another loop, it must be the case |
| that the inner loop's final instruction occurs before the outer loop's. |
| ''' |
| out = CheckResult() |
| for loop in loops: |
| for other in loops: |
| if other.start in loop and other.end not in loop: |
| out.err('Inner loop ends after outer loop (inner loop {}, ' |
| 'outer loop {})'.format(other, loop.pretty())) |
| |
| return out |
| |
| |
| def _check_loop_branching(program: OTBNProgram, |
| loops: List[CodeSection]) -> CheckResult: |
| '''Checks that there are no branches into or out of loop bodies. |
| |
| Branches within the same loop body are permitted (but not branches from an |
| inner loop to an outer loop, as this counts as branching out of the inner |
| loop). Because this isn't necessarily a fatal issue (for instance, it's |
| possible the branched-to code will always return to the loop), this check |
| returns warnings rather than errors. |
| |
| A `jal` instruction with a register other than `x1` as the first operand is |
| treated the same as a branch and not permitted to cross the loop-body |
| boundary. |
| ''' |
| out = CheckResult() |
| |
| # Check all bne and beq instructions, as well as `jal` instructions with |
| # first operands other than x1 (unconditional branch) |
| to_check = _get_branches(program) |
| for pc in _get_pcs_for_mnemonics(program, ['jal']): |
| operands = program.get_operands(pc) |
| if operands['grd'] != 1: |
| to_check.append(pc) |
| |
| for pc in to_check: |
| operands = program.get_operands(pc) |
| branch_addr = operands['offset'] & ((1 << 32) - 1) |
| |
| # Get the loop bodies the branch is inside, if any |
| current_loops = [] |
| for loop in loops: |
| if pc in loop: |
| current_loops.append(loop) |
| |
| # Check that we're not branching out of any loop bodies |
| for loop in current_loops: |
| if branch_addr not in loop: |
| insn = program.get_insn(pc) |
| out.warn( |
| 'Branch out of loop at PC {:#x} (loop from PC {:#x} to PC ' |
| '{:#x}, branch {} to PC {:#x}). This might cause problems ' |
| 'with the loop stack and surprising behavior.'.format( |
| pc, loop.start, loop.end, insn.mnemonic, branch_addr)) |
| |
| # Check that we're not branching *into* a loop body that the branch |
| # instruction is not already in |
| for loop in loops: |
| if (branch_addr in loop) and (loop not in current_loops): |
| out.warn( |
| 'Branch into loop at PC {:#x} (loop from PC {:#x} to PC ' |
| '{:#x}, branch {} to PC {:#x}). This might cause problems ' |
| 'with the loop stack and surprising behavior.'.format( |
| pc, loop.start, loop.end, insn.mnemonic, branch_addr)) |
| |
| return out |
| |
| |
| def _check_loop_stack(program: OTBNProgram, |
| loops: List[CodeSection]) -> CheckResult: |
| '''Checks that loops will likely be properly cleared from loop stack. |
| |
| The checks here are based on the OTBN hardware IP documentation on loop |
| nesting. From the docs: |
| |
| To avoid polluting the loop stack and avoid surprising behaviour, the |
| programmer must ensure that: |
| |
| * Even if there are branches and jumps within a loop body, the final |
| instruction of the loop body gets executed exactly once per |
| iteration. |
| |
| * Nested loops have distinct end addresses. |
| |
| * The end instruction of an outer loop is not executed before an inner |
| loop finishes. |
| |
| In order to avoid simulating the control flow of the entire program to |
| check the first and third conditions, this check takes a conservative, |
| simplistic approach and simply warns about all branching into or out of |
| loop bodies, including jumps that don't use the call stack (e.g. `jal x0, |
| <addr>`). Branching to locations within the same loop body is permitted. |
| |
| The second condition in the list, distinct end addresses, is checked |
| separately. |
| ''' |
| out = CheckResult() |
| out += _check_loop_branching(program, loops) |
| |
| # Check that loops have unique end addresses |
| end_addrs = [] |
| for loop in loops: |
| if loop.end in end_addrs: |
| out.err( |
| 'Loop starting at PC {:#x} shares a final instruction with ' |
| 'another loop; consider adding a NOP instruction.'.format( |
| loop.start)) |
| |
| return out |
| |
| |
| def check_loop(program: OTBNProgram) -> CheckResult: |
| '''Check that loops are properly formed. |
| |
| Performs three checks to rule out certain classes of loop errors and |
| undefined behavior: |
| 1. For loopi instructions, check that the number of iterations is > 0. |
| 2. Ensure that loops do not end in control-flow instructions such as jal or |
| bne, which will raise LOOP errors. |
| 3. Checks that there is no branching into or out of loop bodies. |
| 4. For nested loops, the inner loop is completely contained within the |
| outer loop. |
| ''' |
| loops = _get_loops(program) |
| out = CheckResult() |
| out += _check_loop_iterations(program, loops) |
| out += _check_loop_end_insns(program, loops) |
| out += _check_loop_stack(program, loops) |
| out += _check_loop_inclusion(program, loops) |
| out.set_prefix('check_loop: ') |
| return out |
| |
| |
| def main() -> int: |
| parser = argparse.ArgumentParser() |
| parser.add_argument('elf', help=('The .elf file to check.')) |
| parser.add_argument('-v', '--verbose', action='store_true') |
| args = parser.parse_args() |
| program = decode_elf(args.elf) |
| result = check_loop(program) |
| if args.verbose or result.has_errors() or result.has_warnings(): |
| print(result.report()) |
| return 1 if result.has_errors() else 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |