Files
revng-revng/python/revng/internal/cli/_commands/idb_converter.py
Giacomo Vercesi bfed42ed1e Add revng.support.get_llvmcpy helper
Add the `get_llvmcpy` helper function which returns an instance of
`LLVMCPy` which uses the `llvm-config` that's present in the search
paths.
2025-05-05 16:02:40 +02:00

652 lines
27 KiB
Python

#
# This file is distributed under the MIT License. See LICENSE.md for details.
#
# mypy: disable-error-code="attr-defined,name-defined"
import sys
from typing import Dict, List, Optional, Set, Tuple, Union
import idb
import idb.analysis
import idb.fileformat
import idb.typeinf
import idb.typeinf_flags
import revng.model as m
from revng.model.metaaddress import MetaAddressType
from revng.support import log_error
RevngTypeDefinitions = Union[
m.UnionDefinition,
m.StructDefinition,
m.EnumDefinition,
m.TypedefDefinition,
m.CABIFunctionDefinition,
# Omit RFDs, as this importer never emits them.
# m.RawFunctionDefinition,
]
idb_procname_to_revng_arch = {
"x86_64": m.Architecture.x86_64,
"x86": m.Architecture.x86,
"arm": m.Architecture.arm,
"aarch64": m.Architecture.aarch64,
"mips": m.Architecture.mips,
"mipsb": m.Architecture.mips,
"mipsl": m.Architecture.mipsel,
"s390x": m.Architecture.systemz,
}
revng_arch_to_metaaddr_code_type = {
m.Architecture.x86: MetaAddressType.Code_x86,
m.Architecture.x86_64: MetaAddressType.Code_x86_64,
m.Architecture.arm: MetaAddressType.Code_arm,
m.Architecture.aarch64: MetaAddressType.Code_aarch64,
m.Architecture.mips: MetaAddressType.Code_mips,
m.Architecture.mipsel: MetaAddressType.Code_mipsel,
m.Architecture.systemz: MetaAddressType.Code_systemz,
}
revng_arch_to_abi = {
m.Architecture.arm: m.ABI.AAPCS,
m.Architecture.aarch64: m.ABI.AAPCS64,
m.Architecture.x86: m.ABI.SystemV_x86,
m.Architecture.x86_64: m.ABI.SystemV_x86_64,
m.Architecture.mips: m.ABI.SystemV_MIPS_o32,
m.Architecture.mipsel: m.ABI.SystemV_MIPSEL_o32,
m.Architecture.systemz: m.ABI.SystemZ_s390x,
}
revng_arch_to_abiname = {
m.Architecture.arm: "AAPCS",
m.Architecture.aarch64: "AAPCS64",
m.Architecture.x86: "SystemV_x86",
m.Architecture.x86_64: "SystemV_x86_64",
m.Architecture.mips: "SystemV_MIPS_o32",
m.Architecture.mipsel: "SystemV_MIPSEL_o32",
m.Architecture.systemz: "SystemZ_s390x",
}
def get_pointer_size(architecture):
match architecture: # noqa: R503
case m.Architecture.x86 | m.Architecture.arm | m.Architecture.mips | m.Architecture.mipsel:
return 4
case m.Architecture.x86_64 | m.Architecture.aarch64 | m.Architecture.systemz:
return 8
case default: # noqa: F841
return None
class IDBConverter:
def __init__(self, input_idb: idb.fileformat.IDB, base_addr, verbose):
self.idb: idb.fileformat.IDB = input_idb
self.api = idb.IDAPython(self.idb)
self.arch: m.Architecture = self._get_arch()
self.is64bit: bool = self._is_64_bit()
self.segments: List[m.Segment] = []
self.revng_types_by_id: Dict[int, RevngTypeDefinitions] = {}
self.idb_types_to_revng_types: Dict[int, m.Type] = {}
self.functions: Set[m.Function] = set()
self.dynamic_functions: List[m.DynamicFunction] = []
self.imported_libraries: List[str] = []
self.base_addr = base_addr
self.verbose = verbose
self._structs_to_fixup: Set[Tuple[m.StructDefinition, idb.typeinf.TInfo]] = set()
self._ordinal_types_to_fixup: Set[Tuple[m.Type, int]] = set()
self._unions_to_fixup: Set[Tuple[m.UnionDefinition, idb.typeinf.TInfo]] = set()
self._import_types()
self._fixup_structs()
self._fixup_unions()
self._fixup_ordinal_types()
self._import_functions()
self._collect_imports()
def log(self, message):
if self.verbose:
sys.stderr.write(message + "\n")
def _import_types(self):
"""Imports initial types from the IDB. The types will be incomplete and need to be fixed"""
til = self.idb.til
for type_definition in til.types.defs:
assert isinstance(type_definition, idb.typeinf.TILTypeInfo)
the_type = type_definition.type
assert isinstance(the_type, idb.typeinf.TInfo)
self._convert_idb_type_to_revng_type(the_type, ordinal=type_definition.ordinal)
def _import_functions(self):
metaaddr_type = revng_arch_to_metaaddr_code_type[self.arch]
if self.arch == m.Architecture.arm and self.api.idc.ItemSize(0x0) == 2:
# If the instructions are 16-bit long, it is a Thumb mode.
metaaddr_type = MetaAddressType.Code_arm_thumb
for function_start_addr in self.api.idautils.Functions():
function = idb.analysis.Function(self.idb, function_start_addr)
function_name = function.get_name()
idb_function_type = function.get_signature()
function_attributes: List[m.FunctionAttribute] = []
if self.api.ida_nalt.is_noret(function_start_addr):
function_attributes.insert(0, m.FunctionAttribute.NoReturn)
function_start_addr += self.base_addr
if idb_function_type is not None:
prototype = self._convert_idb_type_to_revng_type(idb_function_type)
revng_function = m.Function(
Name=function_name,
Entry=m.MetaAddress(Address=function_start_addr, Type=metaaddr_type),
Attributes=function_attributes,
Prototype=prototype,
)
else:
self.log(f"warning: Function {function_name} without a signature.")
revng_function = m.Function(
Name=function_name,
Entry=m.MetaAddress(Address=function_start_addr, Type=metaaddr_type),
Attributes=function_attributes,
)
self.functions.add(revng_function)
def _get_arch(self):
inf_structure = self.api.idaapi.get_inf_structure()
procname = inf_structure.procname.lower()
if procname == "arm" and inf_structure.is_64bit():
procname = "aarch64"
elif procname == "metapc" and inf_structure.is_64bit():
procname = "x86_64"
elif procname == "metapc" and inf_structure.is_32bit():
procname = "x86"
return idb_procname_to_revng_arch[procname]
def _is_64_bit(self):
inf_structure = self.api.idaapi.get_inf_structure()
return inf_structure.is_64bit()
def _import_names_helper(self, function_addr, function_name):
function = idb.analysis.Function(self.idb, function_addr)
if not function:
log_error(f"Unable to find function {function_name}.")
return True
function_name = function.get_name()
function_type = None
try:
function_type = function.get_signature()
except Exception as exception:
log_error(f"Unable to parse function type for {function_name}.")
log_error(str(exception))
function_type = None
return True
if function_type is not None:
prototype = self._convert_idb_type_to_revng_type(function_type)
else:
prototype_definition = m.CABIFunctionDefinition(
ABI=revng_arch_to_abiname[self.arch],
Arguments=[],
)
self.revng_types_by_id[prototype_definition.ID] = prototype_definition
prototype = self._type_for_definition(prototype_definition)
dynamic_function = m.DynamicFunction(
Name=function_name,
Prototype=prototype,
)
self.dynamic_functions.append(dynamic_function)
return True
def _collect_imports(self):
# NOTE: If @plt was used, the functions are in Functions() already, but the name starts
# with ".", and we do import types for them during parsing of regular/local functions.
# NOTE: We cannot use api.ida_nalt.get_import_module_qty() here, since it relies
# on `library names`, but IDA prints `.dynsym` as library for each library in case of ELF.
for mod_index in range(self.api.ida_nalt.get_import_module_qty() + 1):
def import_names_callback(function_addr, function_name, ignored_always_none):
assert ignored_always_none is None
return self._import_names_helper(function_addr, function_name)
try:
mod_name = self.api.ida_nalt.get_import_module_name(mod_index)
except KeyError as exception:
# TODO: Due to the bug mentioned below, we try sometimes to
# find imported modules that do not exist.
if mod_index != 0:
# It would be very strange if the mod_index is not 0.
log_error(f"Unable to find module with index: {mod_index}.")
log_error(str(exception))
continue
# NOTE: IDA 7 has a bug when reporting imported library names - prints `.dynsym` for
# each import.
if mod_name and mod_name != ".dynsym":
self.imported_libraries.append(mod_name)
self.api.ida_nalt.enum_import_names(mod_index, import_names_callback)
def _fixup_ordinal_types(self):
"""Fixes some types that were referring an ordinal that was not observed yet."""
while self._ordinal_types_to_fixup:
placeholder_type, idb_ordinal_type = self._ordinal_types_to_fixup.pop()
assert idb_ordinal_type in self.idb_types_to_revng_types
real_type = self.idb_types_to_revng_types[idb_ordinal_type]
placeholder_definition = self.unwrap_definition(placeholder_type)
real_definition = self.unwrap_definition(real_type)
# TODO: Until now, we only ever saw this affecting structs.
assert isinstance(placeholder_definition, m.StructDefinition) and isinstance(
real_definition, m.StructDefinition
)
placeholder_definition.Fields = real_definition.Fields
placeholder_definition.Name = real_definition.Name
placeholder_definition.Size = real_definition.Size
def _fixup_structs(self):
while self._structs_to_fixup:
revng_type, idb_type = self._structs_to_fixup.pop()
assert isinstance(revng_type, m.StructDefinition)
assert idb_type.is_decl_struct()
fields = []
committed_size = 0
# TODO: For now, we are ignoring structs that contain bitfields.
# We should also improve the python-idb package to pickup the struct size directly from
# the IDB files (e.g. to populate the `type.type_details.storage_size` for structs).
for member in idb_type.type_details.members:
if member.type.is_decl_bitfield():
self.log(
f"warning: Ignoring {revng_type.Name} struct that contains a " "bitfield."
)
return
for member in idb_type.type_details.members:
underlying_type = self._convert_idb_type_to_revng_type(member.type)
revng_member = m.StructField(
Name=member.name,
Type=underlying_type,
Offset=committed_size,
)
member_size = member.type.get_size()
if member_size == 0:
self.log(
f"warning: Dropping zero-sized field {member.name} of struct "
f"{revng_type.Name}."
)
else:
fields.append(revng_member)
committed_size += member_size
revng_type.Fields = fields
revng_type.Size = committed_size
def _fixup_unions(self):
while self._unions_to_fixup:
revng_type, idb_type = self._unions_to_fixup.pop()
assert isinstance(revng_type, m.UnionDefinition)
assert idb_type.is_decl_union()
fields = []
for idx, member in enumerate(idb_type.type_details.members):
qualified_type = self._convert_idb_type_to_revng_type(member.type)
revng_member = m.UnionField(
Name=member.name,
Type=qualified_type,
Index=idx,
)
fields.append(revng_member)
revng_type.Fields = fields
def _convert_idb_type_to_revng_type(
self,
type: idb.typeinf.TInfo, # noqa: A002
ordinal=None,
) -> m.Type:
"""
Converts the given TInfo obtained from python-idb to the corresponding revng type.
If available, the integer identifying the type in the IDB (ordinal) should be supplied
to allow handling circular references. If a type with the given ordinal was already
converted the same instance is returned.
"""
assert isinstance(type, idb.typeinf.TInfo)
# Check if we already converted this type, and if so return the existing type.
# Fundamental to handle circular dependencies.
existing_revng_type = self.idb_types_to_revng_types.get(ordinal)
if existing_revng_type is not None:
return existing_revng_type
type_name = type.get_name()
resulting_definition = None
if type.is_decl_typedef():
aliased_type = type.get_final_tinfo()
# IDA's types could be generated/identified in two ways: by ordinal and by names,
# so we handle both ways here.
aliased_type_ordinal = None
if type.type_details.is_ordref:
aliased_type_ordinal = type.type_details.ordinal
else:
aliased_tiltypeinfo = type.til.types.find_by_name(aliased_type.name)
if aliased_tiltypeinfo:
aliased_type_ordinal = aliased_tiltypeinfo.ordinal
underlying = self._convert_idb_type_to_revng_type(
aliased_type, ordinal=aliased_type_ordinal
)
# IDA uses c-like type system in that for each struct/union a corresponding typedef
# is introduced. In our model type system, such typedefs are already included
# in the struct/union definitions (we emit them automatically for each aggregate, with
# no need for an explicit typedef - de facto, always introducing their names into
# the global namespace).
#
# As such, when importing from IDA, unless a typedef carries any extra information,
# there's no point of importing it (it's harmful, actually, as it introduces multiple
# types sharing the same name) - so we just skip it here.
if (
aliased_type.get_name() == type_name
and type.is_decl_const() == aliased_type.is_decl_const()
and isinstance(underlying, m.DefinedType)
):
# skip unnecessary typedefs
resulting_definition = self.unwrap_definition(underlying)
else:
resulting_definition = m.TypedefDefinition(
Name=type_name, UnderlyingType=underlying
)
elif type.is_decl_enum():
underlying = m.PrimitiveType(
PrimitiveKind=m.PrimitiveKind.Unsigned, Size=type.type_details.storage_size
)
entries = []
for member in type.type_details.members:
if member.value >= 2 ** (underlying.Size * 8):
self.log(
f"warning: Value {hex(member.value)} for enum member "
f"{type_name}.{member.name} out of range, ignoring it."
)
continue
# TODO: We should keep the user comment which might exist in member.cmt.
enum_entry = m.EnumEntry(
Name=member.name,
Value=member.value,
)
entries.append(enum_entry)
if len(entries) == 0:
self.log(f"warning: An empty enum type: {type_name}, emitting a typedef instead.")
resulting_definition = m.TypedefDefinition(
Name=type_name, UnderlyingType=underlying
)
else:
resulting_definition = m.EnumDefinition(
Name=type_name,
Entries=entries,
UnderlyingType=underlying,
)
elif type.is_decl_struct():
if type.type_details.ref is not None and type.type_details.ref.type_details.is_ordref:
if type.type_details.ref.type_details.ordinal not in self.idb_types_to_revng_types:
# This is just a forward declaration. Make a placeholder for now.
resulting_definition = m.StructDefinition(Name="", Size=0, Fields=[])
self.revng_types_by_id[resulting_definition.ID] = resulting_definition
wrapped = self._type_for_definition(resulting_definition, type.is_decl_const())
self._ordinal_types_to_fixup.add(
(wrapped, type.type_details.ref.type_details.ordinal)
)
return wrapped
else:
return self.idb_types_to_revng_types[type.type_details.ref.type_details.ordinal]
else:
# Empty structs are not valid in the revng type system.
if len(type.type_details.members) == 0:
self.log(f"warning: Ignoring empty struct {type_name}, typedef it to `void`")
typedef = m.TypedefDefinition(
Name=type_name,
UnderlyingType=m.PrimitiveType(m.PrimitiveKind.Void, 0),
)
self.revng_types_by_id[typedef.ID] = typedef
return self._type_for_definition(typedef, type.is_decl_const())
else:
# Struct members and size will be computed later.
resulting_definition = m.StructDefinition(Name=type_name, Size=0, Fields=[])
self._structs_to_fixup.add((resulting_definition, type))
elif type.is_decl_union():
if type.type_details.ref is not None and type.type_details.ref.type_details.is_ordref:
if type.type_details.ref.type_details.ordinal not in self.idb_types_to_revng_types:
# This is just a forward declaration. Make a placeholder for now.
resulting_definition = m.UnionDefinition(Name="", Fields=[])
self.revng_types_by_id[resulting_definition.ID] = resulting_definition
wrapped = self._type_for_definition(resulting_definition, type.is_decl_const())
self._ordinal_types_to_fixup.add(
(wrapped, type.type_details.ref.type_details.ordinal)
)
return wrapped
else:
return self.idb_types_to_revng_types[type.type_details.ref.type_details.ordinal]
else:
# Empty unions are not valid in the revng type system.
if len(type.type_details.members) == 0:
self.log(f"warning: Ignoring empty union {type_name}, typedef it to `void`")
typedef = m.TypedefDefinition(
Name=type_name,
UnderlyingType=m.PrimitiveType(m.PrimitiveKind.Void, 0),
)
self.revng_types_by_id[typedef.ID] = typedef
return self._type_for_definition(typedef, type.is_decl_const())
else:
# Union members will be computed later.
resulting_definition = m.UnionDefinition(Name=type_name, Fields=[])
self._unions_to_fixup.add((resulting_definition, type))
elif type.is_decl_ptr():
underlying_type = self._convert_idb_type_to_revng_type(type.type_details.obj_type)
result = m.PointerType(
PointeeType=underlying_type,
PointerSize=type.get_size(),
IsConst=type.is_decl_const(),
)
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = result
return result
elif type.is_decl_array():
if type.type_details.n_elems == 0:
self.log(f"warning: Array {type_name} has invalid zero size.")
underlying_type = self._convert_idb_type_to_revng_type(type.type_details.obj_type)
result = m.ArrayType(
ElementType=underlying_type,
ElementCount=type.type_details.n_elems,
IsConst=type.is_decl_const(),
)
if type.is_decl_const():
result.IsConst = True
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = result
return result
elif type.is_decl_bool():
result = m.PrimitiveType(
PrimitiveKind=m.PrimitiveKind.Unsigned,
Size=type.get_size(),
IsConst=type.is_decl_const(),
)
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = result
return result
elif type.is_decl_int() or type.is_decl_floating():
result = m.PrimitiveType(
PrimitiveKind=get_primitive_kind(type),
Size=type.get_size(),
IsConst=type.is_decl_const(),
)
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = result
return result
elif type.is_decl_void():
result = m.PrimitiveType(
PrimitiveKind=m.PrimitiveKind.Void, Size=0, IsConst=type.is_decl_const()
)
if type.get_name() != "":
# Treat this as `typedef void someothername`.
result = m.TypedefDefinition(
Name=type_name, UnderlyingType=result, IsConst=type.is_decl_const()
)
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = result
return result
elif type.is_decl_func():
# TODO: handle non C-ABI functions.
# We cannot handle stack arguments at the moment.
assert type.type_details.stkargs is None
arguments = []
for idx, argument in enumerate(type.type_details.args):
arguments.append(
m.Argument(
Index=idx,
Type=self._convert_idb_type_to_revng_type(argument.type),
Name=argument.name,
)
)
rt = self._convert_idb_type_to_revng_type(type.get_rettype())
if isinstance(rt, m.PrimitiveType) and rt.PrimitiveKind == m.PrimitiveKind.Void:
rt = None
resulting_definition = m.CABIFunctionDefinition(
ABI=revng_arch_to_abi[self.arch],
ReturnType=rt,
Arguments=arguments,
)
elif type.is_decl_partial():
# Represents an unknown or void type with a known size.
assert type.get_size() != 0
# The type should be compatible with being a primitive type.
# NOTE: If we find a case where this is not satisfied, we can produce a char[].
assert (
type.get_size() == 1
or type.get_size() == 2
or type.get_size() == 4
or type.get_size() == 8
or type.get_size() == 10
or type.get_size() == 16
)
result = m.PrimitiveType(
PrimitiveKind=m.PrimitiveKind.Generic,
Size=type.get_size(),
IsConst=type.is_decl_const(),
)
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = result
return result
else:
# IDA does not know anything about this type.
# TODO: In some cases we should emit a void type (e.g. when the type is only ever used
# as a pointer).
if type.get_size() == 0:
result = m.PrimitiveType(
PrimitiveKind=m.PrimitiveKind.Void, Size=0, IsConst=type.is_decl_const()
)
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = result
return result
else:
result = m.PrimitiveType(
PrimitiveKind=m.PrimitiveKind.PointerOrNumber,
Size=type.get_size(),
IsConst=type.is_decl_const(),
)
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = result
return result
assert resulting_definition is not None
result = self._type_for_definition(resulting_definition, type.is_decl_const())
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = result
if not self.revng_types_by_id.get(resulting_definition.ID):
self.revng_types_by_id[resulting_definition.ID] = resulting_definition
return result
def _type_for_definition(self, definition: m.TypeDefinition, is_const: bool = False) -> m.Type:
return m.DefinedType(Definition=m.Reference.create(m.Binary, definition), IsConst=is_const)
def get_model(self) -> m.Binary:
return m.Binary(
# NOTE: We assume that the EntryPoint can be obtained from binary itself, so we use an
# invalid address for it here.
EntryPoint=m.MetaAddress(Address=0x0, Type=MetaAddressType.Invalid),
Functions=list(self.functions),
ImportedDynamicFunctions=list(self.dynamic_functions),
TypeDefinitions=list(self.revng_types_by_id.values()),
Architecture=self.arch,
Segments=self.segments,
ImportedLibraries=self.imported_libraries,
)
def get_revng_type_by_name(self, name):
for revng_type in self.revng_types_by_id.values():
if revng_type.Name == name:
return revng_type
return None
def unwrap_definition(self, defined_type: m.Type) -> Optional[RevngTypeDefinitions]:
if not isinstance(defined_type, m.DefinedType):
raise ValueError("Trying to unwrap a nested definition:\n" + str(defined_type))
return self.revng_types_by_id.get(defined_type.Definition.id)
def get_primitive_kind(idb_type: idb.typeinf.TInfo) -> m.PrimitiveKind:
if idb_type.is_decl_void():
return m.PrimitiveKind.Void
elif idb_type.is_decl_floating():
return m.PrimitiveKind.Float
elif idb.typeinf_flags.is_type_integral(idb_type.get_decltype()):
if idb_type.is_signed():
return m.PrimitiveKind.Signed
elif idb_type.is_unsigned():
return m.PrimitiveKind.Unsigned
else:
return m.PrimitiveKind.Number
elif idb_type.is_decl_ptr():
return m.PrimitiveKind.PointerOrNumber
return m.PrimitiveKind.Generic