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:
- The compiler had to move
DeviceObject
from the stack to a register to initialize the global variable qword_132D0 and also modify theFlags
member. It picked the registerrax
for that; - Because
rax
already contained theresult
variable (in the lower part of it:eax
), it had to be saved elsewhere in the meantime (and moved back toeax
at the end of manipulations withDeviceObject
); - The decompiler could not automatically merge
DeviceObject
withv6
because they use different storage types (stack vs register) and because in theory the writes toDriverObject->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