diff --git a/changelog/dmd.newpdb.dd b/changelog/dmd.newpdb.dd new file mode 100644 index 000000000000..e2dd031d93df --- /dev/null +++ b/changelog/dmd.newpdb.dd @@ -0,0 +1,19 @@ +Improved CodeView / PDB symbolic debug info on Windows + +The Windows CodeView / PDB debug information emitter (used with `-g` when producing MS-COFF object files) now emits additional modern records expected by Microsoft and LLVM debugging tools. These improvements are enabled by default, so debugging with Visual Studio, WinDbg and LLVM-based tools works better out of the box. + +The following are now generated: + +$(LIST + * `S_OBJNAME` and `S_COMPILE3` compiland records reporting the configured language and the real compiler version string, + * `S_ENVBLOCK` and `S_BUILDINFO` (`LF_BUILDINFO`) records describing the build environment, + * `S_FRAMEPROC` records describing each function's stack frame, + * `LF_UDT_SRC_LINE` records recording the source file and line where each user-defined type (struct, class, enum) is defined, so debuggers can jump to a type's definition, + * blake3 source-file checksums in the file-checksums subsection so debuggers can verify the source matches. +) + +No action is required; simply compile with `-g` as before: + +```console +dmd -g myapp.d +``` diff --git a/compiler/src/dmd/backend/backconfig.d b/compiler/src/dmd/backend/backconfig.d index 97fc547483a8..aaa23d4b4173 100644 --- a/compiler/src/dmd/backend/backconfig.d +++ b/compiler/src/dmd/backend/backconfig.d @@ -17,7 +17,8 @@ import core.stdc.stdio; import dmd.backend.cdef; import dmd.backend.cc; import dmd.backend.code; -import dmd.backend.global : ErrorCallbackBackend, error, errorCallbackBackend; +import dmd.backend.global : ErrorCallbackBackend, error, errorCallbackBackend, + GetFileContentsCallback, getFileContentsCallback; import dmd.backend.go : go_flag, GlobalOptimizer; import dmd.backend.rtlsym : rtlsym_init; import dmd.backend.ty; @@ -87,11 +88,13 @@ void out_config_init( bool generatedMain, // a main entrypoint is generated bool dataimports, ref GlobalOptimizer go, - ErrorCallbackBackend errorCallback) + ErrorCallbackBackend errorCallback, + GetFileContentsCallback getFileContents) { //printf("out_config_init()\n"); errorCallbackBackend = errorCallback; + getFileContentsCallback = getFileContents; auto cfg = &config; cfg._version = _version; diff --git a/compiler/src/dmd/backend/cv8.d b/compiler/src/dmd/backend/cv8.d index d56aaf635f22..78b9b9769481 100644 --- a/compiler/src/dmd/backend/cv8.d +++ b/compiler/src/dmd/backend/cv8.d @@ -33,12 +33,14 @@ import dmd.backend.el; import dmd.backend.mscoffobj; import dmd.backend.obj; import dmd.backend.oper; +import dmd.backend.global : getFileContentsCallback; import dmd.common.outbuffer; import dmd.backend.rtlsym; import dmd.backend.symbol : globsym; import dmd.backend.ty; import dmd.backend.type; import dmd.backend.dvarstats; +import dmd.root.filename : FileName; nothrow: @@ -62,6 +64,70 @@ private bool symbol_iscomdat4(Symbol* s) // 'fatal error LNK1318: Unexpected PDB error; RPC (23) '(0x000006BA)' enum CV8_MAX_SYMBOL_LENGTH = 0xffd8; +// Modern CodeView symbol records not declared in cv4.d +enum +{ + S_OBJNAME_V3 = 0x1101, // object file name (same value as S_COMPILAND_V3) + S_COMPILE3 = 0x113C, // compile flags, language and compiler version + S_ENVBLOCK = 0x113D, // build environment block + S_BUILDINFO = 0x114C, // reference to an LF_BUILDINFO record + S_FRAMEPROC = 0x1012, // extra frame and procedure information + S_REGREL32 = 0x1111, // register-relative address + S_LTHREAD32 = 0x1112, // thread-local static data (local) + S_GTHREAD32 = 0x1113, // thread-local static data (global) + S_LPROC32_ID = 0x1146, // local procedure (type field is an LF_FUNC_ID) + S_GPROC32_ID = 0x1147, // global procedure (type field is an LF_FUNC_ID) +} + +// Type leaf records used by the ID stream +enum +{ + LF_FUNC_ID = 0x1601, // function id: function type + name + LF_STRING_ID = 0x1605, // interned string -> type index + LF_BUILDINFO = 0x1603, // cwd, tool, source, pdb, args + LF_UDT_SRC_LINE = 0x1606, // source file/line where a UDT is defined +} + +// .debug$S subsection kinds +enum +{ + DEBUG_S_SYMBOLS = 0xF1, + DEBUG_S_LINES = 0xF2, + DEBUG_S_STRINGTABLE = 0xF3, + DEBUG_S_FILECHKSMS = 0xF4, +} + +// Source file checksum kinds +enum +{ + CHKSUM_NONE = 0, + CHKSUM_MD5 = 1, + CHKSUM_SHA1 = 2, + CHKSUM_SHA256 = 3, +} + +// S_FRAMEPROC flags +enum +{ + CV_FRAME_HASALLOCA = 1 << 0, // function uses alloca() + CV_FRAME_HASINLASM = 1 << 3, // function has inline asm + CV_FRAME_HASEH = 1 << 4, // function has exception handling + CV_FRAME_SECURITY = 1 << 8, // function has a stack security cookie + CV_FRAME_OPTSPEED = 1 << 20, // function was optimized for speed + CV_FRAME_LOCALBP_RBP = 2 << 14, // locals addressed relative to RBP/EBP + CV_FRAME_PARAMBP_RBP = 2 << 16, // parameters addressed relative to RBP/EBP +} + +// Mark a line-number entry as a statement (not an expression) +enum CV_LINE_STATEMENT = 0x80000000; + +// CodeView register encodings for base-pointer-relative records +enum +{ + CV_REG_EBP = 22, // x86 EBP + CV_AMD64_RBP = 334, // x64 RBP +} + // The "F1" section, which is the symbols private __gshared OutBuffer* F1_buf; @@ -225,26 +291,64 @@ void cv8_termfile(const(char)[] objfilename) uint value = 4; objmod.bytes(seg,0,(cast(void*)&value)[0 .. 4]); - /* Start with starting symbol in separate "F1" section + /* Start with starting symbols in a separate "F1" section */ auto buf = OutBuffer(1024); - size_t len = objfilename.length; - buf.write16(cast(int)(2 + 4 + len + 1)); - buf.write16(S_COMPILAND_V3); - buf.write32(0); - buf.write(objfilename.ptr, cast(uint)(len + 1)); - // write S_COMPILE record - buf.write16(2 + 1 + 1 + 2 + 1 + VERSION.length + 1); - buf.write16(S_COMPILE); - buf.writeByte(I64 ? 0xD0 : 6); // target machine AMD64 or x86 (Pentium II) - buf.writeByte(config.flags2 & CFG2gms ? 0 : 'D'); // language index (C/C++/D) - buf.write16(0x800 | (config.inline8087 ? 0 : (1<<3))); // 32-bit, float package - buf.writeByte(VERSION.length + 1); - buf.writeByte('Z'); - buf.write(VERSION.ptr, VERSION.length); + // S_OBJNAME record + { + size_t len = objfilename.length; + buf.write16(cast(int)(2 + 4 + len + 1)); + buf.write16(S_OBJNAME_V3); + buf.write32(0); // signature + buf.write(objfilename.ptr, cast(uint)(len + 1)); + } - cv8_writesection(seg, 0xF1, &buf); + // S_COMPILE3 record: honor the configured language and report the real + // compiler version string (works for dmd/ldc/gdc) + { + const uint flags = config.flags2 & CFG2gms ? 0 : 'D'; // iLanguage in low byte + const(char)[] ver = config._version; + buf.write16(cast(int)(2 + 4 + 2 + 8 + 8 + ver.length + 1)); + buf.write16(S_COMPILE3); + buf.write32(flags); + buf.write16(I64 ? 0xD0 : 0x06); // machine: AMD64 / x86 + buf.write16(0); buf.write16(0); buf.write16(0); buf.write16(0); // front end version + buf.write16(0); buf.write16(0); buf.write16(0); buf.write16(0); // back end version + buf.write(ver.ptr, cast(uint)ver.length); + buf.writeByte(0); + } + + cv8_writesection(seg, DEBUG_S_SYMBOLS, &buf); + + // S_ENVBLOCK record: build environment (cwd), double-null terminated + { + auto ebuf = OutBuffer(128); + char[260] cwd = 0; + if (!getcwd(cwd.ptr, cwd.sizeof)) + cwd[0] = 0; + size_t cwdlen = strlen(cwd.ptr); + uint rec = cast(uint)(2 + 1 + 4 + cwdlen + 1 + 4 + 1 + 1); + ebuf.write16(cast(int)rec); + ebuf.write16(S_ENVBLOCK); + ebuf.writeByte(0); // flags + ebuf.write("cwd\0".ptr, 4); + ebuf.write(cwd.ptr, cast(uint)(cwdlen + 1)); + ebuf.write("cmd\0".ptr, 4); + ebuf.writeByte(0); // empty cmd value + ebuf.writeByte(0); // terminating double-null + cv8_writesection(seg, DEBUG_S_SYMBOLS, &ebuf); + } + + // S_BUILDINFO record referencing an LF_BUILDINFO id record + { + auto bbuf = OutBuffer(64); + idx_t biidx = cv8_buildinfo(); + bbuf.write16(2 + 4); + bbuf.write16(S_BUILDINFO); + bbuf.write32(biidx); + cv8_writesection(seg, DEBUG_S_SYMBOLS, &bbuf); + } // Write out "F2" sections uint length = cast(uint)funcdata.length(); @@ -267,14 +371,14 @@ void cv8_termfile(const(char)[] objfilename) } uint offset = cast(uint)SegData[f2seg].SDoffset + 8; - cv8_writesection(f2seg, 0xF2, F2_buf); + cv8_writesection(f2seg, DEBUG_S_LINES, F2_buf); objmod.reftoident(f2seg, offset, fd.sfunc, 0, CF.seg | CF.off); if (f2seg != seg && fd.f1buf.length()) { // Write out "F1" section const uint f1offset = cast(uint)SegData[f2seg].SDoffset; - cv8_writesection(f2seg, 0xF1, fd.f1buf); + cv8_writesection(f2seg, DEBUG_S_SYMBOLS, fd.f1buf); // Fixups for "F1" section const uint fixupLength = cast(uint)fd.f1fixup.length(); @@ -289,17 +393,17 @@ void cv8_termfile(const(char)[] objfilename) // Write out "F3" section if (F3_buf.length() > 1) - cv8_writesection(seg, 0xF3, F3_buf); + cv8_writesection(seg, DEBUG_S_STRINGTABLE, F3_buf); // Write out "F4" section if (F4_buf.length() > 0) - cv8_writesection(seg, 0xF4, F4_buf); + cv8_writesection(seg, DEBUG_S_FILECHKSMS, F4_buf); if (F1_buf.length()) { // Write out "F1" section uint f1offset = cast(uint)SegData[seg].SDoffset; - cv8_writesection(seg, 0xF1, F1_buf); + cv8_writesection(seg, DEBUG_S_SYMBOLS, F1_buf); // Fixups for "F1" section length = cast(uint)F1fixup.length(); @@ -402,19 +506,22 @@ void cv8_func_term(Symbol* sfunc) const(char)* id = sfunc.prettyIdent ? sfunc.prettyIdent : prettyident(sfunc); + // ID-stream record tying the function's type to its name (S_GPROC32_ID references this) + idx_t funcid = cv8_func_id(id, typidx); + size_t len = strlen(id); if(len > CV8_MAX_SYMBOL_LENGTH) len = CV8_MAX_SYMBOL_LENGTH; /* * 2 length (not including these 2 bytes) - * 2 S_GPROC_V3 + * 2 S_GPROC32_ID / S_LPROC32_ID * 4 parent * 4 pend * 4 pnext * 4 size of function * 4 size of function prolog * 4 offset to function epilog - * 4 type index + * 4 LF_FUNC_ID (ID stream) * 6 seg:offset of function start * 1 flags * n 0 terminated name string @@ -422,14 +529,14 @@ void cv8_func_term(Symbol* sfunc) auto buf = currentfuncdata.f1buf; buf.reserve(cast(uint)(2 + 2 + 4 * 7 + 6 + 1 + len + 1)); buf.write16n(cast(int)(2 + 4 * 7 + 6 + 1 + len + 1)); - buf.write16n(sfunc.Sclass == SC.static_ ? S_LPROC_V3 : S_GPROC_V3); + buf.write16n(sfunc.Sclass == SC.static_ ? S_LPROC32_ID : S_GPROC32_ID); buf.write32(0); // parent buf.write32(0); // pend buf.write32(0); // pnext buf.write32(cast(uint)currentfuncdata.section_length); // size of function buf.write32(cast(uint)cgstate.startoffset); // size of prolog buf.write32(cast(uint)cgstate.retoffset); // offset to epilog - buf.write32(typidx); + buf.write32(funcid); // LF_FUNC_ID (ID stream), resolved to the function type F1_Fixups f1f; f1f.s = sfunc; @@ -443,6 +550,33 @@ void cv8_func_term(Symbol* sfunc) buf.writen(id, len); buf.writeByte(0); + // S_FRAMEPROC: describe this function's stack frame + { + // Locals and parameters are addressed relative to the frame pointer + // (RBP/EBP), matching the S_REGREL32 records emitted for them. + uint frameflags = CV_FRAME_LOCALBP_RBP | CV_FRAME_PARAMBP_RBP; + if (cgstate.Alloca.size) + frameflags |= CV_FRAME_HASALLOCA; + if (cgstate.anyiasm) + frameflags |= CV_FRAME_HASINLASM; + if (cgstate.usednteh) + frameflags |= CV_FRAME_HASEH; + if (config.flags2 & CFG2stomp) + frameflags |= CV_FRAME_SECURITY; + if (config.flags4 & CFG4speed) + frameflags |= CV_FRAME_OPTSPEED; + + buf.write16(2 + 4 + 4 + 4 + 4 + 4 + 2 + 4); + buf.write16(S_FRAMEPROC); + buf.write32(cast(uint)localsize); // cbFrame: total frame size + buf.write32(0); // cbPad + buf.write32(0); // offPad + buf.write32(0); // cbSaveRegs + buf.write32(0); // offExHdlr + buf.write16(0); // sectExHdlr + buf.write32(frameflags); // flags + } + struct cv8 { nothrow: @@ -538,7 +672,7 @@ void cv8_linnum(Srcpos srcpos, uint offset) lastoffset = offset; lastlinnum = srcpos.Slinnum; linepair.write32(offset); - linepair.write32(srcpos.Slinnum | 0x80000000); // mark as statement, not expression + linepair.write32(srcpos.Slinnum | CV_LINE_STATEMENT); // mark as statement, not expression currentfuncdata.linepairbytes += 8; @@ -564,48 +698,23 @@ uint cv8_addfile(const(char)* filename) * Unlike C, there won't be lots of .h source files to be accounted for. */ + // The file names are stored as absolute paths so the debugger can find the + // source without knowing the compilation directory. + const(char)* absname = FileName.toAbsolute(filename); + size_t len = strlen(absname); + uint length = cast(uint)F3_buf.length(); ubyte* p = F3_buf.buf; - size_t len = strlen(filename); - // ensure the filename is absolute to help the debugger to find the source - // without having to know the working directory during compilation - __gshared char[260] cwd = 0; - __gshared uint cwdlen; - bool abs = (*filename == '\\') || - (*filename == '/') || - (*filename && filename[1] == ':'); - - if (!abs && cwd[0] == 0) - { - if (getcwd(cwd.ptr, cwd.sizeof)) - { - cwdlen = cast(uint)strlen(cwd.ptr); - if(cwd[cwdlen - 1] != '\\' && cwd[cwdlen - 1] != '/') - cwd[cwdlen++] = '\\'; - } - } uint off = 1; while (off + len < length) { - if (!abs) - { - if (memcmp(p + off, cwd.ptr, cwdlen) == 0 && - memcmp(p + off + cwdlen, filename, len + 1) == 0) - goto L1; - } - else if (memcmp(p + off, filename, len + 1) == 0) - { // Already there - //printf("\talready there at %x\n", off); - goto L1; - } + if (memcmp(p + off, absname, len + 1) == 0) + goto L1; // already there off += strlen(cast(const(char)* )(p + off)) + 1; } off = length; - // Add it - if(!abs) - F3_buf.write(cwd.ptr, cwdlen); - F3_buf.write(filename, cast(uint)(len + 1)); + F3_buf.write(absname, cast(uint)(len + 1)); // add it L1: // off is the offset of the filename in F3. @@ -615,33 +724,34 @@ L1: p = F4_buf.buf; uint u = 0; - while (u + 8 <= length) + while (u + 6 <= length) { - //printf("\t%x\n", *cast(uint*)(p + u)); if (off == *cast(uint*)(p + u)) - { - //printf("\tfound %x\n", u); return u; - } - u += 4; - ushort type = *cast(ushort*)(p + u); - u += 2; - if (type == 0x0110) - u += 16; // truncated blake3 hash - u += 2; + u += 4; // file name offset + ubyte cklen = *(p + u); // checksum size + u += 2; // skip size + kind bytes + u += cklen; // skip checksum bytes + u = (u + 3) & ~3; // realign to 4 } - // Not there. Add it. + // Not present; append a checksum computed over the source file's content + // so debuggers can verify the source matches. F4_buf.write32(off); - - /* Write 10 01 [blake3 hash] - * or - * 00 00 - */ - F4_buf.write16(0); - - // 2 bytes of pad - F4_buf.write16(0); + ubyte[32] hash = void; + if (cv8_filehash(filename, hash)) + { + F4_buf.writeByte(32); // checksum size + F4_buf.writeByte(CHKSUM_SHA256); // 32-byte checksum slot + F4_buf.write(hash.ptr, 32); + } + else + { + F4_buf.writeByte(0); // no checksum available + F4_buf.writeByte(CHKSUM_NONE); + } + while (F4_buf.length() & 3) + F4_buf.writeByte(0); //printf("\tadded %x\n", length); return length; @@ -719,10 +829,10 @@ static if (1) // Register relative addressing buf.reserve(cast(uint)(2 + 2 + 4 + 4 + 2 + len + 1)); buf.write16n(cast(uint)(2 + 4 + 4 + 2 + len + 1)); - buf.write16n(0x1111); + buf.write16n(S_REGREL32); buf.write32(cast(uint)(s.Soffset + base + cgstate.BPoff)); buf.write32(typidx); - buf.write16n(I64 ? 334 : 22); // relative to RBP/EBP + buf.write16n(I64 ? CV_AMD64_RBP : CV_REG_EBP); // relative to RBP/EBP cv8_writename(buf, id, len); buf.writeByte(0); } @@ -788,7 +898,7 @@ else * n 0 terminated name string */ if (s.ty() & mTYthread) // thread local storage - sr = (sr == S_GDATA_V3) ? 0x1113 : 0x1112; + sr = (sr == S_GDATA_V3) ? S_GTHREAD32 : S_LTHREAD32; buf.reserve(cast(uint)(2 + 2 + 4 + 6 + len + 1)); buf.write16n(cast(uint)(2 + 4 + 6 + len + 1)); @@ -797,7 +907,7 @@ else f1f.s = s; f1f.offset = cast(uint)buf.length(); - F1fixup.write(&f1f, f1f.sizeof); + currentfuncdata.f1fixup.write(&f1f, f1f.sizeof); buf.write32(0); buf.write16n(0); @@ -1198,4 +1308,112 @@ else return cv_debtyp(d); } +/* Compute a blake3 hash over the *content* of the named source file, so that + * debuggers can verify the source matches. The hash goes into the 32-byte + * SHA256 checksum slot of the file-checksums (F4) subsection. + * The content is fetched from the front-end FileManager cache (Module.src) + * rather than re-read from disk, which would add significant I/O to every build. + * Returns: true on success (hash filled in), false if the content is unavailable. + */ +private @trusted +bool cv8_filehash(const(char)* filename, ref ubyte[32] hash) +{ + if (!getFileContentsCallback) + return false; + + size_t length; + const(ubyte)* data = getFileContentsCallback(filename, length); + if (!data) + return false; + + import dmd.common.blake3; + hash = blake3(data[0 .. length]); + return true; +} + +/* LF_STRING_ID: an interned string returning a type index. */ +@trusted +idx_t cv8_string_id(const(char)* s) +{ + if (!s) s = ""; + int idlen = cast(int)strlen(s); + if (idlen > CV8_MAX_SYMBOL_LENGTH) // CodeView records have a 16-bit length field + idlen = CV8_MAX_SYMBOL_LENGTH; + debtyp_t* d = debtyp_alloc(2 + 4 + idlen + 1); + TOWORD(d.data.ptr, LF_STRING_ID); + TOLONG(d.data.ptr + 2, 0); // substring list id + cv_namestring(d.data.ptr + 6, s, idlen); + d.data.ptr[6 + idlen] = 0; + return cv_debtyp(d); +} + +/* LF_BUILDINFO: cwd, build tool, source, pdb, args. */ +@trusted +idx_t cv8_buildinfo() +{ + char[260] cwd = 0; + if (!getcwd(cwd.ptr, cwd.sizeof)) + cwd[0] = 0; + + // build tool: identify the compiler and its version + char[64] tool = 0; + strcpy(tool.ptr, "dmd "); + const(char)[] ver = config._version; + size_t vn = ver.length; + if (vn > tool.sizeof - 5) // leave room for the "dmd " prefix and the null terminator + vn = tool.sizeof - 5; + memcpy(tool.ptr + 4, ver.ptr, vn); + tool[4 + vn] = 0; + + idx_t cwdId = cv8_string_id(cwd.ptr); + idx_t toolId = cv8_string_id(tool.ptr); + idx_t srcId = cv8_string_id(""); + idx_t pdbId = cv8_string_id(""); + idx_t argId = cv8_string_id(""); + debtyp_t* d = debtyp_alloc(2 + 2 + 5 * 4); + TOWORD(d.data.ptr, LF_BUILDINFO); + TOWORD(d.data.ptr + 2, 5); // count + TOLONG(d.data.ptr + 4, cwdId); + TOLONG(d.data.ptr + 8, toolId); + TOLONG(d.data.ptr + 12, srcId); + TOLONG(d.data.ptr + 16, pdbId); + TOLONG(d.data.ptr + 20, argId); + return cv_debtyp(d); +} + +/* LF_FUNC_ID: ties a function type to its name in the ID stream. */ +@trusted +idx_t cv8_func_id(const(char)* id, idx_t functype) +{ + int idlen = cast(int)strlen(id); + if (idlen > CV8_MAX_SYMBOL_LENGTH) // CodeView records have a 16-bit length field + idlen = CV8_MAX_SYMBOL_LENGTH; + debtyp_t* d = debtyp_alloc(2 + 4 + 4 + idlen + 1); + TOWORD(d.data.ptr, LF_FUNC_ID); + TOLONG(d.data.ptr + 2, 0); // parent scope id + TOLONG(d.data.ptr + 6, functype); + cv_namestring(d.data.ptr + 10, id, idlen); + d.data.ptr[10 + idlen] = 0; + return cv_debtyp(d); +} + +/* LF_UDT_SRC_LINE: source file and line where a user-defined type is defined. */ +@trusted +idx_t cv8_udt_src_line(idx_t typidx, const(char)* filename, uint line) +{ + /* The source file must be spelled the same way as in the module's source + * file list (see cv8_addfile), i.e. as an absolute path, otherwise the + * debugger cannot associate this record with a known source file. + */ + const(char)* srcpath = FileName.toAbsolute(filename); + + idx_t srcId = cv8_string_id(srcpath); + debtyp_t* d = debtyp_alloc(2 + 4 + 4 + 4); + TOWORD(d.data.ptr, LF_UDT_SRC_LINE); + TOLONG(d.data.ptr + 2, typidx); // the user-defined type + TOLONG(d.data.ptr + 6, srcId); // LF_STRING_ID of the source file + TOLONG(d.data.ptr + 10, line); // line number of the definition + return cv_debtyp(d); +} + } diff --git a/compiler/src/dmd/backend/global.d b/compiler/src/dmd/backend/global.d index 9743eea960b6..4cdf0d337333 100644 --- a/compiler/src/dmd/backend/global.d +++ b/compiler/src/dmd/backend/global.d @@ -35,6 +35,13 @@ alias ErrorCallbackBackend = extern(C++) void function(const(char)* filename, ui package(dmd.backend) __gshared ErrorCallbackBackend errorCallbackBackend; +/// Callback to fetch cached source-file contents from the front-end FileManager +/// (Module.src), avoiding a re-read from disk. Returns a pointer to the bytes +/// and sets `length`; returns null if the content is unavailable. +alias GetFileContentsCallback = extern(C++) const(ubyte)* function(const(char)* filename, ref size_t length); + +package(dmd.backend) __gshared GetFileContentsCallback getFileContentsCallback; + /** * Backend error report function * Params: diff --git a/compiler/src/dmd/dmsc.d b/compiler/src/dmd/dmsc.d index 3f8b4b0a9913..ffa558006eaa 100644 --- a/compiler/src/dmd/dmsc.d +++ b/compiler/src/dmd/dmsc.d @@ -29,10 +29,27 @@ import dmd.backend.backconfig; import dmd.backend.go; import dmd.backend.cc; import dmd.backend.cdef; -import dmd.backend.global : ErrorCallbackBackend; +import dmd.backend.global : ErrorCallbackBackend, GetFileContentsCallback; import dmd.backend.ty; import dmd.backend.type; +import dmd.file_manager : FileManager; + +/// Callback for the backend to fetch cached source-file contents from the +/// front-end FileManager (Module.src), so hashing source files for debug info +/// reuses the in-memory cache instead of re-reading them from disk. +extern(C++) const(ubyte)* getFileContentsBackend(const(char)* filename, ref size_t length) +{ + length = 0; + if (!global.fileManager) + return null; + const(ubyte)[] data = global.fileManager.getFileContents(FileName(filename[0 .. strlen(filename)])); + if (!data) + return null; + length = data.length; + return data.ptr; +} + /************************************** * Initialize backend config variables. * Params: @@ -98,6 +115,7 @@ void backend_init(const ref Param params, const ref DMDparams driverParams, cons go, // FIXME: casting to @nogc because errors.d is not marked @nogc yet cast(ErrorCallbackBackend) &errorBackend, + cast(GetFileContentsCallback) &getFileContentsBackend, ); out_config_debug( diff --git a/compiler/src/dmd/glue/tocvdebug.d b/compiler/src/dmd/glue/tocvdebug.d index 12c1f350db1e..21c7f6e4bb1a 100644 --- a/compiler/src/dmd/glue/tocvdebug.d +++ b/compiler/src/dmd/glue/tocvdebug.d @@ -278,13 +278,21 @@ uint cv_align(ubyte* p, uint n) /************************************* * write a UDT record to the object file * Params: + * s = user defined type symbol * id = name of user defined type * typidx = type index */ -void cv_udt(const char* id, uint typidx) +void cv_udt(Dsymbol s, const char* id, uint typidx) { if (config.fulltypes == CV8) - return cv8_udt(id, typidx); + { + cv8_udt(id, typidx); + // Record the source file and line where the type is defined + // (LF_UDT_SRC_LINE); the CV4 path has no equivalent record. + if (s) + cast(void)cv8_udt_src_line(typidx, s.loc.filename, s.loc.linnum); + return; + } const len = strlen(id); version (AArch64) // TODO AArch64 @@ -326,7 +334,7 @@ void toDebug(EnumDeclaration ed) { const id = ed.toPrettyChars(true); const idx_t typidx = cv4_Denum(ed); - cv_udt(id, typidx); + cv_udt(ed, id, typidx); } } @@ -604,7 +612,7 @@ void toDebug(StructDeclaration sd) // cv4_outsym(s); - cv_udt(id, typidx); + cv_udt(sd, id, typidx); // return typidx; } @@ -805,7 +813,7 @@ void toDebug(ClassDeclaration cd) // cv4_outsym(s); - cv_udt(id, typidx); + cv_udt(cd, id, typidx); // return typidx; } @@ -918,7 +926,7 @@ void toDebugClosure(Symbol* closstru) TOWORD(d.data.ptr + 6,property); } - cv_udt(closname, typidx); + cv_udt(null, closname, typidx); } /* ===================================================================== */ diff --git a/compiler/test/runnable/testpdb.d b/compiler/test/runnable/testpdb.d index a8c9706be508..a6d4734c8c4b 100644 --- a/compiler/test/runnable/testpdb.d +++ b/compiler/test/runnable/testpdb.d @@ -64,6 +64,12 @@ void main(string[] args) test21665(session, globals); + testSourceChecksums(session, globals); + testCompile3(globals); + testFrameProc(globals); + testFuncId(globals); + testUdtSrcLine(globals); + source.Release(); session.Release(); globals.Release(); @@ -706,6 +712,161 @@ void test21665(IDiaSession session, IDiaSymbol globals) checkBitField!("b", 3, 6); } +/////////////////////////////////////////////// +// Tests for the modern CodeView records emitted by the CV8 debug info +// generator: blake3 source-file checksums (DEBUG_S_FILECHKSMS), S_COMPILE3 +// (compiland language/version) and S_FRAMEPROC (per-function frame info). + +// Each source file now carries a checksum; the legacy emitter wrote none. +void testSourceChecksums(IDiaSession session, IDiaSymbol globals) +{ + // DIA checksum type; the emitter stores a blake3 hash in this 32-byte slot. + enum CV_CHKSUM_SHA_256 = 3; + + IDiaSymbol funcsym = searchSymbol(globals, "testpdb.test15432"); + funcsym || assert(false, "testpdb.test15432 not found"); + + DWORD rva; + funcsym.get_relativeVirtualAddress(&rva) == S_OK || assert(false, "test15432: no rva"); + ULONGLONG length; + funcsym.get_length(&length) == S_OK || assert(false, "test15432: no length"); + + IDiaEnumLineNumbers dialines; + (session.findLinesByRVA(rva, cast(DWORD)length, &dialines) == S_OK && dialines) + || assert(false, "test15432: no line numbers"); + scope(exit) dialines.Release(); + + IDiaLineNumber line; + ULONG fetched; + bool checkedAny = false; + while (dialines.Next(1, &line, &fetched) == S_OK && fetched == 1) + { + IDiaSourceFile src; + if (line.get_sourceFile(&src) == S_OK && src) + { + DWORD cktype; + src.get_checksumType(&cktype) == S_OK || assert(false, "source file has no checksum type"); + // The emitter stores a blake3 hash in the 32-byte SHA-256 checksum + // slot; the legacy emitter wrote CV_CHKSUM_NONE (0). + cktype == CV_CHKSUM_SHA_256 || assert(false, "source file checksum is not the SHA-256 (blake3) slot"); + checkedAny = true; + src.Release(); + } + line.Release(); + } + checkedAny || assert(false, "no source file checksum found"); +} + +// S_COMPILE3 exposes the compiland language and compiler version through +// SymTagCompilandDetails; the legacy S_COMPILE record did not. +void testCompile3(IDiaSymbol globals) +{ + IDiaEnumSymbols enumComps; + (globals.findChildren(SymTagEnum.SymTagCompiland, null, NameSearchOptions.nsNone, &enumComps) == S_OK && enumComps) + || assert(false, "no compilands"); + scope(exit) enumComps.Release(); + + IDiaSymbol compiland; + ULONG fetched; + bool foundD = false; + while (!foundD && enumComps.Next(1, &compiland, &fetched) == S_OK && fetched == 1) + { + IDiaEnumSymbols enumDetails; + if (compiland.findChildren(SymTagEnum.SymTagCompilandDetails, null, NameSearchOptions.nsNone, &enumDetails) == S_OK && enumDetails) + { + IDiaSymbol details; + ULONG f2; + if (enumDetails.Next(1, &details, &f2) == S_OK && f2 == 1 && details) + { + DWORD lang; + if (details.get_language(&lang) == S_OK && lang == 'D') + { + BSTR compilerName; + (details.get_compilerName(&compilerName) == S_OK && compilerName) + || assert(false, "compiland has no compiler name"); + wcslen(compilerName) > 0 || assert(false, "empty compiler name"); + SysFreeString(compilerName); + foundD = true; + } + details.Release(); + } + enumDetails.Release(); + } + compiland.Release(); + } + foundD || assert(false, "no D compiland details (S_COMPILE3) found"); +} + +// S_FRAMEPROC provides the stack frame size; without it get_frameSize fails. +void testFrameProc(IDiaSymbol globals) +{ + IDiaSymbol funcsym = searchSymbol(globals, "testpdb.sum21384"); + funcsym || assert(false, "testpdb.sum21384 not found"); + + DWORD frameSize; + funcsym.get_frameSize(&frameSize) == S_OK + || assert(false, "sum21384: no frame size (S_FRAMEPROC missing)"); +} + +// Functions are emitted as S_GPROC32_ID referencing an LF_FUNC_ID; DIA resolves +// the function's type through that id record. +void testFuncId(IDiaSymbol globals) +{ + IDiaSymbol funcsym = searchSymbol(globals, "testpdb.sum21384"); + funcsym || assert(false, "testpdb.sum21384 not found"); + + DWORD tag; + (funcsym.get_symTag(&tag) == S_OK && tag == SymTagEnum.SymTagFunction) + || assert(false, "sum21384: not a function symbol"); + + IDiaSymbol funcType; + (funcsym.get_type(&funcType) == S_OK && funcType) + || assert(false, "sum21384: no function type (LF_FUNC_ID chain broken)"); + (funcType.get_symTag(&tag) == S_OK && tag == SymTagEnum.SymTagFunctionType) + || assert(false, "sum21384: type is not a function type"); +} + +// LF_UDT_SRC_LINE records the source file/line where a user-defined type is defined. +enum lineUdtStruct = __LINE__ + 1; +struct UdtSrcLineStruct { int x; } + +enum lineUdtClass = __LINE__ + 1; +class UdtSrcLineClass { int y; } + +enum lineUdtEnum = __LINE__ + 1; +enum UdtSrcLineEnum { valueA, valueB } + +void testUdtSrcLine(IDiaSymbol globals) +{ + // Reference each type so that full debug info is emitted for it. + UdtSrcLineStruct s; + s.x = 0; + scope c = new UdtSrcLineClass; + c.y = 0; + UdtSrcLineEnum e = UdtSrcLineEnum.valueA; + assert(e == UdtSrcLineEnum.valueA); + + checkUdtSrcLine(globals, "testpdb.UdtSrcLineStruct"w, SymTagEnum.SymTagUDT, lineUdtStruct); + checkUdtSrcLine(globals, "testpdb.UdtSrcLineClass"w, SymTagEnum.SymTagUDT, lineUdtClass); + checkUdtSrcLine(globals, "testpdb.UdtSrcLineEnum"w, SymTagEnum.SymTagEnum, lineUdtEnum); +} + +void checkUdtSrcLine(IDiaSymbol globals, wstring name, SymTagEnum tag, uint expectedLine) +{ + IDiaSymbol udt = searchSymbol(globals, name.ptr, tag); + udt || assert(false, "testUdtSrcLine: type not found"); + scope(exit) udt.Release(); + + IDiaLineNumber lineNo; + (udt.getSrcLineOnTypeDefn(&lineNo) == S_OK && lineNo) + || assert(false, "testUdtSrcLine: no source line (LF_UDT_SRC_LINE missing)"); + scope(exit) lineNo.Release(); + + DWORD line; + lineNo.get_lineNumber(&line) == S_OK || assert(false, "testUdtSrcLine: no line number"); + line == expectedLine || assert(false, "testUdtSrcLine: wrong definition line"); +} + /////////////////////////////////////////////// import core.stdc.stdio; import core.stdc.wchar_;