| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| #include "vm/codeview.h" | |
| #if defined(DART_PRECOMPILER) | |
| #include "vm/code_descriptors.h" | |
| #include "vm/coff.h" | |
| #include "vm/datastream.h" | |
| #include "vm/debug_info.h" | |
| #include "vm/debug_info_stream.h" | |
| #include "vm/hash_map.h" | |
| #include "vm/image_snapshot.h" | |
| #include "vm/object.h" | |
| #include "vm/os.h" | |
| #include "vm/version.h" | |
| namespace dart { | |
| CodeViewBuilder::CodeViewBuilder(Zone* zone, | |
| CoffWriter* writer, | |
| DebugInfoStream* debug_s, | |
| DebugInfoStream* debug_t) | |
| : zone_(zone), | |
| writer_(writer), | |
| debugS_(ASSERT_NOTNULL(debug_s)), | |
| debugT_(ASSERT_NOTNULL(debug_t)), | |
| functions_(new (zone) ZoneGrowableArray<FunctionInfo>(zone, 64)), | |
| script_string_offsets_(new (zone) ZoneGrowableArray<uint32_t>(zone, 16)), | |
| script_checksum_offsets_(new (zone) | |
| ZoneGrowableArray<uint32_t>(zone, 16)) {} | |
| struct CVLineRow { | |
| uint32_t pc_offset; | |
| int32_t file_index; // index into the debug script table | |
| int32_t line; | |
| }; | |
| class CVLineNumberProgramWriter : public DebugInfoLineNumberProgramWriter { | |
| public: | |
| CVLineNumberProgramWriter(Zone* zone, | |
| CoffWriter* writer, | |
| intptr_t script_count, | |
| intptr_t code_size) | |
| : DebugInfoLineNumberProgramWriter(zone), | |
| rows_(zone, 64), | |
| writer_(ASSERT_NOTNULL(writer)), | |
| script_count_(script_count), | |
| code_size_(code_size) {} | |
| intptr_t LookupCodeLabel(const Code& code) override { | |
| return writer_->LookupDebugCodeLabel(code); | |
| } | |
| intptr_t LookupScript(const Script& script) override { | |
| return writer_->LookupDebugScript(script); | |
| } | |
| void EmitRow(intptr_t file, | |
| intptr_t line, | |
| intptr_t column, | |
| intptr_t label, | |
| intptr_t pc_offset) override { | |
| USE(label); | |
| ASSERT(file >= 1); | |
| ASSERT(line >= 0); | |
| ASSERT(column >= 0); | |
| if (file == file_ && line == line_ && column == column_) { | |
| return; | |
| } | |
| file_ = file; | |
| line_ = line; | |
| column_ = column; | |
| ASSERT(file <= script_count_); | |
| if (line == 0) { | |
| return; | |
| } | |
| ASSERT(pc_offset <= code_size_); | |
| if (pc_offset == code_size_) { | |
| // CodeView line rows describe instruction offsets inside the function | |
| // body. A trailing row exactly at code_size is the DWARF end-address | |
| // boundary and has no instruction to attach to in DEBUG_S_LINES. | |
| return; | |
| } | |
| CVLineRow row; | |
| row.pc_offset = static_cast<uint32_t>(pc_offset); | |
| row.file_index = static_cast<int32_t>(file - 1); | |
| row.line = static_cast<int32_t>(line); | |
| if (!rows_.is_empty()) { | |
| ASSERT(row.pc_offset > rows_.Last().pc_offset); | |
| if (row.pc_offset <= rows_.Last().pc_offset) { | |
| return; | |
| } | |
| } | |
| rows_.Add(row); | |
| return; | |
| } | |
| const GrowableArray<CVLineRow>& rows() const { return rows_; } | |
| private: | |
| GrowableArray<CVLineRow> rows_; | |
| CoffWriter* const writer_; | |
| const intptr_t script_count_; | |
| const intptr_t code_size_; | |
| intptr_t file_ = 0; | |
| intptr_t line_ = 1; | |
| intptr_t column_ = 0; | |
| }; | |
| // ---------------------------------------------------------------------------- | |
| // Script URI resolution | |
| // ---------------------------------------------------------------------------- | |
| const char* CodeViewBuilder::ResolvedScriptUri(const Script& script) { | |
| return DebugInfo::ResolveScriptUri(zone_, script, | |
| writer_->debug_deobfuscation_trie()); | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // Pre-compute string-table and file-checksum offsets per script. | |
| // ---------------------------------------------------------------------------- | |
| // | |
| // CV_LINE_BLOCK records reference a script by its byte-offset within the | |
| // DEBUG_S_FILECHKSMS subsection body. Each FILECHKSMS entry in turn references | |
| // the script URI by its byte-offset within the DEBUG_S_STRINGTABLE subsection | |
| // body. We compute both offsets up-front so the EmitLines step can stamp them | |
| // directly into the line-block headers without re-walking either subsection. | |
| // | |
| // FILECHKSMS entry layout when ChecksumKind=NONE and ChecksumSize=0: | |
| // u32 NameOffset | |
| // u8 ChecksumSize | |
| // u8 ChecksumKind | |
| // padding to 4-byte alignment | |
| // = 6 bytes payload, padded to 8. Each script slot is exactly 8 bytes. | |
| void CodeViewBuilder::ComputeScriptTables() { | |
| script_string_offsets_->Clear(); | |
| script_checksum_offsets_->Clear(); | |
| // String table starts with a mandatory empty string at offset 0. | |
| static constexpr uint32_t kCheckSumEntrySize = 8; | |
| uint32_t string_offset = 1; | |
| const auto& scripts = writer_->debug_scripts(); | |
| for (intptr_t i = 0; i < scripts.length(); i++) { | |
| const Script& script = *(scripts[i]); | |
| const char* uri = ResolvedScriptUri(script); | |
| script_string_offsets_->Add(string_offset); | |
| string_offset += static_cast<uint32_t>(strlen(uri)) + 1; // include NUL | |
| // Actual entries are emitted in EmitFileChecksumsSubsection, see below. | |
| script_checksum_offsets_->Add( | |
| static_cast<uint32_t>(i * kCheckSumEntrySize)); | |
| } | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // Function gathering: one CodeView function per Dart Code. | |
| // ---------------------------------------------------------------------------- | |
| void CodeViewBuilder::GatherFunctions() { | |
| const auto& codes = writer_->debug_codes(); | |
| functions_->Clear(); | |
| functions_->EnsureLength(codes.length(), FunctionInfo()); | |
| Function& function = Function::Handle(zone_); | |
| CStringIntMap usage_count(zone_); | |
| auto mangle_name = [&](const char* name) -> const char* { | |
| auto* const pair = usage_count.Lookup(name); | |
| if (pair == nullptr) { | |
| usage_count.Insert({name, 1}); | |
| return OS::SCreate(zone_, "%s", name); | |
| } | |
| return OS::SCreate(zone_, "%s (#%" Pd ")", name, ++pair->value); | |
| }; | |
| for (intptr_t i = 0; i < codes.length(); i++) { | |
| FunctionInfo& fn = (*functions_)[i]; | |
| const Code& code = *(codes[i]); | |
| const intptr_t label = writer_->LookupDebugCodeLabel(code); | |
| if (label <= 0 || !writer_->label_to_symbol_index_.HasKey(label)) continue; | |
| const intptr_t sym_idx = writer_->label_to_symbol_index_.Lookup(label); | |
| ASSERT(sym_idx > 0 && sym_idx <= writer_->symbols_->length()); | |
| const auto& sym = (*writer_->symbols_)[sym_idx - 1]; | |
| ASSERT(sym.section_number == CoffWriter::kTextSectionNumber); | |
| fn.label = label; | |
| if (code.IsFunctionCode() && !code.IsUnknownDartCode()) { | |
| function = code.function(); | |
| ASSERT(!function.IsNull()); | |
| const char* raw_name = function.QualifiedUserVisibleNameCString(); | |
| const char* deob_name = ImageWriter::Deobfuscate( | |
| zone_, writer_->debug_deobfuscation_trie(), raw_name); | |
| if (deob_name == nullptr || deob_name[0] == '\0') { | |
| deob_name = sym.name != nullptr ? sym.name : "<anonymous>"; | |
| } | |
| fn.name = mangle_name(deob_name); | |
| } else { | |
| // No Dart-level function (stubs etc.) -- use the COFF symbol name we | |
| // already chose for this Code so the .pdb at least shows that. | |
| fn.name = sym.name != nullptr ? mangle_name(sym.name) | |
| : OS::SCreate(zone_, "code_%" Pd, label); | |
| } | |
| } | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // .debug$T type records | |
| // ---------------------------------------------------------------------------- | |
| void CodeViewBuilder::EmitTypeRecords() { | |
| // Leading signature. | |
| debugT_->u4(coff::CV_SIGNATURE_C13); | |
| // LF_ARGLIST (empty). | |
| debugT_->WritePrefixedLengthU16([&]() { | |
| debugT_->u2(coff::LF_ARGLIST); | |
| debugT_->u4(0); // count | |
| debugT_->Align(4); | |
| arglist_type_ = next_type_index_++; | |
| }); | |
| // LF_PROCEDURE (rvtype=T_VOID, calltype=C_NEAR_C, parmcount=0, arglist). | |
| debugT_->WritePrefixedLengthU16([&]() { | |
| debugT_->u2(coff::LF_PROCEDURE); | |
| debugT_->u4(coff::T_VOID); | |
| debugT_->u1(coff::CV_CALL_NEAR_C); | |
| debugT_->u1(0); // func attrs | |
| debugT_->u2(0); // param count | |
| debugT_->u4(arglist_type_); | |
| debugT_->Align(4); | |
| procedure_type_ = next_type_index_++; | |
| }); | |
| // For each function: LF_STRING_ID then LF_FUNC_ID. | |
| for (intptr_t i = 0; i < functions_->length(); i++) { | |
| FunctionInfo& fn = (*functions_)[i]; | |
| if (fn.name == nullptr) continue; | |
| // LF_STRING_ID with the function's name. | |
| debugT_->WritePrefixedLengthU16([&]() { | |
| debugT_->u2(coff::LF_STRING_ID); | |
| debugT_->u4(0); // substring | |
| debugT_->string(fn.name); | |
| debugT_->Align(4); | |
| next_type_index_++; | |
| }); | |
| // LF_FUNC_ID. | |
| debugT_->WritePrefixedLengthU16([&]() { | |
| debugT_->u2(coff::LF_FUNC_ID); | |
| debugT_->u4(0); // parent scope | |
| debugT_->u4(procedure_type_); | |
| debugT_->string(fn.name); | |
| debugT_->Align(4); | |
| fn.func_id_type = next_type_index_++; | |
| }); | |
| } | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // .debug$S subsections | |
| // ---------------------------------------------------------------------------- | |
| void CodeViewBuilder::EmitOneFunctionSymbol(const FunctionInfo& fn, | |
| const Code& code) { | |
| const uint32_t code_size = static_cast<uint32_t>(code.Size()); | |
| auto* const relocs = writer_->by_kind_[CoffWriter::kSecDebugS]->relocs; | |
| // S_GPROC32_ID. | |
| debugS_->WritePrefixedLengthU16([&]() { | |
| debugS_->u2(coff::S_GPROC32_ID); | |
| debugS_->u4(0); // Parent | |
| debugS_->u4(0); // End (would patch with offset to S_PROC_ID_END) | |
| debugS_->u4(0); // Next | |
| debugS_->u4(code_size); | |
| debugS_->u4(0); // DebugStart | |
| debugS_->u4(code_size); // DebugEnd | |
| debugS_->u4(fn.func_id_type); | |
| // CodeOffset slot -- SECREL relocation against this function's COFF | |
| // symbol. | |
| relocs->Add({static_cast<uint32_t>(debugS_->Position()), fn.label, | |
| coff::PE_REL_AMD64_SECREL}); | |
| debugS_->u4(0); // inline addend | |
| // Segment slot -- SECTION relocation against this function's COFF symbol. | |
| relocs->Add({static_cast<uint32_t>(debugS_->Position()), fn.label, | |
| coff::PE_REL_AMD64_SECTION}); | |
| debugS_->u2(0); | |
| debugS_->u1(0); // flags | |
| debugS_->string(fn.name); | |
| debugS_->Align(4); | |
| }); | |
| // S_FRAMEPROC. | |
| debugS_->WritePrefixedLengthU16([&]() { | |
| debugS_->u2(coff::S_FRAMEPROC); | |
| debugS_->u4(0); // frame size | |
| debugS_->u4(0); // padding size | |
| debugS_->u4(0); // offset to padding | |
| debugS_->u4(0); // bytes of callee-saved regs | |
| debugS_->u4(0); // offset of exception handler | |
| debugS_->u2(0); // section of exception handler | |
| // Flags: EncodedLocalBasePointer=rbp (bits 14-15 == 0b01). | |
| debugS_->u4(1u << 14); | |
| debugS_->Align(4); | |
| }); | |
| // S_PROC_ID_END. | |
| debugS_->u2(2); // length = 2 (kind only) | |
| debugS_->u2(coff::S_PROC_ID_END); | |
| } | |
| void CodeViewBuilder::EmitObjectAndCompileSymbolsSubsection() { | |
| debugS_->u4(coff::DEBUG_S_SYMBOLS); | |
| debugS_->WritePrefixedLength([&]() { | |
| // S_OBJNAME. | |
| debugS_->WritePrefixedLengthU16([&]() { | |
| debugS_->u2(coff::S_OBJNAME); | |
| debugS_->u4(0); // signature | |
| debugS_->string("snapshot.obj"); | |
| debugS_->Align(4); | |
| }); | |
| // S_COMPILE3. | |
| debugS_->WritePrefixedLengthU16([&]() { | |
| debugS_->u2(coff::S_COMPILE3); | |
| debugS_->u4(static_cast<uint32_t>(coff::CV_CFL_C)); | |
| debugS_->u2(coff::CV_CFL_X64); | |
| debugS_->u2(0); // FE Major | |
| debugS_->u2(0); // FE Minor | |
| debugS_->u2(0); // FE Build | |
| debugS_->u2(0); // FE QFE | |
| debugS_->u2(0); // BE Major | |
| debugS_->u2(0); // BE Minor | |
| debugS_->u2(0); // BE Build | |
| debugS_->u2(0); // BE QFE | |
| debugS_->string("Dart AOT"); | |
| debugS_->Align(4); | |
| }); | |
| }); | |
| debugS_->Align(4); | |
| } | |
| void CodeViewBuilder::EmitFunctionSymbolsSubsection(const FunctionInfo& fn, | |
| const Code& code) { | |
| debugS_->u4(coff::DEBUG_S_SYMBOLS); | |
| debugS_->WritePrefixedLength([&]() { EmitOneFunctionSymbol(fn, code); }); | |
| debugS_->Align(4); | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // Per-function line tables: walk each Code's CodeSourceMap to build a sequence | |
| // of (pc_offset, file_index, line) rows, then emit one DEBUG_S_LINES subsection | |
| // per Code with the rows grouped into CV_LINE_BLOCKs by file. | |
| // ---------------------------------------------------------------------------- | |
| void CodeViewBuilder::EmitLinesSubsection(const FunctionInfo& fn, | |
| const Code& code) { | |
| if (!code.IsFunctionCode() || code.IsUnknownDartCode()) return; | |
| CodeSourceMap& map = CodeSourceMap::Handle(zone_, code.code_source_map()); | |
| if (map.IsNull() || map.Length() == 0) { | |
| return; | |
| } | |
| Function& root_function = Function::Handle(zone_, code.function()); | |
| ASSERT(!root_function.IsNull()); | |
| CVLineNumberProgramWriter writer( | |
| zone_, writer_, writer_->debug_scripts().length(), code.Size()); | |
| DebugInfo::WriteLineNumberProgramForCode(zone_, code, &writer); | |
| const auto& rows = writer.rows(); | |
| if (rows.is_empty()) return; | |
| // Group consecutive rows by file_index into CV_LINE_BLOCK chunks. | |
| struct Block { | |
| int32_t file_index; | |
| intptr_t start; // index into rows | |
| intptr_t count; | |
| }; | |
| GrowableArray<Block> blocks(zone_, 4); | |
| for (intptr_t r = 0; r < rows.length(); r++) { | |
| if (blocks.is_empty() || blocks.Last().file_index != rows[r].file_index) { | |
| Block b = {rows[r].file_index, r, 1}; | |
| blocks.Add(b); | |
| } else { | |
| blocks.Last().count++; | |
| } | |
| } | |
| if (blocks.is_empty()) return; | |
| intptr_t code_offset_pos = 0; | |
| intptr_t section_pos = 0; | |
| debugS_->u4(coff::DEBUG_S_LINES); | |
| debugS_->WritePrefixedLength([&]() { | |
| // DEBUG_S_LINES section header. | |
| code_offset_pos = debugS_->Position(); | |
| debugS_->u4(0); // inline addend | |
| section_pos = debugS_->Position(); | |
| debugS_->u2(0); // Section (SECTION reloc) | |
| debugS_->u2(0); // Flags | |
| debugS_->u4(static_cast<uint32_t>(code.Size())); | |
| for (intptr_t b = 0; b < blocks.length(); b++) { | |
| const Block& blk = blocks[b]; | |
| const uint32_t chksm_offset = (*script_checksum_offsets_)[blk.file_index]; | |
| debugS_->u4(chksm_offset); // NameIndex (file checksum offset) | |
| debugS_->u4(static_cast<uint32_t>(blk.count)); | |
| const uint32_t block_size = static_cast<uint32_t>( | |
| sizeof(coff::CvLineBlockHeader) + sizeof(coff::CvLine) * blk.count); | |
| debugS_->u4(block_size); | |
| for (intptr_t r = blk.start; r < blk.start + blk.count; r++) { | |
| debugS_->u4(rows[r].pc_offset); | |
| const uint32_t line_field = | |
| (static_cast<uint32_t>(rows[r].line) & coff::CvLine::kLineMask) | | |
| coff::CvLine::kIsStatementBit; | |
| debugS_->u4(line_field); | |
| } | |
| } | |
| }); | |
| debugS_->Align(4); | |
| writer_->by_kind_[CoffWriter::kSecDebugS]->relocs->Add( | |
| {static_cast<uint32_t>(code_offset_pos), fn.label, | |
| coff::PE_REL_AMD64_SECREL}); | |
| writer_->by_kind_[CoffWriter::kSecDebugS]->relocs->Add( | |
| {static_cast<uint32_t>(section_pos), fn.label, | |
| coff::PE_REL_AMD64_SECTION}); | |
| } | |
| void CodeViewBuilder::EmitFileChecksumsSubsection() { | |
| debugS_->u4(coff::DEBUG_S_FILECHKSMS); | |
| debugS_->WritePrefixedLength([&]() { | |
| const auto& scripts = writer_->debug_scripts(); | |
| for (intptr_t i = 0; i < scripts.length(); i++) { | |
| // Each entry is exactly 8 bytes (6 payload + 2 padding), matching the | |
| // layout assumed by ComputeScriptTables. | |
| debugS_->u4((*script_string_offsets_)[i]); // NameOffset | |
| debugS_->u1(0); // ChecksumSize | |
| debugS_->u1(coff::CHKSUM_TYPE_NONE); | |
| debugS_->u1(0); // padding | |
| debugS_->u1(0); // padding | |
| } | |
| }); | |
| debugS_->Align(4); | |
| } | |
| void CodeViewBuilder::EmitStringTableSubsection() { | |
| debugS_->u4(coff::DEBUG_S_STRINGTABLE); | |
| debugS_->WritePrefixedLength([&]() { | |
| // Leading mandatory empty string. | |
| debugS_->u1(0); | |
| const auto& scripts = writer_->debug_scripts(); | |
| for (intptr_t i = 0; i < scripts.length(); i++) { | |
| const Script& script = *(scripts[i]); | |
| const char* uri = ResolvedScriptUri(script); | |
| debugS_->string(uri); // NOLINT(build/include_what_you_use) | |
| } | |
| }); | |
| debugS_->Align(4); | |
| } | |
| // ---------------------------------------------------------------------------- | |
| // Build | |
| // ---------------------------------------------------------------------------- | |
| void CodeViewBuilder::Build() { | |
| GatherFunctions(); | |
| ComputeScriptTables(); | |
| EmitTypeRecords(); | |
| debugS_->u4(coff::CV_SIGNATURE_C13); | |
| EmitObjectAndCompileSymbolsSubsection(); | |
| const auto& codes = writer_->debug_codes(); | |
| ASSERT(functions_->length() == codes.length()); | |
| for (intptr_t i = 0; i < codes.length(); i++) { | |
| const FunctionInfo& fn = (*functions_)[i]; | |
| if (fn.name == nullptr) continue; | |
| const Code& code = *(codes[i]); | |
| EmitFunctionSymbolsSubsection(fn, code); | |
| EmitLinesSubsection(fn, code); | |
| } | |
| EmitFileChecksumsSubsection(); | |
| EmitStringTableSubsection(); | |
| } | |
| } // namespace dart | |
| #endif // DART_PRECOMPILER |