Skip to content

Commit

Permalink
Merge branch 'main' into issue-366-_Bugfinder_Add_integration_test_fo…
Browse files Browse the repository at this point in the history
…r_bugfinder_script
  • Loading branch information
ebehner authored Apr 4, 2024
2 parents 0a34b79 + 4d9cd14 commit 77ca3e4
Show file tree
Hide file tree
Showing 151 changed files with 2,655 additions and 1,337 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Automatic release

on:
workflow_dispatch:
schedule:
- cron: '0 0 1 */3 *'

jobs:
release:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v3
with: # https://github.com/actions/checkout/issues/1471
fetch-depth: 0
fetch-tags: true

- name: Set git user
uses: fregante/setup-git-user@v2

- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: 3.x

- name: Update repository for release
id: update
run: echo "version=$(python update_for_release.py)" >> "$GITHUB_OUTPUT" # returns new version to output

- name: Commit changes & push
run: |
git add plugin.json
git commit -m "Update for release ${{ steps.update.outputs.version }}"
git tag "v${{ steps.update.outputs.version }}"
git push origin v${{ steps.update.outputs.version }}
- name: Create release
uses: softprops/action-gh-release@v1
with:
name: Release ${{ steps.update.outputs.version }}
body: "Automatic release"
tag_name: v${{ steps.update.outputs.version }}
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ RUN apt -y update && apt -y upgrade && apt install -y --no-install-recommends \
virtualenv \
unzip \
astyle \
graphviz \
# plotting ascii graphs for debug purposes
libgraph-easy-perl \
z3
Expand Down
1 change: 1 addition & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Main plugin file registering plugin commands in bianryninja."""

from logging import info, warning
from os.path import dirname, realpath
from sys import path
Expand Down
80 changes: 71 additions & 9 deletions decompiler/backend/cexpressiongenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,37 @@
from itertools import chain, repeat

from decompiler.structures import pseudo as expressions
from decompiler.structures.pseudo import Float, FunctionTypeDef, Integer, OperationType, Pointer, StringSymbol, Type
from decompiler.structures.pseudo import (
ArrayType,
CustomType,
Float,
FunctionTypeDef,
GlobalVariable,
Integer,
OperationType,
Pointer,
Type,
)
from decompiler.structures.pseudo import instructions as instructions
from decompiler.structures.pseudo import operations as operations
from decompiler.structures.pseudo.operations import MemberAccess
from decompiler.structures.visitors.interfaces import DataflowObjectVisitorInterface
from decompiler.util.integer_util import normalize_int

MAX_GLOBAL_INIT_LENGTH = 128


def inline_global_variable(var) -> bool:
if not var.is_constant:
return False
match var.type:
case ArrayType():
if var.type.type in [Integer.char(), CustomType.wchar16(), CustomType.wchar32()]:
return True
case _:
return False
return False


class CExpressionGenerator(DataflowObjectVisitorInterface):
"""Generate C code for Expressions.
Expand Down Expand Up @@ -145,17 +169,52 @@ def visit_unknown_expression(self, expr: expressions.UnknownExpression) -> str:

def visit_constant(self, expr: expressions.Constant) -> str:
"""Return constant in a format that will be parsed correctly by a compiler."""
if isinstance(expr, expressions.NotUseableConstant):
return expr.value
if isinstance(expr, expressions.Symbol):
return expr.name
if isinstance(expr.type, Integer):
value = self._get_integer_literal_value(expr)
return self._format_integer_literal(expr.type, value)
if isinstance(expr, StringSymbol):
return expr.name
if isinstance(expr.type, Pointer):
match (expr.value):
case (
str()
): # Technically every string will be lifted as an ConstantArray. Will still leave this, if someone creates a string as a char*
string = expr.value if len(expr.value) <= MAX_GLOBAL_INIT_LENGTH else expr.value[:MAX_GLOBAL_INIT_LENGTH] + "..."
match expr.type.type:
case CustomType(text="wchar16") | CustomType(text="wchar32"):
return f'L"{string}"'
case _:
return f'"{string}"'
case bytes():
val = "".join("\\x{:02x}".format(x) for x in expr.value)
return f'"{val}"' if len(val) <= MAX_GLOBAL_INIT_LENGTH else f'"{val[:MAX_GLOBAL_INIT_LENGTH]}..."'

return self._format_string_literal(expr)

def visit_constant_composition(self, expr: expressions.ConstantComposition):
"""Visit a Constant Array."""
match expr.type.type:
case CustomType(text="wchar16") | CustomType(text="wchar32"):
val = "".join([x.value for x in expr.value])
return f'L"{val}"' if len(val) <= MAX_GLOBAL_INIT_LENGTH else f'L"{val[:MAX_GLOBAL_INIT_LENGTH]}..."'
case Integer(8):
val = "".join([x.value for x in expr.value][:MAX_GLOBAL_INIT_LENGTH])
return f'"{val}"' if len(val) <= MAX_GLOBAL_INIT_LENGTH else f'"{val[:MAX_GLOBAL_INIT_LENGTH]}..."'
case _:
return f'{", ".join([self.visit(x) for x in expr.value])}' # Todo: Should we print every member? Could get pretty big

def visit_variable(self, expr: expressions.Variable) -> str:
"""Return a string representation of the variable."""
return f"{expr.name}" if (label := expr.ssa_label) is None else f"{expr.name}_{label}"

