diff --git a/runtime/platform/mach_o.h b/runtime/platform/mach_o.h index 24f38b793de..b5e658a9bcb 100644 --- a/runtime/platform/mach_o.h +++ b/runtime/platform/mach_o.h @@ -537,6 +537,112 @@ struct cs_code_directory { // 8-byte aligned (like blobs) and the hash data is 16-byte aligned. }; +// Compact unwinding information constants for encodings. + +// Architecture-independent constants for encodings. + +// A shorthand for the zero-value encoding that denotes that the associated +// memory space does not contain function instructions. +static constexpr uint32_t UNWIND_INFO_ENCODING_NONE = 0; + +static constexpr uint32_t UNWIND_INFO_ENCODING_IS_NOT_FUNCTION_START = + 0x80000000; +static constexpr uint32_t UNWIND_INFO_ENCODING_HAS_LSDA = 0x40000000; +static constexpr uint32_t UNWIND_INFO_ENCODING_PERSONALITY_MASK = 0x30000000; + +// Currently compact unwinding information is only generated for ARM64, so only +// the constants used by the MachO writer are included below. + +// ARM64-specific constants for encodings. +static constexpr uint32_t UNWIND_INFO_ENCODING_ARM64_MODE_MASK = 0x0F000000; + +// A standard ARM64 prologue where FP/LR are immediately pushed on the +// stack and then SP is copied to FP. If there are any non-volatile +// registers saved, they are saved in pairs right below the FP/LR pair +// in register number order. +// +// In the MachO writer, this is the only non-zero encoding used and +// no non-volatile register pairs are recorded as being saved, so +// the appropriate constants for each pair and for other encodings are elided. +static constexpr uint32_t UNWIND_INFO_ENCODING_ARM64_MODE_FRAME = 0x04000000; + +// Note that fields ending in section_offset are offsets into the unwind info +// section as a whole, whereas fields ending in page_offset are offsets into +// the specific second level page (e.g., the page header starts at offset 0). + +struct unwind_info_lsda_index { + uint32_t function_offset; + uint32_t lsda_offset; +}; + +struct unwind_info_first_level_page_index { + uint32_t function_offset; + uint32_t second_level_page_section_offset; + uint32_t lsda_index_section_offset; +}; + +// Second level pages have two formats: a compressed and a "regular" format. +// As the Mach-O writer only creates a single second level page with four +// entries currently, it uses the regular format as it is simpler. + +static constexpr uint32_t UNWIND_INFO_REGULAR_SECOND_LEVEL_PAGE = 2; + +struct unwind_info_regular_second_level_page_entry { + uint32_t function_offset; + uint32_t encoding; +}; + +struct unwind_info_regular_second_level_page_header { + uint32_t kind; // UNWIND_INFO_REGULAR_SECOND_LEVEL_PAGE + uint16_t entry_page_offset; + uint16_t entry_count; +}; + +static const size_t UNWIND_INFO_SECOND_LEVEL_PAGE_MAX_SIZE = 4 * KB; + +static constexpr uint32_t UNWIND_INFO_REGULAR_SECOND_LEVEL_PAGE_MAX_ENTRIES = + (UNWIND_INFO_SECOND_LEVEL_PAGE_MAX_SIZE - + sizeof(unwind_info_regular_second_level_page_header)) / + sizeof(unwind_info_regular_second_level_page_entry); + +constexpr size_t UnwindInfoRegularSecondLevelPageSize(intptr_t entries) { + return sizeof(unwind_info_regular_second_level_page_header) + + sizeof(unwind_info_regular_second_level_page_entry) * entries; +} + +static constexpr uint32_t UNWIND_INFO_VERSION = 1; + +struct unwind_info_header { + uint32_t version; + uint32_t common_encodings_section_offset; + uint32_t common_encodings_count; + uint32_t personalities_section_offset; + uint32_t personalities_count; + uint32_t first_level_page_indices_section_offset; + uint32_t first_level_page_indices_count; + + // Note that the last first level page indices is a sentinel that contains + // the end of the covered space as its function offset and the end + // of the lsda array as its lsda_index_section_offset. + // + // sentinel_index = first_level_page_indices_count - 1 + // + // lsda_size = + // first_level_pages[sentinel_index].lsda_index_section_offset - + // first_level_pages[0].lsda_index_section_offset + // + // lsda_count = lsda_size / sizeof(unwind_info_lsda_index) + // second_level_page_count = sentinel_index; + + // Variadic payload of unwind info section after unwind_info_header: + // uint32_t common_encodings[common_encodings_count] + // uint32_t personalities[personalities_count] + // unwind_info_first_level_page_index + // first_level_pages[first_level_page_indices_count] + // unwind_info_lsda_index lsda[lsda_count] + // ... regular and compressed second level pages ... +}; + #pragma pack(pop) } // namespace mach_o diff --git a/runtime/tests/vm/dart/unobfuscated_static_symbols_test.dart b/runtime/tests/vm/dart/unobfuscated_static_symbols_test.dart index 96217d17a7b..51d46746810 100644 --- a/runtime/tests/vm/dart/unobfuscated_static_symbols_test.dart +++ b/runtime/tests/vm/dart/unobfuscated_static_symbols_test.dart @@ -12,9 +12,7 @@ import "dart:io"; import 'package:expect/expect.dart'; -import 'package:native_stack_traces/elf.dart'; import 'package:native_stack_traces/src/dwarf_container.dart'; -import 'package:native_stack_traces/src/macho.dart'; import 'package:path/path.dart' as path; import 'use_flag_test_helper.dart'; @@ -62,76 +60,6 @@ Future main(List args) async { }); } -const commonGenSnapshotArgs = [ - // Make sure that the runs are deterministic so we can depend on the same - // snapshot being generated each time. - '--deterministic', -]; - -enum SnapshotType { - elf, - machoDylib, - assembly; - - String get kindString { - switch (this) { - case elf: - return 'app-aot-elf'; - case machoDylib: - return 'app-aot-macho-dylib'; - case assembly: - return 'app-aot-assembly'; - } - } - - String get fileArgumentName { - switch (this) { - case elf: - return 'elf'; - case machoDylib: - return 'macho'; - case assembly: - return 'assembly'; - } - } - - DwarfContainer? fromFile(String filename) { - switch (this) { - case elf: - return Elf.fromFile(filename); - case machoDylib: - return MachO.fromFile(filename); - case assembly: - return Elf.fromFile(filename) ?? MachO.fromFile(filename); - } - } - - @override - String toString() => name; -} - -Future createSnapshot( - String scriptDill, - SnapshotType snapshotType, - String finalPath, [ - List extraArgs = const [], -]) async { - String output = finalPath; - if (snapshotType == SnapshotType.assembly) { - output = path.withoutExtension(finalPath) + '.S'; - } - await run(genSnapshot, [ - ...commonGenSnapshotArgs, - ...extraArgs, - '--snapshot-kind=${snapshotType.kindString}', - '--${snapshotType.fileArgumentName}=$output', - scriptDill, - ]); - if (snapshotType == SnapshotType.assembly) { - await assembleSnapshot(output, finalPath); - } -} - Future?> retrieveDebugMap( SnapshotType snapshotType, String snapshotPath, diff --git a/runtime/tests/vm/dart/unwinding_information_test.dart b/runtime/tests/vm/dart/unwinding_information_test.dart new file mode 100644 index 00000000000..b54cb19707f --- /dev/null +++ b/runtime/tests/vm/dart/unwinding_information_test.dart @@ -0,0 +1,130 @@ +// Copyright (c) 2025, 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. + +// This test checks that the compact unwinding information is appropriately +// generated for Mac ARM64 snapshots. + +// OtherResources=use_save_debugging_info_flag_program.dart + +import "dart:io"; + +import 'package:expect/expect.dart'; +import 'package:native_stack_traces/src/dwarf_container.dart'; +import 'package:path/path.dart' as path; + +import 'use_flag_test_helper.dart'; + +Future main(List args) async { + if (!isAOTRuntime) { + return; // Running in JIT: AOT binaries not available. + } + + // Currently, this test only checks compact unwinding information, which + // is only generated on Mac ARM64 binaries. + if (!Platform.isMacOS || !buildDir.endsWith('ARM64')) { + return; + } + + // These are the tools we need to be available to run on a given platform: + if (!await testExecutable(genSnapshot)) { + throw "Cannot run test as $genSnapshot not available"; + } + if (!await testExecutable(dartPrecompiledRuntime)) { + throw "Cannot run test as $dartPrecompiledRuntime not available"; + } + if (!File(platformDill).existsSync()) { + throw "Cannot run test as $platformDill does not exist"; + } + + await withTempDir('unwinding-information', (String tempDir) async { + final cwDir = path.dirname(Platform.script.toFilePath()); + final script = path.join( + cwDir, + 'use_save_debugging_info_flag_program.dart', + ); + final scriptDill = path.join(tempDir, 'flag_program.dill'); + + // Compile script to Kernel IR. + await run(genKernel, [ + '--aot', + '--platform=$platformDill', + '-o', + scriptDill, + script, + ]); + + await checkMachO(tempDir, scriptDill); + }); +} + +Future> retrieveUnwindInfo( + SnapshotType snapshotType, + String snapshotPath, +) async { + if (snapshotType != SnapshotType.machoDylib) { + throw ArgumentError("Unhandled snapshot type"); + } + final objdump = llvmTool('llvm-objdump'); + if (objdump == null) { + throw StateError('Expected llvm-objdump in buildutils'); + } + return await runOutput(objdump, ['--macho', '-u', snapshotPath]); +} + +Future checkSnapshotType( + String tempDir, + String scriptDill, + SnapshotType snapshotType, +) async { + final scriptUnstrippedSnapshot = path.join( + tempDir, + 'unstripped-$snapshotType.so', + ); + await createSnapshot(scriptDill, snapshotType, scriptUnstrippedSnapshot); + final unstrippedCase = TestCase( + snapshotType, + scriptUnstrippedSnapshot, + snapshotType.fromFile(scriptUnstrippedSnapshot)!, + await retrieveUnwindInfo(snapshotType, scriptUnstrippedSnapshot), + ); + + final scriptStrippedSnapshot = path.join( + tempDir, + 'stripped-$snapshotType.so', + ); + await createSnapshot(scriptDill, snapshotType, scriptStrippedSnapshot, [ + '--strip', + ]); + final strippedCase = TestCase( + snapshotType, + scriptStrippedSnapshot, + snapshotType.fromFile(scriptStrippedSnapshot)!, + await retrieveUnwindInfo(snapshotType, scriptStrippedSnapshot), + ); + + checkCases(unstrippedCase, strippedCase); +} + +Future checkMachO(String tempDir, String scriptDill) async { + await checkSnapshotType(tempDir, scriptDill, SnapshotType.machoDylib); +} + +class TestCase { + final SnapshotType snapshotType; + final String snapshotPath; + final DwarfContainer container; + final List unwindInfo; + + TestCase( + this.snapshotType, + this.snapshotPath, + this.container, + this.unwindInfo, + ); +} + +void checkCases(TestCase unstripped, TestCase stripped) { + Expect.isNotEmpty(unstripped.unwindInfo); + Expect.deepEquals(unstripped.unwindInfo, stripped.unwindInfo); +} diff --git a/runtime/tests/vm/dart/use_flag_test_helper.dart b/runtime/tests/vm/dart/use_flag_test_helper.dart index 9154e3132be..69ecb38fa6e 100644 --- a/runtime/tests/vm/dart/use_flag_test_helper.dart +++ b/runtime/tests/vm/dart/use_flag_test_helper.dart @@ -8,6 +8,10 @@ import 'dart:ffi'; import 'package:expect/config.dart'; import 'package:expect/expect.dart'; +import 'package:native_stack_traces/src/elf.dart' show Elf; +import 'package:native_stack_traces/src/dwarf_container.dart' + show DwarfContainer; +import 'package:native_stack_traces/src/macho.dart' show MachO; import 'package:path/path.dart' as path; final isAOTRuntime = isVmAotConfiguration; @@ -286,3 +290,73 @@ Future withTempDir(String name, Future fun(String dir)) async { } } } + +enum SnapshotType { + elf, + machoDylib, + assembly; + + String get kindString { + switch (this) { + case elf: + return 'app-aot-elf'; + case machoDylib: + return 'app-aot-macho-dylib'; + case assembly: + return 'app-aot-assembly'; + } + } + + String get fileArgumentName { + switch (this) { + case elf: + return 'elf'; + case machoDylib: + return 'macho'; + case assembly: + return 'assembly'; + } + } + + DwarfContainer? fromFile(String filename) { + switch (this) { + case elf: + return Elf.fromFile(filename); + case machoDylib: + return MachO.fromFile(filename); + case assembly: + return Elf.fromFile(filename) ?? MachO.fromFile(filename); + } + } + + @override + String toString() => name; +} + +const _commonGenSnapshotArgs = [ + // Make sure that the runs are deterministic so we can depend on the same + // snapshot being generated each time. + '--deterministic', +]; + +Future createSnapshot( + String scriptDill, + SnapshotType snapshotType, + String finalPath, [ + List extraArgs = const [], +]) async { + String output = finalPath; + if (snapshotType == SnapshotType.assembly) { + output = path.withoutExtension(finalPath) + '.S'; + } + await run(genSnapshot, [ + ..._commonGenSnapshotArgs, + ...extraArgs, + '--snapshot-kind=${snapshotType.kindString}', + '--${snapshotType.fileArgumentName}=$output', + scriptDill, + ]); + if (snapshotType == SnapshotType.assembly) { + await assembleSnapshot(output, finalPath); + } +} diff --git a/runtime/vm/dwarf.cc b/runtime/vm/dwarf.cc index d3f28e29c6a..daa279227a4 100644 --- a/runtime/vm/dwarf.cc +++ b/runtime/vm/dwarf.cc @@ -1000,7 +1000,7 @@ void Dwarf::WriteCallFrameInformationRecords( // backwards: it will be subtracted from the current position. stream->u4(stream->Position() - cie_start); // Start address as a PC relative reference. - stream->RelativeSymbolOffset(fde.label); + stream->RelativeSymbolOffset(fde.label, kInt32Size); stream->u4(fde.size); // Size. stream->u1(0); // Augmentation Data length. diff --git a/runtime/vm/dwarf.h b/runtime/vm/dwarf.h index 9e0353d3b0a..ee93a82247f 100644 --- a/runtime/vm/dwarf.h +++ b/runtime/vm/dwarf.h @@ -144,12 +144,25 @@ class DwarfWriteStream : public ValueObject { virtual void WritePrefixedLength(const char* symbol_prefix, std::function body) = 0; - virtual void OffsetFromSymbol(intptr_t label, intptr_t offset) = 0; + // Generates a relocated address from the given symbol label and offset. + // + // If no size is provided, the size of the relocated address in the stream + // is the native word size. + virtual void OffsetFromSymbol(intptr_t label, + intptr_t offset, + size_t size = kAddressSize) = 0; virtual void InitializeAbstractOrigins(intptr_t size) = 0; virtual void RegisterAbstractOrigin(intptr_t index) = 0; virtual void AbstractOrigin(intptr_t index) = 0; + protected: +#if defined(TARGET_ARCH_IS_32_BIT) + static constexpr size_t kAddressSize = kInt32Size; +#else + static constexpr size_t kAddressSize = kInt64Size; +#endif + DISALLOW_COPY_AND_ASSIGN(DwarfWriteStream); }; diff --git a/runtime/vm/dwarf_so_writer.h b/runtime/vm/dwarf_so_writer.h index 3f7cd948b04..7b939916ec0 100644 --- a/runtime/vm/dwarf_so_writer.h +++ b/runtime/vm/dwarf_so_writer.h @@ -28,18 +28,19 @@ class DwarfSharedObjectStream : public DwarfWriteStream { intptr_t bytes_written() const { return stream_->bytes_written(); } intptr_t Position() const { return stream_->Position(); } - void sleb128(intptr_t value) { stream_->WriteSLEB128(value); } - void uleb128(uintptr_t value) { stream_->WriteLEB128(value); } - void u1(uint8_t value) { stream_->WriteByte(value); } - void u2(uint16_t value) { stream_->WriteFixed(value); } - void u4(uint32_t value) { stream_->WriteFixed(value); } - void u8(uint64_t value) { stream_->WriteFixed(value); } - void string(const char* cstr) { // NOLINT + void sleb128(intptr_t value) override { stream_->WriteSLEB128(value); } + void uleb128(uintptr_t value) override { stream_->WriteLEB128(value); } + void u1(uint8_t value) override { stream_->WriteByte(value); } + void u2(uint16_t value) override { stream_->WriteFixed(value); } + void u4(uint32_t value) override { stream_->WriteFixed(value); } + void u8(uint64_t value) override { stream_->WriteFixed(value); } + void string(const char* cstr) override { // NOLINT // Unlike stream_->WriteString(), we want the null terminator written. stream_->WriteBytes(cstr, strlen(cstr) + 1); } // The prefix is ignored for DwarfSharedObjectStreams. - void WritePrefixedLength(const char* unused, std::function body) { + void WritePrefixedLength(const char* unused, + std::function body) override { const intptr_t fixup = stream_->Position(); // We assume DWARF v2 currently, so all sizes are 32-bit. u4(0); @@ -57,49 +58,53 @@ class DwarfSharedObjectStream : public DwarfWriteStream { WritePrefixedLength(nullptr, body); } - void OffsetFromSymbol(intptr_t label, intptr_t offset) { - relocations_->Add({kAddressSize, stream_->Position(), + void OffsetFromSymbol(intptr_t label, + intptr_t offset, + size_t size = kAddressSize) override { + ASSERT(size > 0); + ASSERT(size <= static_cast(kInt64Size)); + relocations_->Add({size, stream_->Position(), SharedObjectWriter::Relocation::kSnapshotRelative, 0, label, offset}); - addr(0); // Resolved later. + const uint64_t placeholder = 0; // Resolved later. + stream_->WriteBytes(&placeholder, size); } - template - void RelativeSymbolOffset(intptr_t label) { - relocations_->Add({sizeof(T), stream_->Position(), - SharedObjectWriter::Relocation::kSelfRelative, 0, label, - 0}); - stream_->WriteFixed(0); // Resolved later. - } - void InitializeAbstractOrigins(intptr_t size) { + void InitializeAbstractOrigins(intptr_t size) override { abstract_origins_size_ = size; abstract_origins_ = zone_->Alloc(abstract_origins_size_); } - void RegisterAbstractOrigin(intptr_t index) { + void RegisterAbstractOrigin(intptr_t index) override { ASSERT(abstract_origins_ != nullptr); ASSERT(index < abstract_origins_size_); abstract_origins_[index] = stream_->Position(); } - void AbstractOrigin(intptr_t index) { u4(abstract_origins_[index]); } + void AbstractOrigin(intptr_t index) override { u4(abstract_origins_[index]); } + + // Generates the offset of the virtual address corresponding to the given + // symbol label from the current position in the output. That is, if + // X = the virtual address of the current position + // Y = the virtual address of the symbol + // then the value at the current position in the output is Y - X. + // + // If no size is provided, the size of the offset in the stream is + // the native word size. + void RelativeSymbolOffset(intptr_t label, size_t size = kAddressSize) { + relocations_->Add({size, stream_->Position(), + SharedObjectWriter::Relocation::kSelfRelative, 0, label, + 0}); + const uint64_t placeholder = 0; // Resolved later. + stream_->WriteBytes(&placeholder, size); + } + + intptr_t Align(intptr_t alignment, intptr_t offset = 0) { + return stream_->Align(alignment, offset); + } const SharedObjectWriter::RelocationArray* relocations() const { return relocations_; } protected: -#if defined(TARGET_ARCH_IS_32_BIT) - static constexpr intptr_t kAddressSize = kInt32Size; -#else - static constexpr intptr_t kAddressSize = kInt64Size; -#endif - - void addr(uword value) { -#if defined(TARGET_ARCH_IS_32_BIT) - u4(value); -#else - u8(value); -#endif - } - Zone* const zone_; NonStreamingWriteStream* const stream_; SharedObjectWriter::RelocationArray* const relocations_ = nullptr; diff --git a/runtime/vm/image_snapshot.cc b/runtime/vm/image_snapshot.cc index 7cba27bf631..2bd2650b6a9 100644 --- a/runtime/vm/image_snapshot.cc +++ b/runtime/vm/image_snapshot.cc @@ -1064,24 +1064,26 @@ class DwarfAssemblyStream : public DwarfWriteStream { stream_(ASSERT_NOTNULL(stream)), label_to_name_(label_to_name) {} - void sleb128(intptr_t value) { stream_->Printf(".sleb128 %" Pd "\n", value); } - void uleb128(uintptr_t value) { + void sleb128(intptr_t value) override { + stream_->Printf(".sleb128 %" Pd "\n", value); + } + void uleb128(uintptr_t value) override { stream_->Printf(".uleb128 %" Pd "\n", value); } - void u1(uint8_t value) { + void u1(uint8_t value) override { stream_->Printf("%s %u\n", kSizeDirectives[kInt8SizeLog2], value); } - void u2(uint16_t value) { + void u2(uint16_t value) override { stream_->Printf("%s %u\n", kSizeDirectives[kInt16SizeLog2], value); } - void u4(uint32_t value) { + void u4(uint32_t value) override { stream_->Printf("%s %" Pu32 "\n", kSizeDirectives[kInt32SizeLog2], value); } - void u8(uint64_t value) { + void u8(uint64_t value) override { stream_->Printf("%s %" Pu64 "\n", kSizeDirectives[kInt64SizeLog2], value); } - void string(const char* cstr) { // NOLINT - stream_->WriteString(".string \""); // NOLINT + void string(const char* cstr) override { // NOLINT + stream_->WriteString(".string \""); // NOLINT while (char c = *cstr++) { if (c == '"') { stream_->WriteString("\\\""); @@ -1097,7 +1099,8 @@ class DwarfAssemblyStream : public DwarfWriteStream { } stream_->WriteString("\"\n"); } - void WritePrefixedLength(const char* prefix, std::function body) { + void WritePrefixedLength(const char* prefix, + std::function body) override { ASSERT(prefix != nullptr); const char* const length_prefix_symbol = OS::SCreate(zone_, ".L%s_length_prefix", prefix); @@ -1113,23 +1116,25 @@ class DwarfAssemblyStream : public DwarfWriteStream { body(); stream_->Printf(".L%s_end:\n", prefix); } - void OffsetFromSymbol(intptr_t label, intptr_t offset) { + void OffsetFromSymbol(intptr_t label, + intptr_t offset, + size_t size = kAddressSize) override { const char* symbol = label_to_name_.Lookup(label); ASSERT(symbol != nullptr); if (offset == 0) { - PrintNamedAddress(symbol); + PrintNamedAddress(symbol, size); } else { - PrintNamedAddressWithOffset(symbol, offset); + PrintNamedAddressWithOffset(symbol, offset, size); } } // No-op, we'll be using labels. - void InitializeAbstractOrigins(intptr_t size) {} - void RegisterAbstractOrigin(intptr_t index) { + void InitializeAbstractOrigins(intptr_t size) override {} + void RegisterAbstractOrigin(intptr_t index) override { // Label for DW_AT_abstract_origin references stream_->Printf("Lfunc%" Pd " = .-%s\n", index, kDebugInfoLabel); } - void AbstractOrigin(intptr_t index) { + void AbstractOrigin(intptr_t index) override { stream_->Printf("%s Lfunc%" Pd "\n", kSizeDirectives[kInt32SizeLog2], index); } @@ -1171,11 +1176,15 @@ class DwarfAssemblyStream : public DwarfWriteStream { private: static constexpr const char* kDebugInfoLabel = ".Ldebug_info"; - void PrintNamedAddress(const char* name) { - stream_->Printf("%s \"%s\"\n", kWordDirective, name); + void PrintNamedAddress(const char* name, size_t size) { + auto* const directive = kSizeDirectives[Utils::ShiftForPowerOfTwo(size)]; + stream_->Printf("%s \"%s\"\n", directive, name); } - void PrintNamedAddressWithOffset(const char* name, intptr_t offset) { - stream_->Printf("%s \"%s\" + %" Pd "\n", kWordDirective, name, offset); + void PrintNamedAddressWithOffset(const char* name, + intptr_t offset, + size_t size) { + auto* const directive = kSizeDirectives[Utils::ShiftForPowerOfTwo(size)]; + stream_->Printf("%s \"%s\" + %" Pd "\n", directive, name, offset); } Zone* const zone_; @@ -1759,6 +1768,8 @@ void AssemblyImageWriter::FrameUnwindPrologue() { assembly_stream_->WriteString(".cfi_offset rip, -8\n"); assembly_stream_->WriteString(".cfi_offset rbp, -16\n"); #elif defined(TARGET_ARCH_ARM64) + // If this changes, then MachOHeader::GenerateUnwindingInformation must + // also change to generate the appropriate compact unwinding information. COMPILE_ASSERT(R29 == FP); COMPILE_ASSERT(R30 == LINK_REGISTER); assembly_stream_->WriteString(".cfi_def_cfa x29, 16\n"); diff --git a/runtime/vm/image_snapshot.h b/runtime/vm/image_snapshot.h index baeb115a8b4..fc4ed54002d 100644 --- a/runtime/vm/image_snapshot.h +++ b/runtime/vm/image_snapshot.h @@ -161,6 +161,7 @@ class Image : ValueObject { friend class BlobImageWriter; friend class ImageWriter; friend class SharedObjectWriter; + friend class MachOHeader; // For kHeaderSize. DISALLOW_COPY_AND_ASSIGN(Image); }; diff --git a/runtime/vm/mach_o.cc b/runtime/vm/mach_o.cc index 8cc3385e24a..58c88d24532 100644 --- a/runtime/vm/mach_o.cc +++ b/runtime/vm/mach_o.cc @@ -11,9 +11,11 @@ #include "openssl/sha.h" #include "platform/mach_o.h" #include "platform/unwinding_records.h" +#include "vm/compiler/runtime_api.h" #include "vm/dwarf.h" #include "vm/dwarf_so_writer.h" #include "vm/hash_map.h" +#include "vm/image_snapshot.h" #include "vm/os.h" #include "vm/unwinding_records.h" #include "vm/zone_text_buffer.h" @@ -1758,6 +1760,12 @@ class MachOHeader : public MachOContents { void FinalizeCommands(); void ComputeOffsets(); +#if defined(DART_TARGET_OS_MACOS) && defined(TARGET_ARCH_ARM64) + void GenerateCompactUnwindingInformation( + DwarfSharedObjectStream& stream, + const GrowableArray& fdes); +#endif + // Returns the symbol table that is included in the output, which // may or may not be the full symbol table. // @@ -1931,18 +1939,21 @@ void MachOHeader::Finalize() { // debugging information. CreateBSS(); + FinalizeDwarfSections(); + + // Generate miscellenous load commands needed for the final output. + GenerateMiscellaneousCommands(); + // Generate appropriate unwinding information for the target platform, // for example, unwinding records on Windows. GenerateUnwindingInformation(); - FinalizeDwarfSections(); - - // Create and initialize the dynamic and static symbol tables. + // Initialize both the static and dynamic symbol tables. Calls to methods + // that change section numbering (by either adding or reordering sections + // and/or segments) after this point must update the section numbers on + // section symbols to match. InitializeSymbolTables(); - // Generate miscellenous load commands needed for the final output. - GenerateMiscellaneousCommands(); - // Reorders the added commands as well as adding segments and commands // that must appear at the end of the file. FinalizeCommands(); @@ -2086,6 +2097,134 @@ void MachOHeader::CreateBSS() { } } +#if defined(DART_TARGET_OS_MACOS) && defined(TARGET_ARCH_ARM64) +void MachOHeader::GenerateCompactUnwindingInformation( + DwarfSharedObjectStream& stream, + const GrowableArray& fdes) { + // Since we currently generate only regular second level pages, there's + // no need for common encodings as those are only used by compressed + // second level pages. + const intptr_t common_encodings_offset = sizeof(mach_o::unwind_info_header); + GrowableArray common_encodings(zone(), 0); + + const intptr_t personalities_offset = + common_encodings_offset + common_encodings.length() * kInt32Size; + GrowableArray personalities(zone(), 0); + + // For N FDEs, we generate 2N entries: + // * One at the start of the text section with the none encoding. + // * One at the start of each FDE's InstructionsSection payload with + // the frame encoding. + // * For all but the last FDE, one at the end of the InstructionsSection + // payload with the none encoding. + // No entry is needed for the end of the last FDE, since it is + // already recorded as the end of the instructions in the first + // page index sentinel entry. + const intptr_t second_level_page_entry_count = 2 * fdes.length(); + const bool second_level_pages_count = + (second_level_page_entry_count + + (mach_o::UNWIND_INFO_REGULAR_SECOND_LEVEL_PAGE_MAX_ENTRIES - 1)) / + mach_o::UNWIND_INFO_REGULAR_SECOND_LEVEL_PAGE_MAX_ENTRIES; + + const intptr_t first_level_page_indices_offset = + personalities_offset + personalities.length() * kInt32Size; + // There is one first level page index per second level page, plus an + // additional first level page index that serves as as a sentinel and + // contains the ending offset of the LSDA entries. + const intptr_t first_level_page_indices_count = second_level_pages_count + 1; + + // Align the LSDA indices to the target word size, as the first level page + // indices are 12 bytes long and so may not end on a word boundary + // on 64-bit systems. + const intptr_t lsda_indices_offset = + Utils::RoundUp(first_level_page_indices_offset + + first_level_page_indices_count * + sizeof(mach_o::unwind_info_first_level_page_index), + compiler::target::kWordSize); + GrowableArray lsda_indices(zone(), 0); + + const intptr_t second_level_pages_offset = + lsda_indices_offset + + lsda_indices.length() * sizeof(mach_o::unwind_info_lsda_index); + // We should only generate at most 2 FDEs and thus 4 entries, so there + // should only be one second level page that, if placed right after + // the other content, is wholly contained in a 4 * KB page. + ASSERT_EQUAL(1, second_level_pages_count); + const intptr_t second_level_pages_size = + mach_o::UnwindInfoRegularSecondLevelPageSize( + second_level_page_entry_count); + const intptr_t unwind_info_size = + second_level_pages_offset + second_level_pages_size; + ASSERT(static_cast(unwind_info_size) <= + mach_o::UNWIND_INFO_SECOND_LEVEL_PAGE_MAX_SIZE); + + stream.u4(mach_o::UNWIND_INFO_VERSION); + stream.u4(common_encodings_offset); + stream.u4(common_encodings.length()); + stream.u4(personalities_offset); + stream.u4(personalities.length()); + stream.u4(first_level_page_indices_offset); + stream.u4(first_level_page_indices_count); + + ASSERT_EQUAL(common_encodings_offset, stream.Position()); + for (const auto& encoding : common_encodings) { + stream.u4(encoding); + } + + ASSERT_EQUAL(personalities_offset, stream.Position()); + for (const auto& personality : personalities) { + stream.u4(personality); + } + + ASSERT_EQUAL(first_level_page_indices_offset, stream.Position()); + ASSERT_EQUAL(2, first_level_page_indices_count); + const auto& first_fde = fdes[0]; + const auto& last_fde = fdes.Last(); + stream.OffsetFromSymbol(first_fde.label, 0, kInt32Size); + stream.u4(second_level_pages_offset); + stream.u4(lsda_indices_offset); + // Sentinel that includes the end of the function space as the offset + // and has an LSDA index offset at the end of the LSDA index array. + stream.OffsetFromSymbol(last_fde.label, last_fde.size, kInt32Size); + stream.u4(0); // No second level page. + stream.u4(lsda_indices_offset + + lsda_indices.length() * sizeof(mach_o::unwind_info_lsda_index)); + + stream.Align(compiler::target::kWordSize); + ASSERT_EQUAL(lsda_indices_offset, stream.Position()); + for (const auto& lsda_index : lsda_indices) { + stream.u4(lsda_index.function_offset); + stream.u4(lsda_index.lsda_offset); + } + + ASSERT_EQUAL(second_level_pages_offset, stream.Position()); + ASSERT_EQUAL(1, second_level_pages_count); + stream.u4(mach_o::UNWIND_INFO_REGULAR_SECOND_LEVEL_PAGE); + stream.u2(sizeof(mach_o::unwind_info_regular_second_level_page_header)); + stream.u2(second_level_page_entry_count); + // Each instructions image starts with the Image header and the + // InstructionsSection header. + const intptr_t header_size = + Image::kHeaderSize + compiler::target::InstructionsSection::HeaderSize(); + // There are no instructions until the first InstructionsSection payload. + stream.OffsetFromSymbol(fdes[0].label, 0, kInt32Size); + stream.u4(mach_o::UNWIND_INFO_ENCODING_NONE); + for (intptr_t i = 0, n = fdes.length(); i < n - 1; i++) { + const auto& fde = fdes[i]; + // The payload of the InstructionsSection. + stream.OffsetFromSymbol(fde.label, header_size, kInt32Size); + stream.u4(mach_o::UNWIND_INFO_ENCODING_ARM64_MODE_FRAME); + // The padding (if any) between this Image and the next. + stream.OffsetFromSymbol(fde.label, fde.size, kInt32Size); + stream.u4(mach_o::UNWIND_INFO_ENCODING_NONE); + } + // The payload of the last InstructionsSection. + stream.OffsetFromSymbol(fdes.Last().label, header_size, kInt32Size); + stream.u4(mach_o::UNWIND_INFO_ENCODING_ARM64_MODE_FRAME); + ASSERT_EQUAL(unwind_info_size, stream.Position()); +} +#endif + void MachOHeader::GenerateUnwindingInformation() { #if !defined(TARGET_ARCH_IA32) // Unwinding information is added to the text segment in Mach-O files. @@ -2098,42 +2237,50 @@ void MachOHeader::GenerateUnwindingInformation() { // the Mach-O loader, we don't actually need to generate the instructions, // just use an appropriate zerofill section for it. const bool use_zerofill = type_ == SnapshotType::DebugInfo; - auto const section_type = - use_zerofill ? mach_o::S_ZEROFILL : mach_o::S_REGULAR; + const intptr_t alignment = compiler::target::kWordSize; + auto add_unwind_section = + [&](MachOSegment* segment, const char* sectname, + const ZoneWriteStream& stream, + const SharedObjectWriter::RelocationArray* relocations = nullptr) { + // Not idempotent. + ASSERT(segment->FindSection(sectname) == nullptr); + auto* const section = new (zone()) + MachOSection(zone(), sectname, + use_zerofill ? mach_o::S_ZEROFILL : mach_o::S_REGULAR, + mach_o::S_NO_ATTRIBUTES, !use_zerofill, alignment); + section->AddPortion(use_zerofill ? nullptr : stream.buffer(), + stream.bytes_written(), + use_zerofill ? nullptr : relocations); + segment->AddContents(section); + }; -#if defined(DART_TARGET_OS_MACOS) - // TODO(dartbug.com/60307): Add compact unwind information. - USE(section_type); -#else ASSERT(text_segment_ != nullptr); if (auto* const text_section = text_segment_->FindSection(mach_o::SECT_TEXT)) { - ASSERT(use_zerofill || !text_segment_->HasZerofillSections()); - // Not idempotent. - ASSERT(text_segment_->FindSection(mach_o::SECT_EH_FRAME) == nullptr); - - // For the __eh_frame section, the easiest way to determine the size is to - // generate the contents and just discard them if using zerofill. + // Generate the DWARF FDEs even for MacOS, because the same information + // is used to create the compact unwinding info. GrowableArray fdes(zone_, 0); for (const auto& portion : text_section->portions()) { ASSERT(portion.label != 0); fdes.Add({portion.label, portion.size}); } + // Even if the unwinding information is not written to the output, it is + // generated so a zerofill section of the appropriate size can be created. ZoneWriteStream stream(zone(), DwarfSharedObjectStream::kInitialBufferSize); - DwarfSharedObjectStream dwarf_stream(zone_, &stream); - Dwarf::WriteCallFrameInformationRecords(&dwarf_stream, fdes); + DwarfSharedObjectStream dwarf_stream(zone(), &stream); - auto* const eh_frame = new (zone()) - MachOSection(zone(), mach_o::SECT_EH_FRAME, section_type, - mach_o::S_NO_ATTRIBUTES, /*has_contents=*/!use_zerofill, - /*alignment=*/compiler::target::kWordSize); - eh_frame->AddPortion(use_zerofill ? nullptr : dwarf_stream.buffer(), - dwarf_stream.bytes_written(), - use_zerofill ? nullptr : dwarf_stream.relocations()); - text_segment_->AddContents(eh_frame); +#if defined(DART_TARGET_OS_MACOS) && defined(TARGET_ARCH_ARM64) + GenerateCompactUnwindingInformation(dwarf_stream, fdes); + auto* const sectname = mach_o::SECT_UNWIND_INFO; +#else + Dwarf::WriteCallFrameInformationRecords(&dwarf_stream, fdes); + auto* const sectname = mach_o::SECT_EH_FRAME; +#endif + + add_unwind_section(text_segment_, sectname, stream, + dwarf_stream.relocations()); } -#endif // defined(DART_TARGET_OS_MACOS) #if defined(UNWINDING_RECORDS_WINDOWS_PRECOMPILER) // Append Windows unwinding instructions as a __unwind_info section at @@ -2141,32 +2288,20 @@ void MachOHeader::GenerateUnwindingInformation() { for (auto* const command : commands_) { if (auto* const segment = command->AsMachOSegment()) { if (segment->IsExecutable()) { + // Only more zerofill sections can come after zerofill sections, and + // the unwinding instructions cover the entire executable segment up + // to the unwinding instructions including zerofill sections. ASSERT(use_zerofill || !segment->HasZerofillSections()); - // Not idempotent. - ASSERT(segment->FindSection(mach_o::SECT_UNWIND_INFO) == nullptr); - - auto* const unwinding_records = new (zone()) MachOSection( - zone(), mach_o::SECT_UNWIND_INFO, section_type, - mach_o::S_NO_ATTRIBUTES, - /*has_contents=*/!use_zerofill, compiler::target::kWordSize); const intptr_t records_size = UnwindingRecordsPlatform::SizeInBytes(); - const intptr_t section_start = Utils::RoundUp( - segment->UnpaddedMemorySize(), unwinding_records->Alignment()); - const uint8_t* bytes = nullptr; - if (!use_zerofill) { - ZoneWriteStream stream(zone(), /*initial_size=*/records_size); - uint8_t* unwinding_instructions = - zone()->Alloc(records_size); - stream.WriteBytes(UnwindingRecords::GenerateRecordsInto( - section_start, unwinding_instructions), - records_size); - ASSERT_EQUAL(records_size, stream.Position()); - bytes = stream.buffer(); - } - unwinding_records->AddPortion(bytes, records_size); - segment->AddContents(unwinding_records); - ASSERT_EQUAL(section_start + records_size, - segment->UnpaddedMemorySize()); + ZoneWriteStream stream(zone(), /*initial_size=*/records_size); + uint8_t* unwinding_instructions = zone()->Alloc(records_size); + const intptr_t section_start = + Utils::RoundUp(segment->UnpaddedMemorySize(), alignment); + stream.WriteBytes(UnwindingRecords::GenerateRecordsInto( + section_start, unwinding_instructions), + records_size); + ASSERT_EQUAL(records_size, stream.Position()); + add_unwind_section(segment, mach_o::SECT_UNWIND_INFO, stream); } } }