Repository navigation
Write the managed ilasm PDB to a separate file, as native ilasm does - #135289
Conversation
|
Azure Pipelines: Successfully started running 5 pipeline(s). 11 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @dotnet/jit-contrib |
|
Instead of adding tests under |
|
Thanks, both are done. The output writer is now two types: |
There was a problem hiding this comment.
🟡 Changes recommended
An output symlink targeting its PDB path can cause a successful run to lose the generated image.
2 open findings
What changed in this PR
Updates managed ilasm to emit separate Portable PDB files and align debug-directory behavior with native ilasm.
Changes:
- Returns and writes standalone PDB data with CodeView/checksum entries.
- Adds safe PDB replacement and stale-PDB handling.
- Expands unit, generated, CLI, and round-trip tests.
| File | Description |
|---|---|
SourceDirectiveTests.cs |
Updates source-directive PDB tests. |
OutputWriterTests.cs |
Tests stream-based output decisions. |
OutputFileWriterTests.cs |
Tests filesystem output behavior. |
ModuleTests.cs |
Reads standalone module PDBs. |
ILAssembler.Tests.csproj |
Links CLI output components. |
PdbGeneratedCaseTests.cs |
Adds generated PDB/output tests. |
PdbCaseGenerator.cs |
Generates deterministic test cases. |
DocumentCompilerTestHelpers.cs |
Adds standalone-PDB helpers. |
CompilerOptionsTests.cs |
Tests PDB options and directories. |
CommandLinePdbFileTests.cs |
Adds end-to-end CLI coverage. |
AssemblyTests.cs |
Updates deterministic PDB tests. |
VTableExportPEBuilder.cs |
Uses the shared PE builder. |
OutputWriter.cs |
Implements stream-based output handling. |
Options.cs |
Documents and adds PDB path options. |
ILAssemblerPEBuilder.cs |
Suppresses empty debug directories. |
CompilationResult.cs |
Exposes generated PDB bytes. |
GrammarActions.BuildImage.cs |
Builds standalone PDBs and references. |
Program.cs |
Writes image and PDB outputs. |
OutputFileWriter.cs |
Implements filesystem output handling. |
IlasmRootCommand.cs |
Clarifies debug-mode help. |
README.md |
Documents PDB behavior. |
CLRTest.Jit.targets |
Updates round-trip determinism checks. |
🧠 Review effort: Balanced
jkoritzinsky
left a comment
There was a problem hiding this comment.
One more request. Otherwise looking good to me so far.
jkoritzinsky
left a comment
There was a problem hiding this comment.
One last change, then this will be good to merge.
Head branch was pushed to by a user without write access
da5e802 to
8d4f4e2
Compare
With /DEBUG or /PDB, the managed ilasm embedded the Portable PDB in the image and named "assembly.pdb" in the CodeView entry. Native ilasm writes the PDB as a separate file named after the output, and records the full path of that file in the CodeView entry. Match native ilasm: - The assembler returns the serialized PDB in CompilationResult.PortablePdb instead of embedding it, and the CLI writes it to <output>.pdb beside the output. - The CodeView entry names the PDB path given in the new Options.PdbFilePath. The CLI passes the full path of <output>.pdb. Library callers that do not set it get OutputFileName with its extension replaced by .pdb, or assembly.pdb when there is no output file name. - A PdbChecksum entry follows the CodeView entry. It holds the SHA-256 hash of the PDB with its 20-byte id zeroed, which is the hash the PortablePdbBuilder id provider computes, so the deterministic id and the checksum come from one hash. - With /DET, a Reproducible entry follows, as native ilasm and the C# compiler write it. - An image built without a PDB has no debug directory. ManagedPEBuilder adds a Reproducible-only directory to a deterministic image when it is given none; the new ILAssemblerPEBuilder suppresses that, for the standard and the vtfixup/export builders. The CLI's file output moves into OutputWriter, which the tests compile: - The image is written and closed first. The PDB is then written to a new temporary file in the same directory and renamed over <output>.pdb, so <output>.pdb never holds a partial PDB. - When the image is written without a PDB, an existing <output>.pdb is deleted only if it belongs to the image that was replaced: that image names the PDB's id in its CodeView entry. Any other file is kept. When the assembly fails and produces no image, nothing is written or deleted; with /ERR, an image written despite errors is handled as after a successful assembly. Native ilasm deletes <output>.PDB whether or not it belongs to the replaced image, and also when the assembly fails, and on Linux that is a different file from .pdb; that can remove a PDB that ilasm did not write, so it is not reproduced. - An output named like its PDB (-OUTPUT=Min.pdb with /DEBUG) is refused instead of being overwritten by the PDB. /PDB still produces only the PDB: it adds no DebuggableAttribute and leaves the JIT settings unchanged. Tests now pin that. The README documents the PDB behaviour, and the doc comments state the DebuggableAttribute modes correctly: /DEBUG=IMPL is 0x103, Default | IgnoreSymbolStoreSequencePoints | DisableOptimizations, and does not enable Edit and Continue. Tests: the unit tests read the PDB from the compilation result and pin the debug directory entries and their order, the CodeView path and id, the checksum, deterministic output and the output writer's file handling, including which PDB it deletes and that it closes the image before touching the PDB. Seeded theories check the same rules over generated programs and option combinations, and the output writer over an enumeration of output names and pre-existing output and PDB states. src/tests/ilasm/PortablePdb/IlasmPdbFileTester.cs runs native and managed ilasm end to end. Its managed cases are conditional on CORE_ROOT having the managed ilasm, which is built only where the SDK tools are; cases native ilasm does not satisfy run against the managed ilasm only.
The managed ilasm produced a PDB whenever the source had .line directives, even without /DEBUG or /PDB. Native ilasm produces a PDB only when one of those switches is given. Match native ilasm: a PDB is produced only with --debug, --debug-mode or --pdb. Without them the image has no debug directory, with or without --deterministic, and the .line directives are still parsed and validated but their sequence points are not emitted. --debug-mode alone counts as --debug, as it already did for the DebuggableAttribute. Tests that relied on the implicit PDB now pass Debug = true. New unit tests pin that .line alone produces no PDB, that --debug-mode alone produces one, and that an image without a PDB has no debug directory and no debug data, deterministic or not, with or without .line. Seeded theories over generated programs and options pin that a PDB is produced exactly when a switch requests it and that an image without one has no debug directory and no debug data. IlasmPdbFileTester checks both ilasms end to end without a switch, with and without .line and /DET (the managed ilasm when CORE_ROOT has it). The README and the doc comments say which switches produce a PDB.
…rip harness The "Test PDB determinism" step assembled with -DEBUG to -output=<name>.pdb and hashed that file. With native ilasm the PDB is <output without extension>.pdb, the same path, so the PDB overwrote the image and the step hashed the PDB. With the previous managed ilasm the step hashed the image, which had the PDB embedded, so it also checked that the image assembled with -DEBUG repeats. The managed ilasm now writes the PDB beside the output and refuses an output path the PDB would overwrite, so the step failed with "ILASM failed with exit code 1" in the managedilasmroundtrip scenario. Assemble to IL-RT/<name>.pdbcheck.dll instead and hash both that image and the PDB that ilasm writes beside it, IL-RT/<name>.pdbcheck.pdb, and fail the step if either differs between the two runs. Native and managed ilasm both write that pair, so the step checks the -DEBUG image and the PDB for both again. The step also no longer overwrites the test's own <name>.pdb, the C# compiler's PDB, in the test directory. The rest of the round trip is unchanged.
… path-based type in ilasm OutputWriter moves to the ILAssembler library and works on streams only: it writes and closes the image, then writes the PDB, or, when no PDB was produced, deletes the existing PDB only if the replaced image's CodeView entry names its id. It refuses to write a PDB over the output. The caller supplies the output and the PDB through the new IOutputStreams interface, so every decision can be tested with in-memory streams. OutputFileWriter in the ilasm tool backs those streams with files: it resolves <output>.pdb, opens the existing image and PDB, creates the output, writes the PDB to a temporary file in the same directory and renames it into place, and deletes a stale PDB. Program calls it. The behaviour of the tool is unchanged. The tests that need no file system now run on streams in OutputWriterTests; OutputFileWriterTests keeps the path, temporary file, rename, deletion and same-file tests, and the seeded output-writer theories run against OutputFileWriter.
The end-to-end PDB tests that ran the managed ilasm as a process from src/tests/ilasm now run the command line in process in ILAssembler.Tests (CommandLinePdbFileTests): Program.cs is linked into the test project, as the command-line parsing already is, and each test assembles inline IL sources into its own temporary directory. They cover the PDB beside the output and its CodeView entry, the debug directory order, the checksum, no PDB without a switch (with and without .line and -DET), the DebuggableAttribute, stale-PDB replacement and deletion, an unrelated PDB kept, failed builds, -ERR, the refusal of an output named like its PDB, and byte-identical -DET repeats. The rows that ran native ilasm as a control are dropped; native ilasm is not part of ILAssembler.Tests. src/tests/ilasm is back to what it was before this change.
… PDB ids The IOutputStreams remarks now say exactly when OutputWriter.Write calls each member: OpenExistingOutput only when no PDB is produced and the PDB path is not the output path; CreateOutput unless the PDB would overwrite the output; WritePdb when a PDB is produced; otherwise OpenExistingPdb only when the replaced output yielded a PDB id, and TryDeletePdb only when that id equals the existing PDB's id. The stream test that keeps a PDB the replaced image does not reference again asserts its precondition, which the file-based test had before the split: the replaced image's CodeView entry and the PDB at the PDB path carry different ids. The ids are read with PEReader and MetadataReaderProvider rather than the writer's own readers. The two pairs differ by construction: they are compiled deterministically from different sources, and a deterministic PDB id is a hash of the PDB.
…ests that cannot run as skipped Review feedback on the output file writer: - IsOutputPath compared the output and PDB paths as strings, so an output that is a symbolic link to <output>.pdb was not recognized: the image was written through the link into the PDB's file, the PDB was then renamed over it, and the image was lost although the run reported success. IsOutputPath now also follows the chain of symbolic links that starts at the output path (relative targets against the link's directory, dangling targets included, at most 40 links) and compares each target with the PDB path, so such an output gets the existing "would overwrite the output file" refusal. A missing path counts as no link; any other failure to read a link propagates, so an output that cannot be inspected is not written. A link at the PDB path and a hard link between the two names need no check, because the PDB is renamed into place rather than written through either name; tests pin both. - The rename test returned early on Windows under a plain [Fact], so it was reported as passed there without running. It now uses [ConditionalFact] and is reported as skipped on Windows. The new symbolic-link tests are conditioned on whether the process can create a symbolic link, so they run on Windows where that is permitted and are reported as skipped otherwise; the hard-link test runs everywhere. The test project references Microsoft.DotNet.XUnitExtensions for the attribute, as the coreclr tool test projects do.
The review asked that a deterministic build not record the PDB's full path, in the way the native linker's /PDBALTPATH:%_PDB% does, and that a non-deterministic build keep it. The image's CodeView entry now names the PDB's file name and extension (Min.pdb) with --deterministic, and its full path otherwise. The PDB file is still written beside the output in both modes. Native ilasm records the full path in both modes, so the deterministic rule is a deliberate difference from it. So a deterministic image no longer depends on the directory it is written to: the same input assembled to two directories gives byte-identical images, as it already gave byte-identical PDBs. The command line makes the choice, because it is the one place that knows the PDB's full path; the assembler records Options.PdbFilePath as given. New command-line tests pin, under -DET: the file name with -DEBUG and -PDB; the file name for a relative output path (reported as skipped where no relative path reaches the temporary directory); that System.Reflection.Metadata finds the PDB beside the image from that name, with the matching id; and identical images and PDBs across two output directories. The existing full-path test now states that it covers builds without -DET. The README and the doc comments state what is recorded in each mode.
The README is for build instructions, not for the tool's features, so this restores it to main's text. The behaviour it described is stated in the doc comments of the options, which are unchanged.
8d4f4e2 to
09d4a59
Compare


