0xd4d / dnlib

Reads and writes .NET assemblies and modules
MIT License
2.18k stars 587 forks source link

"Expected a null constant." error when saving assembly with pdb #550

Open maxkatz6 opened 8 months ago

maxkatz6 commented 8 months ago

I noticed this issue only happens when I used "local const" in the code, specifically when const type is an enum from another assembly. For example, this method:

public static bool IsMatch(string text, string pattern, CompareInfo compareInfo)
{
    const CompareOptions options = CompareOptions.IgnoreSymbols | CompareOptions.StringSort | CompareOptions.IgnoreNonSpace;
    return compareInfo.Compare(text, pattern, options) == 0;
}

Simply replacing const CompareOptions options with CompareOptions options will fix the problem, so we have a workaround, though this bug is confusing. Repro project (both dll project and executable console app):

ClassLibrary1.zip

maxkatz6 commented 8 months ago

Also, unrelated to the original issue, and could be my misunderstanding, but I can't really run this code:

var filePath = args[0];
var asm = AssemblyDef.Load(filePath);
...
asm.Write(filePath, writerParameters);

as it will fail with System.IO.IOException: The requested operation cannot be performed on a file with a user-mapped section open.. Does dnLib hold file open after loading while trying to write to the same file?

wtfsck commented 8 months ago

It's memory mapped and you're using Windows which doesn't allow writing to the file until it's closed. You could instead read all bytes from the file and pass in the byte[] to the Load() method to load it that way and nothing gets memory mapped.

wtfsck commented 8 months ago

I think you need to add an assembly resolver to fix this. See the README on how to do that.

maxkatz6 commented 8 months ago

It's memory mapped and you're using Windows which doesn't allow writing to the file until it's closed. You could instead read all bytes from the file and pass in the byte[] to the Load() method to load it that way and nothing gets memory mapped.

Yeah, reading from byte array works fine. That's what I also did in the sample.

I think you need to add an assembly resolver to fix this. See the README on how to do that.

Hm. In my actual project I did use an assembly resolver, where I initially found this issue. I trimmed everything down until I got this minimal sample. I can try to update the repro of the issue, but it still fails with resolver. And removing const magically solves the issue.

wtfsck commented 8 months ago

Make sure the assembly resolver finds the assembly with the enum CompareOptions and any other assemblies it references, if any.

UlyssesWu commented 5 months ago

I met this issue, too. Could we just skip this error? Sometimes we cannot find every dll reference, but we still want some pdb info kept. However with this issue we can only get an empty pdb.

I have written some simple code to bypass this but I don't know if it will cause more problems, like making this pdb unusable.

https://github.com/0xd4d/dnlib/blob/1e0ec2607a146c17973e4e71051ff96d2ae7c2bc/src/DotNet/Pdb/Portable/LocalConstantSigBlobWriter.cs#L144-L152

    case ElementType.Class when value is not byte[] && value is not null:
        var realElementType = ConstantUser.GetElementType(value);
        if (realElementType != ElementType.End) {
            writer.WriteByte((byte)realElementType);
            WritePrimitiveValue(writer, realElementType, value);
            WriteTypeDefOrRef(writer, type.ToTypeDefOrRef());
            return;
        }
        break;
philipp-naused commented 2 months ago

As a workaround, you can remove the "invalid" constants from the PDB:

foreach (MethodDef method in new MemberFinder().FindAll(module).MethodDefs.Keys)
{
    IList<PdbConstant>? constants = method.Body?.PdbMethod?.Scope?.Constants;
    if (constants is not null)
    {
        for (int i = constants.Count - 1; i >= 0; i--)
        {
            // workaround for https://github.com/0xd4d/dnlib/issues/550
            // remove any constant with a reference type where the value is not a byte[] or null.
            PdbConstant constant = constants[i];
            if (constant.Type?.ElementType is ElementType.Class &&
                constant.Value is not null and not byte[])
            {
                constants.RemoveAt(i);
            }
        }
    }
}