Hex-Rays' blog

Igor’s tip of the week #77: Mapped variables – Hex Rays

Written by Igor Skochinsky | Feb 17, 2022

Quick rename can be useful when you have code which copies data around so the variable names stay the same or similar. However, sometimes there is a way to get rid of duplicate variables altogether.

Reasons for duplicate variables

Even if in the source code a specific variable may appear only once, on the machine code level it is not always possible. For example, most arithmetic operations use machine registers, so the values have to be moved from memory to registers to perform them. Conversely, sometimes a value has to be moved to memory from a register, for example:

  • taking a reference/address of a variable requires that it resides in memory;
  • when there are too few available registers, some variables have to be spilled to the stack;
  • when a calling convention uses stack for passing arguments;
  • recursive calls or closures are usually implemented by storing the current variables on the stack;
  • some other situations.

All this means that the same variable may be present in different locations during the lifetime of the function. Although the decompiler tries its best to merge these different locations into a single variable, it is not always possible, so extra variables may appear in the pseudocode.

Example

For a simple example, we can go back to DriverEntry in kprocesshacker.sys from the last post. The initial output looks like this:

NTSTATUS __stdcall DriverEntry(_DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath)
{
  NTSTATUS result; // eax
  NTSTATUS v5; // r11d
  PDEVICE_OBJECT v6; // rax
  struct _UNICODE_STRING DestinationString; // [rsp+40h] [rbp-18h] BYREF
  PDEVICE_OBJECT DeviceObject; // [rsp+60h] [rbp+8h] BYREF

  qword_132C0 = (__int64)DriverObject;
  VersionInformation.dwOSVersionInfoSize = 284;
  result = RtlGetVersion(&VersionInformation);
  if ( result >= 0 )
  {
    result = sub_15100(RegistryPath);
    if ( result >= 0 )
    {
      RtlInitUnicodeString(&DestinationString, L"\\Device\\KProcessHacker3");
      result = IoCreateDevice(DriverObject, 0, &DestinationString, 0x22u, 0x100u, 0, &DeviceObject);
      v5 = result;
      if ( result >= 0 )
      {
        v6 = DeviceObject;
        DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)&sub_11008;
        qword_132D0 = (__int64)v6;
        DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)&sub_1114C;
        DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)&sub_11198;
        DriverObject->DriverUnload = (PDRIVER_UNLOAD)sub_150EC;
        v6->Flags &= ~0x80u;
        return v5;
      }
    }
  }
  return result;
}

We can see that there are two variables which look redundant: v5 and v6. v5 is a copy of result which resides in r11d and v6  is a copy of DeviceObject which resides in rax. It seems they were introduced for related reasons:

  1. The compiler had to move DeviceObject from the stack to a register to initialize the global variable qword_132D0 and also modify the Flags member. It picked the register rax for that;
  2. Because rax already contained the result variable (in the lower part of it: eax), it had to be saved elsewhere in the meantime (and moved back to eax at the end of manipulations with DeviceObject);
  3. The decompiler could not automatically merge DeviceObjectwith v6 because they use different storage types (stack vs register) and because in theory the writes to DriverObject->MajorFunction could have changed the stack variable, so the values would not be the same anymore.

Mapping variables

After looking at the code closely, it seems that v5 and v6 can be replaced correspondingly by result and DeviceObject in all cases. To ask the decompiler do it, we can use “Map to another variable” action from the context menu.

When you use it for the first time, the following warning appears:

Alternatively, you can use the hotkey = (equals sign); it’s best to use it on the initial assignment such as v6 = DeviceObject because then the best match (the other side of assignment) will be preselected in the list of replacement candidates. In our case we get only one candidate, but in big functions you may have several variables of the same type, so triggering the action on an assignment helps ensure that you pick the correct one.

After mapping both variables, the output no longer mentions them:

NTSTATUS __stdcall DriverEntry(_DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath)
{
  NTSTATUS result; // eax MAPDST
  struct _UNICODE_STRING DestinationString; // [rsp+40h] [rbp-18h] BYREF
  PDEVICE_OBJECT DeviceObject; // [rsp+60h] [rbp+8h] MAPDST BYREF

  qword_132C0 = (__int64)DriverObject;
  VersionInformation.dwOSVersionInfoSize = 284;
  result = RtlGetVersion(&VersionInformation);
  if ( result >= 0 )
  {
    result = sub_15100(RegistryPath);
    if ( result >= 0 )
    {
      RtlInitUnicodeString(&DestinationString, L"\\Device\\KProcessHacker3");
      result = IoCreateDevice(DriverObject, 0, &DestinationString, 0x22u, 0x100u, 0, &DeviceObject);
      if ( result >= 0 )
      {
        DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)&sub_11008;
        qword_132D0 = (__int64)DeviceObject;
        DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)&sub_1114C;
        DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)&sub_11198;
        DriverObject->DriverUnload = (PDRIVER_UNLOAD)sub_150EC;
        DeviceObject->Flags &= ~0x80u;
      }
    }
  }
  return result;
}

You can see that result and DeviceObject variables now have a new annotation: MAPDST. This means that some other variable(s) have been mapped to them. 

Unmapping variables

If you’ve changed your mind and want to see how the original pseudocode looked like, or observe something suspicious in the output involving mapped variables, you can remove the mapping by right-clicking a mapped variable (marked with MAPDST) and choosing “Unmap variable(s)”.

More info: Hex-Rays interactive operation: Map to another variable