blob: e68c2e250edf7d88705887bea394b976d27286ef [file] [edit]
// 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