def visit_global_variable(self, expr: expressions.GlobalVariable):
"""Inline a global variable if its initial value is constant and not of void type"""
if inline_global_variable(expr):
return self.visit(expr.initial_value)
return expr.name

def visit_register_pair(self, expr: expressions.Variable) -> str:
"""Return a string representation of the register pair and log."""
logging.error(f"generated code for register pair {expr}")
Expand All @@ -167,10 +226,12 @@ def visit_list_operation(self, op: operations.ListOperation) -> str:

def visit_unary_operation(self, op: operations.UnaryOperation) -> str:
"""Return a string representation of the given unary operation (e.g. !a or &a)."""
operand = self._visit_bracketed(op.operand) if self._has_lower_precedence(op.operand, op) else self.visit(op.operand)
if op.operation == OperationType.address and isinstance(op.operand, GlobalVariable) and isinstance(op.operand.type, ArrayType):
return operand
if isinstance(op, MemberAccess):
operator_str = "->" if isinstance(op.struct_variable.type, Pointer) else self.C_SYNTAX[op.operation]
return f"{self.visit(op.struct_variable)}{operator_str}{op.member_name}"
operand = self._visit_bracketed(op.operand) if self._has_lower_precedence(op.operand, op) else self.visit(op.operand)
return f"{operand}{operator_str}{op.member_name}"
if op.operation == OperationType.cast and op.contraction:
return f"({int(op.type.size / 8)}: ){operand}"
if op.operation == OperationType.cast:
Expand Down Expand Up @@ -209,10 +270,9 @@ def visit_call(self, op: operations.Call) -> str:
Generic labels starting with 'arg' e.g. 'arg1', 'arg2' are being filtered.
Additionally we filter ellipsis argument '...' that is lifted from type string.
"""
func_name = self.visit(op.function)
if isinstance(op.function, expressions.Constant):
func_name = func_name.strip('"')
output = f"{func_name}("
func_expr_str = self._visit_bracketed(op.function) if self._has_lower_precedence(op.function, op) else self.visit(op.function)

output = f"{func_expr_str}("
if op.meta_data is not None:
parameter_names = op.meta_data.get("param_names", [])
is_tailcall = op.meta_data.get("is_tailcall")
Expand Down Expand Up @@ -354,5 +414,7 @@ def format_variables_declaration(var_type: Type, var_names: list[str]) -> str:
parameter_names = ", ".join(str(parameter) for parameter in fun_type.parameters)
declarations_without_return_type = [f"(* {var_name})({parameter_names})" for var_name in var_names]
return f"{fun_type.return_type} {', '.join(declarations_without_return_type)}"
case ArrayType():
return f"{var_type.type}* {', '.join(var_names)}"
case _:
return f"{var_type} {', '.join(var_names)}"
1 change: 1 addition & 0 deletions decompiler/backend/codegenerator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Module in charge of bundling all classes utilized to generate c-code from an AST."""

from string import Template
from typing import Iterable, List

Expand Down
30 changes: 27 additions & 3 deletions decompiler/backend/codevisitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(self, task: DecompilerTask):
self._int_repr_scope: int = task.options.getint("code-generator.int_representation_scope", fallback=256)
self._neg_hex_as_twos_complement: bool = task.options.getboolean("code-generator.negative_hex_as_twos_complement", fallback=True)
self._aggressive_array_detection: bool = task.options.getboolean("code-generator.aggressive_array_detection", fallback=False)
self._preferred_true_branch: str = task.options.getstring("code-generator.preferred_true_branch", fallback="none")
self.task = task

def visit_seq_node(self, node: ast_nodes.SeqNode) -> str:
Expand Down Expand Up @@ -70,10 +71,33 @@ def visit_condition_node(self, node: ast_nodes.ConditionNode) -> str:
true_str = self.visit(node.true_branch_child)
if node.false_branch is None:
return f"if ({self._condition_string(node.condition)}) {{{true_str}}}"

false_str = self.visit(node.false_branch_child)
if isinstance(node.false_branch_child, ast_nodes.ConditionNode):
return f"if ({self._condition_string(node.condition)}){{{true_str}}} else {false_str}"
return f"if ({self._condition_string(node.condition)}){{{true_str}}} else{{{false_str}}}"

condition = node.condition
true_child = node.true_branch_child
false_child = node.false_branch_child

swap_branches = None

# if only one branch is a condition node, we want to decide swapping by which branch is a condition node
if isinstance(false_child, ast_nodes.ConditionNode) != isinstance(true_child, ast_nodes.ConditionNode):
swap_branches = not isinstance(false_child, ast_nodes.ConditionNode)

# if we haven't already decided on swapping (swap_branches is None), decide by length
if swap_branches is None:
length_comparisons = {"none": None, "smallest": len(true_str) > len(false_str), "largest": len(true_str) < len(false_str)}
swap_branches = length_comparisons[self._preferred_true_branch]

if swap_branches:
condition = ~condition
true_str, false_str = false_str, true_str
true_child, false_child = false_child, true_child

if isinstance(false_child, ast_nodes.ConditionNode):
return f"if ({self._condition_string(condition)}) {{{true_str}}} else {false_str}"
else:
return f"if ({self._condition_string(condition)}) {{{true_str}}} else {{{false_str}}}"

def visit_true_node(self, node: ast_nodes.TrueNode) -> str:
"""Generate code for the given TrueNode by evaluating its child (Wrapper)."""
Expand Down
Loading

0 comments on commit 77ca3e4

Please sign in to comment.