Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ILSpy uses incorrect variable names when debug symbols are present and variable names end with numbers. #3113

Closed
ElektroKill opened this issue Nov 7, 2023 · 0 comments
Labels
Bug Decompiler The decompiler engine itself

Comments

@ElektroKill
Copy link
Contributor

Input code

public static void Main(string[] args) {
	delegate*<dynamic, dynamic, dynamic> F1 = null;
	delegate*<object, object, dynamic> F2 = null;
	delegate*<dynamic, object, object> F3 = null;
	delegate*<object, dynamic, object> F4 = null;
	delegate*<object, object, object> F5 = null;
	delegate*<object, object, ref dynamic> F6 = null;
	delegate*<ref dynamic, object, object> F7 = null;
	delegate*<object, ref dynamic, object> F8 = null;
	delegate*<ref object, ref object, dynamic> F9 = null;
	delegate*<dynamic, ref object, ref object> F10 = null;
	delegate*<ref object, dynamic, ref object> F11 = null;
	delegate*<object, ref readonly dynamic> F12 = null;
	delegate*<in dynamic, object> F13 = null;
	delegate*<out dynamic, object> F14 = null;
	D<delegate*<dynamic>[], dynamic> F15 = null;
	delegate*<A<object>.B<dynamic>> F16 = null;

	Use(F1);
	Use(F2);
	Use(F3);
	Use(F4);
	Use(F5);
	Use(F6);
	Use(F7);
	Use(F8);
	Use(F9);
	Use(F10);
	Use(F11);
	Use(F12);
	Use(F13);
	Use(F14);
	Use(F15);
	Use(F16);
}

public static void Use(void* t) { }

public static void Use<T>(T t) { }

Erroneous output

public unsafe static void Main(string[] args)
{
	delegate*<object, object, object> F1 = null;
	delegate*<object, object, object> F9 = null;
	delegate*<object, object, object> F10 = null;
	delegate*<object, object, object> F11 = null;
	delegate*<object, object, object> F12 = null;
	delegate*<object, object, ref object> F13 = null;
	delegate*<ref object, object, object> F14 = null;
	delegate*<object, ref object, object> F15 = null;
	delegate*<ref object, ref object, object> F16 = null;
	delegate*<object, ref object, ref object> F2 = null;
	delegate*<ref object, object, ref object> F3 = null;
	delegate*<object, ref readonly object> F4 = null;
	delegate*<in object, object> F5 = null;
	delegate*<out object, object> F6 = null;
	D<delegate*<object>[], object> F7 = null;
	delegate*<A<object>.B<object>> F8 = null;
	Use((void*)F1);
	Use((void*)F9);
	Use((void*)F10);
	Use((void*)F11);
	Use((void*)F12);
	Use((void*)F13);
	Use((void*)F14);
	Use((void*)F15);
	Use((void*)F16);
	Use((void*)F2);
	Use((void*)F3);
	Use((void*)F4);
	Use((void*)F5);
	Use((void*)F6);
	Use(F7);
	Use((void*)F8);
}

public unsafe static void Use(void* t)
{
}

public static void Use<T>(T t)
{
}

Notice the different order of variable names in the Use(X) statements. Looking at the types of variables makes the issue apparent too. It seems like ILSpy is shuffling around the names of the variables.

After some debugging, it turns out that the AssignVariableNames stage is at fault here. It processes the variables in the order returned by OrderBy(v => v.Name). When the stage encounters a non-synthetic variable it triggers this code path:

case VariableKind.Local when v.Index != null:
if (assignedLocalSignatureIndices.TryGetValue(v.Index.Value, out string name))
{
// make sure all local ILVariables that refer to the same slot in the locals signature
// are assigned the same name.
v.Name = name;
}
else
{
AssignName();
// Remember the newly assigned name:
assignedLocalSignatureIndices.Add(v.Index.Value, v.Name);
}
break;

AssignName:

void AssignName()
{
if (v.HasGeneratedName || !IsValidName(v.Name) || ConflictWithLocal(v))
{
// don't use the name from the debug symbols if it looks like a generated name
v.Name = null;
}
else
{
// use the name from the debug symbols
// (but ensure we don't use the same name for two variables)
v.Name = GetAlternativeName(v.Name);
}

For the non-synthetic variables in this file the else block is triggered and a call to GetAlternativeName is made. This call is responsible for the change of F10 to F2 etc. This method uses an assignment algorithm that increments the number at the end of the variable and since the variables are processed in order returned by OrderBy(v => v.Name), F1 will be processed first, F10 next, F11, F12 etc. which causes the variable names to become incorrect.

In my opinion, the order should not really be the thing to worry about here but rather the approach the variable name assigner takes for local variables where the names are almost certainly what the user would want them to be and the processing of them should probably be restricted to validation of them and escaping strange characters in case of obfuscated PDB files.

Details

@ElektroKill ElektroKill added Bug Decompiler The decompiler engine itself labels Nov 7, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 13, 2024
mattsh247 pushed a commit to mattsh247/ILSpy that referenced this issue Jul 30, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug Decompiler The decompiler engine itself
Projects
None yet
Development

No branches or pull requests

1 participant