Repository navigation
Managed ilasm: remaining PDB differences from native ilasm after #135289, #135297, #135311 and #135312 #135314
Description
Activity
- addeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Oct 6, 2026 dotnet-policy-service commented
on Oct 6, 2026 ContributorMore actionsTagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.dotnet-policy-service commented
on Oct 6, 2026 ContributorMore actionsTagging subscribers to this area: @JulieLeeMSFT, @dotnet/jit-contrib
See info in area-owners.md if you want to be subscribed.Point 14, measured. The four bytes are the
#US(user string) heap, and the cause is in the native metadata writer rather than in either PDB writer.native PDB managed PDB #USheap8 bytes: 00 03 20 00 00+ 3 padding4 bytes: 00+ 3 paddingevery other stream same size (in 27 of the 30 pairs below) same size Native's heap holds the mandatory empty entry followed by one user string, a single space.
CLiteWeightStgdbRW::GetSaveSize(src/coreclr/md/enc/liteweightstgdbrw.cpp) adds it to any user-string heap that is still empty at save time, with the comment "An empty user string pool causes problems with edit and continue". Native ilasm saves the PDB through that same engine, and a PDB never has user strings, so every native PDB gets it. The same function adds a" "string to an empty#Stringsheap, underFEATURE_METADATA_EMIT_PORTABLE_PDB, "in order to be recognized by the VS debugger". That one fits in the heap's alignment padding (00 20 00 00against00 00 00 00), so it never shows as a size difference; it is in every native PDB whose#Stringsheap would otherwise be empty and absent once a local variable name is in the heap.Why native needs them: its pool-saving step skips a pool with no data ("If there is no data, then don't bother"), so without the seeds the
#USand#Stringsstreams would be missing from the file. The managed ilasm writes through System.Reflection.Metadata, which emits the heap streams even when they hold only the empty entry; a Roslyn-built PDB has the same 4-byte#USheap.Which is right: both are structurally valid. ECMA-335 lets a writer omit or keep an empty heap and allows unreferenced entries, and the Portable PDB spec requires only the
#Pdband#~streams. The debugging tables never reference#US. The readers in this repository (MetadataReader,PortablePdbSymbolReader,StackTraceSymbols) do not depend on the#Stringsseed either; I have not tested Visual Studio against a PDB whose#Stringsheap is empty, which is what native's comment is about. I would leave the managed output as it is. No change proposed.Evidence (Linux x64; native ilasm built from
main, managed at22a31f5c325, #135312's head): 35 inputs, each assembled by both tools from the same directory with the same input path and output name. 30 produced a PDB from both tools; native rejected four (the#lineand partial.lineinputs of items 1 and 2, and one.line-only input) and one failed in my harness over an include path. The two PDBs of each pair were compared stream by stream, the#Pdbstream after its 20-byte id, which always differs. In all 30 the#USheaps are exactly the two byte strings above. 27 of the 30 PDBs differ in size by exactly 4 bytes. The other three: two#includeinputs where native keeps a Document row for an included file that has no sequence point of its own (the deliberate difference listed in #135312), and a netmodule, whose native#Pdbstream records theMemberRefandCustomAttributerow counts of theDebuggableAttributenative attaches to a netmodule (item 4). As a check on the mechanism, an input withldstr "x"gives DLLs whose#USheaps agree in size, because native does not seed a non-empty heap, and PDBs that are still 4 bytes apart.For anyone comparing the two PDBs byte for byte, the streams of equal size are not byte-identical, for these reasons: the
Sortedmask in the#~header (native also marks absent type-system tables as sorted; System.Reflection.Metadata's PDB writer marks the present sorted debugging tables); theTypeRefrow count in#Pdb, which mirrors the DLL, where the managed tool emits aTypeReftoSystem.ValueTypein 24 of the 30 inputs through the lookup in its unsealed-type check (GrammarActions.Types.Headers.cs), unreferenced when the type extendsObject; the order of local names in#Stringsin two inputs; the separator byte of a document name with no directory part (native 0, managed/, same name); and the stream order (managed writes#Pdbfirst, native last). In the DLL, not the PDB, strings emitted through native'sDefineUserStringget a trailing flag byte of 1 ("This byte is not used by the runtime"), where System.Reflection.Metadata computes it as ECMA-335 II.24.2.4 specifies. TheValueTypereference and the trailing byte are DLL matters; I can add them as rows here if you want them tracked.Note
This measurement and this note were prepared with AI assistance (Anthropic Claude and OpenAI Codex) under my direction. AI agents ran the probes on my machine. I reviewed the text before posting.
Added item 15 to the list above:
#includeresolution. The managed tool resolves an include relative to the current directory and then, unlike native, relative to the first input file's directory; neither tool resolves a nested include relative to the including file. Two questions are in the item. Nothing in the open PRs depends on the answer.Note
This note was prepared with AI assistance (Anthropic Claude) under my direction. I reviewed the text before posting.
- removeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Oct 9, 2026
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
While checking the managed ilasm's (
src/tools/ilasm) PDB output against native ilasm and againstsrc/tests/ilasm/PortablePdb, I found a number of differences. The ones that make that test suite fail are addressed by the PRs below. The rest are listed here, grouped, so they can be picked up one at a time. Nothing beyond the open PRs is planned unless you name it: are any of these important to you, or ones you would like to see addressed? I am happy to take whichever you point at, and to leave alone anything you consider intended or not worth a change.Already addressed (open PRs, in landing order)
.pdbfile with CodeView, PdbChecksum and Reproducible entries..linefile as documents, theLocalSignaturerow id in the sequence-point blob header.LocalScope/LocalVariablerows..ilsource when no.lineis in effect;#included files as documents.Related issues: #135211 (PdbChecksum hashes only the
#Pdbstream;-DETleaves the PDB id's stamp at 0), #135214 (-DETderives MVID, PE timestamp and PDB id from a zero-filled buffer), #135213 (ILLink leaves zero-lengthLocalScoperows), #46328 (end-column default of a partly specified.line).Parity gaps in the managed tool
#line(auto-increment) is parsed but the increment is never applied. Native's implementation is also broken (it increments the start line but not the end line, and then rejects its own sequence point), so this would be spec conformance rather than parity..linedirectives (ilasm throws an error when partly-defined .line directives are used #46328): native fails the assembly; managed emitsstart+1as the end column. The issue proposed0xffff. Which is wanted?0xFEEFEEstart line without0,0columns is treated as hidden, and columns of 0x10000 or more are encoded although the format forbids them. Native warns and fails the method body.-DEBUGgets noDebuggableAttribute; native attaches one viaAssemblyAttributesGoHere. The legacy two-BooleanDebuggableAttributeconstructor for a v1mscorlibtarget is not emitted either.0x04030201; native uses the clock. ThePdbChecksumentry's timestamp is 0 (native: the PDB stamp). Overlaps with ilasm: PdbChecksum hashes only the #Pdb stream, and -DET leaves the PDB ID's stamp at 0 #135211..entrypoint, or one on an instance method, is accepted silently; native reports an error.-ERROR, native drops the rest of a method (and the following method) after a syntax error inside a body; managed keeps both methods, with the bad instruction emitting nothing. Which recovery is intended?recordToString, so a single warning dumps the whole source text (74 lines forTestLocalScopes4.il, which triggers the in-use warning as it does with native). Native printsfile(line) : warning : message. I would send a small PR forfile(line,col) : severity ILAxxxx : messageif that is wanted.4294967296reads as 0. The local-slot path checks the literal as written since Managed ilasm: honour explicit local slots, scope local names lexically, and emit LocalScope/LocalVariable rows #135311; the other consumers do not.Places where the managed tool is stricter than native
.linepath with single backslashes ('C:\p\win.cs') is a lexer error; native silently strips the backslashes (document nameC:pwin.cs)..languageGUID without braces is accepted; native ignores it.Both look like improvements. Should they be listed in
MANAGED-ILASM-FIXES.mdas deliberate?Native defects found on the way
No change to native is proposed; these are noted so the managed behaviour is not changed to match them. Each is already recorded as a deliberate difference in
MANAGED-ILASM-FIXES.mdby the PRs above.EmitLocalslets an implicit local after[65535]take slot 65536, which no local instruction can name, and the PDB writer wraps itsLocalVariableindex to 0;[4294967295]is read as[-1], and a hexadecimal slot literal of more than 16 digits is read by its low bits, all without a diagnostic.#definemacro is mapped to line 1 (the macro text is lexed as a fresh source); after an#include, a.linefile named inside the include stays current in the including file with the include's coordinates.Curiosity
#USheap seed of the native metadata writer; no change proposed (Managed ilasm: remaining PDB differences from native ilasm after #135289, #135297, #135311 and #135312 #135314 (comment)).Added 2026-10-07
#includeresolution. Both tools resolve an#includepath as written, relative to the current directory, and both honour-INCLUDE. The managed tool then also tries the path relative to the first input file's directory, where native fails the include. An#includeinside an included file is resolved the same way as a top-level one in each tool, never relative to the including file. Is the input-directory fallback intended, and should it be listed inMANAGED-ILASM-FIXES.mdas deliberate? Should nested includes resolve relative to the including file, as C preprocessors do? (Measured with the file in the current directory, beside the input, in both, and nested one level, against both tools.)Inputs, commands and dumps for each are available on request; tested on Linux x64 against
mainat4a6440c3f03.Note
This survey and this note were prepared with AI assistance (Anthropic Claude and OpenAI Codex) under my direction. AI agents ran the builds and probes on my machine. I reviewed the text before posting.