In #135212, @jkotas pointed out that native ilasm is being replaced by the C# rewrite in
src/tools/ilasmand suggested fixing issues there. I listed three PDB differences between the managed and native ilasm, and @jkoritzinsky confirmed that they are unintended and should be fixed in managed ilasm. This PR fixes them.Changes
-DEBUG/-PDBEmbeddedPortablePdbentry)<output>.pdbbeside the output, not embedded, as native ilasm.linewithout-DEBUGor-PDB.lineis still parsed and validatedassembly.pdb-DETonly its file name (Min.pdb), a deliberate difference that follows the linker's/PDBALTPATH:%_PDB%PdbChecksum, then, with-DET,Reproducible: the same entry types in the same order as native ilasm.PortablePdbBuilderid provider computes, so under-DETthe PDB id and the checksum come from one hash.-DET.ManagedPEBuilderadds a Reproducible-only directory to a deterministic image when it is given none. A smallILAssemblerPEBuildersubclass, which the vtfixup/export builder also derives from, clears a debug directory with no entries.CompilationResult.PortablePdbreturns the PDB bytes, the newOptions.PdbFilePathsupplies the CodeView path, and the CLI writes the file.PdbFilePathget the output file name with a.pdbextension, orassembly.pdb.OutputWriterinILAssemblerworks on streams and makes the decisions below, andOutputFileWriterinilasmsupplies the files:<output>.pdb, so<output>.pdbnever holds a partial PDB. If the PDB cannot be written, the run fails with the new image beside the previous PDB, and deleting the temporary file is attempted.<output>.pdbonly if it belongs to the image that was replaced, that is, if the previous image's CodeView entry names that PDB's id. Any other file is kept, including when the previous output is not a PE image it can read (a COFF object file or a file over 2 GB, for example), and an output that is itself named like its PDB is never deleted. A PDB that cannot be deleted is left in place without failing the run. When assembly fails and produces no output, it writes and deletes nothing; with-ERR, an image produced despite errors is written and its PDB is handled as for a successful assembly.<output>.PDBwhenever it writes no PDB, including when assembly fails. I dropped that deliberately, because it can remove a PDB that ilasm did not write or that belongs to an image that was not replaced.<output>.PDB, so it has not been removing.pdbfiles there.-OUTPUT=Min.pdb -DEBUG), it reports an error and writes nothing, where native ilasm overwrites the image with the PDB. The comparison ignores case on Windows and macOS.--debug-modealone produces a PDB, as it already implied--debugfor theDebuggableAttribute.-PDBstill adds noDebuggableAttributeof its own.DebugMode.Impldocs said it enables Edit and Continue. IMPL is 0x103,Default | IgnoreSymbolStoreSequencePoints | DisableOptimizations, and the docs now say so.--debug-modehelp says what the modes do to the JIT.Round-trip harness (needed for this PR to merge).
src/tests/Common/CLRTest.Jit.targetschecked PDB determinism by assembling with-DEBUG -output=<name>.pdband hashing that file. That works with native ilasm only because the PDB overwrites the image.managedilasmroundtriplane would fail.IL-RT/<name>.pdbcheck.dlland hashes both that image and the.pdbbeside it, which both ilasms write. The previous managed run had also checked the-DEBUGimage, by hashing it with the PDB embedded.-DETfails the image comparison, and with the image comparison removed fails the PDB comparison, as it should. I could not run the lanes themselves. In the round-trip lane, reassembling a non-debug test assembly without-DEBUGnow removes the compiler's<name>.pdb, which belongs to the replaced image; the reassembled image has no debug directory, so nothing reads it, and the previous step overwrote that file anyway.Determinism. With
-DETthe CodeView entry records only the PDB's file name, so the same input assembled to outputs of the same name in different directories gives byte-identical images and PDBs (native ilasm records the full path in both modes; this follows the linker's/PDBALTPATH:%_PDB%convention instead).Remaining differences from native ilasm:
DebugDirectoryBuilder), the PDB stamp in native. The managed entry is new in this PR.0x04030201in managed, the clock in native.-DETPDB id: the PDB content hash in managed, the metadata hash in native.-OUTPUT=a.b/Min: native writesa.pdbin the parent directory; managed writesa.b/Min.pdb. That naming is deliberate.-DET: the PDB's file name in managed, its full path in native. That is deliberate (see Determinism above).<output>.PDBdeletion..ilsource as a document.DebuggableAttributeonAssemblyAttributesGoHere; managed emits it only with.assembly.Testing (Linux x64)
ILAssembler.Tests: 1216 before; 2124 at commit 1, 2332 at commits 2 and 3, 2344 at commit 4, 2366 at commits 5 and 6, 2374 at commit 7, and 2379 at commit 8, all passing on Linux; on Windows two file-system tests skip, five symbolic-link tests skip unless the process can create symbolic links, and the relative-output test skips if the temporary directory is on another drive than the working directory.Unit tests read the PDB from the compilation result. They pin:
-DET;.linealone;-PDBwithout aDebuggableAttribute;OutputWriterTeststests the output writer on in-memory streams, without the file system. It covers:OutputFileWriterTestscovers the file side: the PDB path, the temporary file and rename, deletion, and the case rule.Seeded theories check the same rules over generated programs and option combinations (800 cases). They also check the file writer over an enumeration of output names and pre-existing output and PDB states (270 cases). Each case is named by its index and description.
CommandLinePdbFileTestsruns the command line in process on IL sources in a temporary directory (27 cases). It covers:-DET, a CodeView entry with only the PDB's file name, for absolute and relative output paths, and the PDB found beside the image from that name;.lineand with-DET;-PDBwithout aDebuggableAttribute;-ERRwith a recoverable error;-DETrepeats, and byte-identical-DETimages and PDBs across two output directories.The cases that ran native ilasm as a control were dropped when these tests moved out of
src/tests/ilasm.I ran 59 selected mutations, one at a time: 19 in the compiler (checksum, entry order, gating), 32 in the two output writers and 8 in the command line. 58 were detected. The survivor removes a file-existence check before opening a file; it is equivalent for stable files, because a missing file raises an
IOExceptionthat the library counts as no id.-DETrepeats, and stale-PDB handling.src/tests/ilasm/PortablePdbagainst managed ilasm: 0/10 before, 4/10 after.TestPortablePdbDebugDirectorycases now pass.TestDocuments1), and the.ilsource as a document (TestPortablePdbDocuments). Fixes for those are in progress separately and will follow as their own PRs.Not run: Windows, macOS, Arm64, the round-trip lanes, or the full
src/testssuite. The ilasm pipeline runs on this PR and covers the round-trip lanes on eight platforms andILAssembler.Testson Windows x64.Questions:
ImmutableArray<byte>? CompilationResult.PortablePdbthe shape you want, orImmutableArray<byte>withIsDefault?System.Reflection.Metadatafor "no debug directory" than theILAssemblerPEBuildersubclass?ManagedPEBuilderhas no public way to decline its default Reproducible entry.-DET, and its full path otherwise.<output>.pdbonly when it belongs to the replaced image the behaviour you want?--debug-modehelp wording right?Note
This change was prepared with AI assistance (Anthropic Claude and OpenAI Codex) under my direction. AI agents wrote the code and ran the build and tests on my machine. I reviewed the change before posting.