https://github.com/jasonmolenda created https://github.com/llvm/llvm-project/pull/144627
The "process metadata" LC_NOTE allows for thread IDs to be specified in a Mach-O corefile. This extends the JSON recognzied in that LC_NOTE to allow for additional registers to be supplied on a per-thread basis. The registers included in a Mach-O corefile LC_THREAD load command can only be one of the register flavors that the kernel (xnu) defines in <mach/arm/thread_status.h> for arm64 -- the general purpose registers, floating point registers, exception registers. JTAG style corefile producers may have access to many additional registers beyond these that EL0 programs typically use, for instance TCR_EL1 on AArch64, and people developing low level code need access to these registers. This patch defines a format for including these registers for any thread. The JSON in "process metadata" is a dictionary that must have a `threads` key. The value is an array of entries, one per LC_THREAD in the Mach-O corefile. The number of entries must match the LC_THREADs so they can be correctly associated. Each thread's dictionary must have two keys, `sets`, and `registers`. `sets` is an array of register set names. If a register set name matches one from the LC_THREAD core registers, any registers that are defined will be added to that register set. e.g. metadata can add a register to the "General Purpose Registers" set that lldb shows users. `registers` is an array of dictionaries, one per register. Each register must have the keys `name`, `value`, `bitsize`, and `set`. It may provide additional keys like `alt-name`, that `DynamicRegisterInfo::SetRegisterInfo` recognizes. This `sets` + `registers` formatting is the same that is used by the `target.process.python-os-plugin-path` script interface uses, both are parsed by `DynamicRegisterInfo`. The one addition is that in this LC_NOTE metadata, each register must also have a `value` field, with the value provided in big-endian base 10, as usual with JSON. In RegisterContextUnifiedCore, I combine the register sets & registers from the LC_THREAD for a specific thread, and the metadata sets & registers for that thread from the LC_NOTE. Even if no LC_NOTE is present, this class ingests the LC_THREAD register contexts and reformats it to its internal stores before returning itself as the RegisterContex, instead of shortcutting and returning the core's native RegisterContext. I could have gone either way with that, but in the end I decided if the code is correct, we should live on it always. I added a test where we process save-core to create a userland corefile, then use a utility "add-lcnote" to strip the existing "process metadata" LC_NOTE that lldb put in it, and adds a new one from a JSON string. rdar://74358787 >From 92348b28fb02901e9bbbb437b92c1ddf8cfed31c Mon Sep 17 00:00:00 2001 From: Jason Molenda <jmole...@apple.com> Date: Tue, 17 Jun 2025 18:57:11 -0700 Subject: [PATCH] [lldb][Mach-O] Allow "process metadata" LC_NOTE to supply registers The "process metadata" LC_NOTE allows for thread IDs to be specified in a Mach-O corefile. This extends the JSON recognzied in that LC_NOTE to allow for additional registers to be supplied on a per-thread basis. The registers included in a Mach-O corefile LC_THREAD load command can only be one of the register flavors that the kernel (xnu) defines in <mach/arm/thread_status.h> for arm64 -- the general purpose registers, floating point registers, exception registers. JTAG style corefile producers may have access to many additional registers beyond these that EL0 programs typically use, for instance TCR_EL1 on AArch64, and people developing low level code need access to these registers. This patch defines a format for including these registers for any thread. The JSON in "process metadata" is a dictionary that must have a `threads` key. The value is an array of entries, one per LC_THREAD in the Mach-O corefile. The number of entries must match the LC_THREADs so they can be correctly associated. Each thread's dictionary must have two keys, `sets`, and `registers`. `sets` is an array of register set names. If a register set name matches one from the LC_THREAD core registers, any registers that are defined will be added to that register set. e.g. metadata can add a register to the "General Purpose Registers" set that lldb shows users. `registers` is an array of dictionaries, one per register. Each register must have the keys `name`, `value`, `bitsize`, and `set`. It may provide additional keys like `alt-name`, that `DynamicRegisterInfo::SetRegisterInfo` recognizes. This `sets` + `registers` formatting is the same that is used by the `target.process.python-os-plugin-path` script interface uses, both are parsed by `DynamicRegisterInfo`. The one addition is that in this LC_NOTE metadata, each register must also have a `value` field, with the value provided in big-endian base 10, as usual with JSON. In RegisterContextUnifiedCore, I combine the register sets & registers from the LC_THREAD for a specific thread, and the metadata sets & registers for that thread from the LC_NOTE. Even if no LC_NOTE is present, this class ingests the LC_THREAD register contexts and reformats it to its internal stores before returning itself as the RegisterContex, instead of shortcutting and returning the core's native RegisterContext. I could have gone either way with that, but in the end I decided if the code is correct, we should live on it always. I added a test where we process save-core to create a userland corefile, then use a utility "add-lcnote" to strip the existing "process metadata" LC_NOTE that lldb put in it, and adds a new one from a JSON string. rdar://74358787 --- lldb/include/lldb/Symbol/ObjectFile.h | 17 +- .../ObjectFile/Mach-O/ObjectFileMachO.cpp | 61 ++- .../ObjectFile/Mach-O/ObjectFileMachO.h | 2 + .../Plugins/Process/mach-core/CMakeLists.txt | 1 + .../mach-core/RegisterContextUnifiedCore.cpp | 293 +++++++++++++ .../mach-core/RegisterContextUnifiedCore.h | 57 +++ .../Process/mach-core/ThreadMachCore.cpp | 55 ++- .../lc-note/additional-registers/Makefile | 11 + .../TestMetadataRegisters.py | 100 +++++ .../additional-registers/add-lcnote.cpp | 384 ++++++++++++++++++ .../lc-note/additional-registers/main.c | 11 + 11 files changed, 957 insertions(+), 35 deletions(-) create mode 100644 lldb/source/Plugins/Process/mach-core/RegisterContextUnifiedCore.cpp create mode 100644 lldb/source/Plugins/Process/mach-core/RegisterContextUnifiedCore.h create mode 100644 lldb/test/API/macosx/lc-note/additional-registers/Makefile create mode 100644 lldb/test/API/macosx/lc-note/additional-registers/TestMetadataRegisters.py create mode 100644 lldb/test/API/macosx/lc-note/additional-registers/add-lcnote.cpp create mode 100644 lldb/test/API/macosx/lc-note/additional-registers/main.c diff --git a/lldb/include/lldb/Symbol/ObjectFile.h b/lldb/include/lldb/Symbol/ObjectFile.h index 43567592dd447..1b9ae1fb31a69 100644 --- a/lldb/include/lldb/Symbol/ObjectFile.h +++ b/lldb/include/lldb/Symbol/ObjectFile.h @@ -18,6 +18,7 @@ #include "lldb/Utility/Endian.h" #include "lldb/Utility/FileSpec.h" #include "lldb/Utility/FileSpecList.h" +#include "lldb/Utility/StructuredData.h" #include "lldb/Utility/UUID.h" #include "lldb/lldb-private.h" #include "llvm/Support/Threading.h" @@ -544,9 +545,9 @@ class ObjectFile : public std::enable_shared_from_this<ObjectFile>, return false; } - /// Get metadata about threads from the corefile. + /// Get metadata about thread ids from the corefile. /// - /// The corefile may have metadata (e.g. a Mach-O "thread extrainfo" + /// The corefile may have metadata (e.g. a Mach-O "process metadata" /// LC_NOTE) which for the threads in the process; this method tries /// to retrieve them. /// @@ -568,6 +569,18 @@ class ObjectFile : public std::enable_shared_from_this<ObjectFile>, return false; } + /// Get process metadata from the corefile in a StructuredData dictionary. + /// + /// The corefile may have notes (e.g. a Mach-O "process metadata" LC_NOTE) + /// which provide metadata about the process and threads in a JSON or + /// similar format. + /// + /// \return + /// A StructuredData object with the metadata in the note, if there is + /// one. An empty shared pointer is returned if not metadata is found, + /// or a problem parsing it. + virtual StructuredData::ObjectSP GetCorefileProcessMetadata() { return {}; } + virtual lldb::RegisterContextSP GetThreadContextAtIndex(uint32_t idx, lldb_private::Thread &thread) { return lldb::RegisterContextSP(); diff --git a/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.cpp b/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.cpp index b1741926b74aa..aea45293095e7 100644 --- a/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.cpp +++ b/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.cpp @@ -5794,27 +5794,9 @@ bool ObjectFileMachO::GetCorefileThreadExtraInfos( std::lock_guard<std::recursive_mutex> guard(module_sp->GetMutex()); Log *log(GetLog(LLDBLog::Object | LLDBLog::Process | LLDBLog::Thread)); - auto lc_notes = FindLC_NOTEByName("process metadata"); - for (auto lc_note : lc_notes) { - offset_t payload_offset = std::get<0>(lc_note); - offset_t strsize = std::get<1>(lc_note); - std::string buf(strsize, '\0'); - if (m_data.CopyData(payload_offset, strsize, buf.data()) != strsize) { - LLDB_LOGF(log, - "Unable to read %" PRIu64 - " bytes of 'process metadata' LC_NOTE JSON contents", - strsize); - return false; - } - while (buf.back() == '\0') - buf.resize(buf.size() - 1); - StructuredData::ObjectSP object_sp = StructuredData::ParseJSON(buf); + StructuredData::ObjectSP object_sp = GetCorefileProcessMetadata(); + if (object_sp) { StructuredData::Dictionary *dict = object_sp->GetAsDictionary(); - if (!dict) { - LLDB_LOGF(log, "Unable to read 'process metadata' LC_NOTE, did not " - "get a dictionary."); - return false; - } StructuredData::Array *threads; if (!dict->GetValueForKeyAsArray("threads", threads) || !threads) { LLDB_LOGF(log, @@ -5857,6 +5839,45 @@ bool ObjectFileMachO::GetCorefileThreadExtraInfos( return false; } +StructuredData::ObjectSP ObjectFileMachO::GetCorefileProcessMetadata() { + ModuleSP module_sp(GetModule()); + if (!module_sp) + return {}; + + Log *log(GetLog(LLDBLog::Object | LLDBLog::Process | LLDBLog::Thread)); + std::lock_guard<std::recursive_mutex> guard(module_sp->GetMutex()); + auto lc_notes = FindLC_NOTEByName("process metadata"); + if (lc_notes.size() == 0) + return {}; + + if (lc_notes.size() > 1) + LLDB_LOGF( + log, + "Multiple 'process metadata' LC_NOTEs found, only using the first."); + + offset_t payload_offset = std::get<0>(lc_notes[0]); + offset_t strsize = std::get<1>(lc_notes[0]); + std::string buf(strsize, '\0'); + if (m_data.CopyData(payload_offset, strsize, buf.data()) != strsize) { + LLDB_LOGF(log, + "Unable to read %" PRIu64 + " bytes of 'process metadata' LC_NOTE JSON contents", + strsize); + return {}; + } + while (buf.back() == '\0') + buf.resize(buf.size() - 1); + StructuredData::ObjectSP object_sp = StructuredData::ParseJSON(buf); + StructuredData::Dictionary *dict = object_sp->GetAsDictionary(); + if (!dict) { + LLDB_LOGF(log, "Unable to read 'process metadata' LC_NOTE, did not " + "get a dictionary."); + return {}; + } + + return object_sp; +} + lldb::RegisterContextSP ObjectFileMachO::GetThreadContextAtIndex(uint32_t idx, lldb_private::Thread &thread) { diff --git a/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.h b/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.h index 7f67f5e04f1d6..7e3a6754dd0b8 100644 --- a/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.h +++ b/lldb/source/Plugins/ObjectFile/Mach-O/ObjectFileMachO.h @@ -133,6 +133,8 @@ class ObjectFileMachO : public lldb_private::ObjectFile { bool GetCorefileThreadExtraInfos(std::vector<lldb::tid_t> &tids) override; + lldb_private::StructuredData::ObjectSP GetCorefileProcessMetadata() override; + bool LoadCoreFileImages(lldb_private::Process &process) override; lldb::RegisterContextSP diff --git a/lldb/source/Plugins/Process/mach-core/CMakeLists.txt b/lldb/source/Plugins/Process/mach-core/CMakeLists.txt index a1ea85ec4c728..926f06d23c64b 100644 --- a/lldb/source/Plugins/Process/mach-core/CMakeLists.txt +++ b/lldb/source/Plugins/Process/mach-core/CMakeLists.txt @@ -1,6 +1,7 @@ add_lldb_library(lldbPluginProcessMachCore PLUGIN ProcessMachCore.cpp ThreadMachCore.cpp + RegisterContextUnifiedCore.cpp LINK_COMPONENTS Support diff --git a/lldb/source/Plugins/Process/mach-core/RegisterContextUnifiedCore.cpp b/lldb/source/Plugins/Process/mach-core/RegisterContextUnifiedCore.cpp new file mode 100644 index 0000000000000..774c0ca605b3c --- /dev/null +++ b/lldb/source/Plugins/Process/mach-core/RegisterContextUnifiedCore.cpp @@ -0,0 +1,293 @@ +//===-- RegisterContextUnifiedCore.cpp ------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "RegisterContextUnifiedCore.h" +#include "lldb/Target/DynamicRegisterInfo.h" +#include "lldb/Target/Process.h" +#include "lldb/Utility/DataExtractor.h" +#include "lldb/Utility/RegisterValue.h" +#include "lldb/Utility/StructuredData.h" + +using namespace lldb; +using namespace lldb_private; + +RegisterContextUnifiedCore::RegisterContextUnifiedCore( + Thread &thread, uint32_t concrete_frame_idx, + RegisterContextSP core_thread_regctx_sp, + StructuredData::ObjectSP metadata_thread_registers) + : RegisterContext(thread, concrete_frame_idx) { + + ProcessSP process_sp(thread.GetProcess()); + Target &target = process_sp->GetTarget(); + StructuredData::Dictionary *metadata_registers_dict = nullptr; + + // If we have thread metadata, check if the keys for register + // definitions are present; if not, clear the ObjectSP. + if (metadata_thread_registers && + metadata_thread_registers->GetAsDictionary()->HasKey("register_info")) { + metadata_registers_dict = metadata_thread_registers->GetAsDictionary() + ->GetValueForKey("register_info") + ->GetAsDictionary(); + if (metadata_registers_dict) + if (!metadata_registers_dict->HasKey("sets") || + !metadata_registers_dict->HasKey("registers")) + metadata_registers_dict = nullptr; + } + + // When creating a register set list from the two sources, + // the LC_THREAD aka core_thread_regctx_sp register sets + // will be used at the same indexes. + // Any additional sets named by the thread metadata registers + // will be added. If the thread metadata registers specify + // a set with the same name, the already-used index from the + // core register context will be used. + std::map<size_t, size_t> metadata_regset_to_combined_regset; + + // Calculate the total size of the register store buffer we need + // for all registers. The corefile register definitions may include + // RegisterInfo descriptions of registers that aren't actually + // available. For simplicity, calculate the size of all registers + // as if they are available, so we can maintain the same offsets into + // the buffer. + uint32_t core_buffer_end = 0; + for (size_t idx = 0; idx < core_thread_regctx_sp->GetRegisterCount(); idx++) { + const RegisterInfo *reginfo = + core_thread_regctx_sp->GetRegisterInfoAtIndex(idx); + core_buffer_end = + std::max(reginfo->byte_offset + reginfo->byte_size, core_buffer_end); + } + + // Add metadata register sizes to the total buffer size. + uint32_t combined_buffer_end = core_buffer_end; + if (metadata_registers_dict) { + StructuredData::Array *registers = nullptr; + if (metadata_registers_dict->GetValueForKeyAsArray("registers", registers)) + registers->ForEach( + [&combined_buffer_end](StructuredData::Object *ent) -> bool { + uint32_t bitsize; + if (!ent->GetAsDictionary()->GetValueForKeyAsInteger("bitsize", + bitsize)) + return false; + combined_buffer_end += (bitsize / 8); + return true; + }); + } + m_register_data.resize(combined_buffer_end, 0); + + // Copy the core register values into our combined data buffer, + // skip registers that are contained within another (e.g. w0 vs. x0) + // and registers that return as "unavailable". + for (size_t idx = 0; idx < core_thread_regctx_sp->GetRegisterCount(); idx++) { + const RegisterInfo *reginfo = + core_thread_regctx_sp->GetRegisterInfoAtIndex(idx); + RegisterValue val; + if (!reginfo->value_regs && + core_thread_regctx_sp->ReadRegister(reginfo, val)) + memcpy(m_register_data.data() + reginfo->byte_offset, val.GetBytes(), + val.GetByteSize()); + } + + // Set 'offset' fields for each register definition into our combined + // register data buffer. DynamicRegisterInfo needs + // this field set to parse the JSON. + // Also copy the values of the registers into our register data buffer. + if (metadata_registers_dict) { + size_t offset = core_buffer_end; + ByteOrder byte_order = core_thread_regctx_sp->GetByteOrder(); + StructuredData::Array *registers; + if (metadata_registers_dict->GetValueForKeyAsArray("registers", registers)) + registers->ForEach([this, &offset, + byte_order](StructuredData::Object *ent) -> bool { + uint64_t bitsize; + uint64_t value; + if (!ent->GetAsDictionary()->GetValueForKeyAsInteger("bitsize", + bitsize)) + return false; + if (!ent->GetAsDictionary()->GetValueForKeyAsInteger("value", value)) { + // we had a bitsize but no value, so move the offset forward I guess. + offset += (bitsize / 8); + return false; + } + ent->GetAsDictionary()->AddIntegerItem("offset", offset); + Status error; + switch (bitsize / 8) { + case 2: { + Scalar value_scalar((uint16_t)value); + value_scalar.GetAsMemoryData(m_register_data.data() + offset, 2, + byte_order, error); + offset += 2; + } break; + case 4: { + Scalar value_scalar((uint32_t)value); + value_scalar.GetAsMemoryData(m_register_data.data() + offset, 4, + byte_order, error); + offset += 4; + } break; + case 8: { + Scalar value_scalar((uint64_t)value); + value_scalar.GetAsMemoryData(m_register_data.data() + offset, 8, + byte_order, error); + offset += 8; + } break; + } + return true; + }); + } + + // Create a DynamicRegisterInfo from the metadata JSON. + std::unique_ptr<DynamicRegisterInfo> additional_reginfo_up; + if (metadata_registers_dict) + additional_reginfo_up = DynamicRegisterInfo::Create( + *metadata_registers_dict, target.GetArchitecture()); + + // Copy the core thread register sets into our combined register set list. + for (size_t idx = 0; idx < core_thread_regctx_sp->GetRegisterSetCount(); + idx++) { + RegisterSet new_set; + const RegisterSet *old_set = core_thread_regctx_sp->GetRegisterSet(idx); + new_set.name = ConstString(old_set->name).AsCString(); + if (old_set->short_name) + new_set.short_name = ConstString(old_set->short_name).AsCString(); + else + new_set.short_name = nullptr; + m_register_sets.push_back(new_set); + } + + // Set up our RegisterSet array. + // First copying all of the core thread register sets, + // then any additional unique register sets from the metadata. + if (additional_reginfo_up) { + for (size_t idx = 0; idx < additional_reginfo_up->GetNumRegisterSets(); + idx++) { + bool found_match = false; + const RegisterSet *old_set = additional_reginfo_up->GetRegisterSet(idx); + for (size_t jdx = 0; jdx < m_register_sets.size(); jdx++) { + if (strcmp(m_register_sets[jdx].name, old_set->name) == 0) { + metadata_regset_to_combined_regset[idx] = jdx; + found_match = true; + break; + } + } + if (!found_match) { + RegisterSet new_set; + new_set.name = ConstString(old_set->name).AsCString(); + if (old_set->short_name) + new_set.short_name = ConstString(old_set->short_name).AsCString(); + else + new_set.short_name = nullptr; + metadata_regset_to_combined_regset[idx] = m_register_sets.size(); + m_register_sets.push_back(new_set); + } + } + } + + // Set up our RegisterInfo array, one RegisterSet at a time. + // The metadata registers may be declared to be in a core thread + // register set (e.g. "General Purpose Registers", so we scan + // both core registers and metadata registers should be examined + // when creating the combined register sets. + for (size_t combined_regset_idx = 0; + combined_regset_idx < m_register_sets.size(); combined_regset_idx++) { + uint32_t registers_this_regset = 0; + if (combined_regset_idx < core_thread_regctx_sp->GetRegisterSetCount()) { + const RegisterSet *regset = + core_thread_regctx_sp->GetRegisterSet(combined_regset_idx); + for (size_t j = 0; j < regset->num_registers; j++) { + uint32_t reg_idx = regset->registers[j]; + const RegisterInfo *reginfo = + core_thread_regctx_sp->GetRegisterInfoAtIndex(reg_idx); + RegisterValue val; + if (!reginfo->value_regs && + core_thread_regctx_sp->ReadRegister(reginfo, val)) { + m_regset_regnum_collection[combined_regset_idx].push_back( + m_register_infos.size()); + m_register_infos.push_back(*reginfo); + registers_this_regset++; + } + } + } + if (additional_reginfo_up) { + // Find the register set in the metadata that matches this register + // set, then copy all its RegisterInfos. + for (size_t setidx = 0; + setidx < additional_reginfo_up->GetNumRegisterSets(); setidx++) { + if (metadata_regset_to_combined_regset[setidx] == combined_regset_idx) { + const RegisterSet *regset = + additional_reginfo_up->GetRegisterSet(setidx); + for (size_t j = 0; j < regset->num_registers; j++) { + uint32_t reg_idx = regset->registers[j]; + const RegisterInfo *reginfo = + additional_reginfo_up->GetRegisterInfoAtIndex(reg_idx); + m_regset_regnum_collection[combined_regset_idx].push_back( + m_register_infos.size()); + // register names in DynamicRegisterInfo are ConstString stored; + // we can reuse the char* pointers here without retaining the + // DynamicRegisterInfo. + m_register_infos.push_back(*reginfo); + registers_this_regset++; + } + } + } + } + m_register_sets[combined_regset_idx].num_registers = registers_this_regset; + m_register_sets[combined_regset_idx].registers = + m_regset_regnum_collection[combined_regset_idx].data(); + } +} + +size_t RegisterContextUnifiedCore::GetRegisterCount() { + return m_register_infos.size(); +} + +const RegisterInfo * +RegisterContextUnifiedCore::GetRegisterInfoAtIndex(size_t reg) { + return &m_register_infos[reg]; +} + +size_t RegisterContextUnifiedCore::GetRegisterSetCount() { + return m_register_sets.size(); +} + +const RegisterSet *RegisterContextUnifiedCore::GetRegisterSet(size_t set) { + return &m_register_sets[set]; +} + +bool RegisterContextUnifiedCore::ReadRegister( + const lldb_private::RegisterInfo *reg_info, + lldb_private::RegisterValue &value) { + if (!reg_info) + return false; + ProcessSP process_sp(m_thread.GetProcess()); + if (process_sp) { + DataExtractor regdata(m_register_data.data(), m_register_data.size(), + process_sp->GetByteOrder(), + process_sp->GetAddressByteSize()); + offset_t offset = reg_info->byte_offset; + switch (reg_info->byte_size) { + case 2: + value.SetUInt16(regdata.GetU16(&offset)); + break; + case 4: + value.SetUInt32(regdata.GetU32(&offset)); + break; + case 8: + value.SetUInt64(regdata.GetU64(&offset)); + break; + default: + return false; + } + return true; + } + return false; +} + +bool RegisterContextUnifiedCore::WriteRegister( + const lldb_private::RegisterInfo *reg_info, + const lldb_private::RegisterValue &value) { + return false; +} diff --git a/lldb/source/Plugins/Process/mach-core/RegisterContextUnifiedCore.h b/lldb/source/Plugins/Process/mach-core/RegisterContextUnifiedCore.h new file mode 100644 index 0000000000000..47e2fb7cac5a3 --- /dev/null +++ b/lldb/source/Plugins/Process/mach-core/RegisterContextUnifiedCore.h @@ -0,0 +1,57 @@ +//===-- RegisterContextUnifiedCore.h ----------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_PLUGINS_PROCESS_REGISTERCONTEXT_UNIFIED_CORE_H +#define LLDB_SOURCE_PLUGINS_PROCESS_REGISTERCONTEXT_UNIFIED_CORE_H + +#include <string> +#include <vector> + +#include "lldb/Target/RegisterContext.h" +#include "lldb/Utility/ConstString.h" +#include "lldb/Utility/StructuredData.h" +#include "lldb/lldb-enumerations.h" +#include "lldb/lldb-private.h" + +namespace lldb_private { + +class RegisterContextUnifiedCore : public RegisterContext { +public: + RegisterContextUnifiedCore( + Thread &thread, uint32_t concrete_frame_idx, + lldb::RegisterContextSP core_thread_regctx_sp, + lldb_private::StructuredData::ObjectSP metadata_thread_registers); + + void InvalidateAllRegisters() override{}; + + size_t GetRegisterCount() override; + + const lldb_private::RegisterInfo *GetRegisterInfoAtIndex(size_t reg) override; + + size_t GetRegisterSetCount() override; + + const lldb_private::RegisterSet *GetRegisterSet(size_t set) override; + + bool ReadRegister(const lldb_private::RegisterInfo *reg_info, + lldb_private::RegisterValue &value) override; + + bool WriteRegister(const lldb_private::RegisterInfo *reg_info, + const lldb_private::RegisterValue &value) override; + +private: + std::vector<lldb_private::RegisterSet> m_register_sets; + std::vector<lldb_private::RegisterInfo> m_register_infos; + // For each register set, an array of register numbers included. + std::map<size_t, std::vector<uint32_t>> m_regset_regnum_collection; + // Bytes of the register contents. + std::vector<uint8_t> m_register_data; +}; + +} // namespace lldb_private + +#endif // LLDB_SOURCE_PLUGINS_PROCESS_REGISTERCONTEXT_UNIFIED_CORE_H diff --git a/lldb/source/Plugins/Process/mach-core/ThreadMachCore.cpp b/lldb/source/Plugins/Process/mach-core/ThreadMachCore.cpp index 003cdd0fcaa57..0bb9f4da9bc92 100644 --- a/lldb/source/Plugins/Process/mach-core/ThreadMachCore.cpp +++ b/lldb/source/Plugins/Process/mach-core/ThreadMachCore.cpp @@ -6,12 +6,18 @@ // //===----------------------------------------------------------------------===// +#include <optional> +#include <string> +#include <vector> + +#include "RegisterContextUnifiedCore.h" #include "ThreadMachCore.h" #include "lldb/Breakpoint/Watchpoint.h" #include "lldb/Host/SafeMachO.h" #include "lldb/Symbol/ObjectFile.h" #include "lldb/Target/AppleArm64ExceptionClass.h" +#include "lldb/Target/DynamicRegisterInfo.h" #include "lldb/Target/Process.h" #include "lldb/Target/RegisterContext.h" #include "lldb/Target/StopInfo.h" @@ -22,6 +28,7 @@ #include "lldb/Utility/RegisterValue.h" #include "lldb/Utility/State.h" #include "lldb/Utility/StreamString.h" +#include "lldb/Utility/StructuredData.h" #include "ProcessMachCore.h" //#include "RegisterContextKDP_arm.h" @@ -70,27 +77,49 @@ lldb::RegisterContextSP ThreadMachCore::GetRegisterContext() { lldb::RegisterContextSP ThreadMachCore::CreateRegisterContextForFrame(StackFrame *frame) { - lldb::RegisterContextSP reg_ctx_sp; uint32_t concrete_frame_idx = 0; if (frame) concrete_frame_idx = frame->GetConcreteFrameIndex(); + if (concrete_frame_idx > 0) + return GetUnwinder().CreateRegisterContextForFrame(frame); + + if (m_thread_reg_ctx_sp) + return m_thread_reg_ctx_sp; - if (concrete_frame_idx == 0) { - if (!m_thread_reg_ctx_sp) { - ProcessSP process_sp(GetProcess()); + ProcessSP process_sp(GetProcess()); - ObjectFile *core_objfile = - static_cast<ProcessMachCore *>(process_sp.get())->GetCoreObjectFile(); - if (core_objfile) - m_thread_reg_ctx_sp = core_objfile->GetThreadContextAtIndex( - m_objfile_lc_thread_idx, *this); + ObjectFile *core_objfile = + static_cast<ProcessMachCore *>(process_sp.get())->GetCoreObjectFile(); + if (!core_objfile) + return {}; + + RegisterContextSP core_thread_regctx_sp = + core_objfile->GetThreadContextAtIndex(m_objfile_lc_thread_idx, *this); + + if (!core_thread_regctx_sp) + return {}; + + StructuredData::ObjectSP process_md_sp = + core_objfile->GetCorefileProcessMetadata(); + + StructuredData::ObjectSP thread_md_sp; + if (process_md_sp && process_md_sp->GetAsDictionary() && + process_md_sp->GetAsDictionary()->HasKey("threads")) { + StructuredData::Array *threads = process_md_sp->GetAsDictionary() + ->GetValueForKey("threads") + ->GetAsArray(); + if (threads) { + StructuredData::ObjectSP thread_sp = + threads->GetItemAtIndex(m_objfile_lc_thread_idx); + if (thread_sp && thread_sp->GetAsDictionary()) + thread_md_sp = thread_sp; } - reg_ctx_sp = m_thread_reg_ctx_sp; - } else { - reg_ctx_sp = GetUnwinder().CreateRegisterContextForFrame(frame); } - return reg_ctx_sp; + m_thread_reg_ctx_sp = std::make_shared<RegisterContextUnifiedCore>( + *this, concrete_frame_idx, core_thread_regctx_sp, thread_md_sp); + + return m_thread_reg_ctx_sp; } static bool IsCrashExceptionClass(AppleArm64ExceptionClass EC) { diff --git a/lldb/test/API/macosx/lc-note/additional-registers/Makefile b/lldb/test/API/macosx/lc-note/additional-registers/Makefile new file mode 100644 index 0000000000000..e232ec5d69675 --- /dev/null +++ b/lldb/test/API/macosx/lc-note/additional-registers/Makefile @@ -0,0 +1,11 @@ +MAKE_DSYM := NO +C_SOURCES := main.c +CXXFLAGS_EXTRAS := -std=c++17 + +all: a.out add-lcnote + +add-lcnote: + "$(MAKE)" -f "$(MAKEFILE_RULES)" EXE=add-lcnote \ + CXX=$(CC) CXXFLAGS_EXTRAS="$(CXXFLAGS_EXTRAS)" CXX_SOURCES=add-lcnote.cpp + +include Makefile.rules diff --git a/lldb/test/API/macosx/lc-note/additional-registers/TestMetadataRegisters.py b/lldb/test/API/macosx/lc-note/additional-registers/TestMetadataRegisters.py new file mode 100644 index 0000000000000..104f79b62865c --- /dev/null +++ b/lldb/test/API/macosx/lc-note/additional-registers/TestMetadataRegisters.py @@ -0,0 +1,100 @@ +"""Test that lldb will read additional registers from Mach-O LC_NOTE metadata.""" + +import os +import re +import subprocess + +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil + + +class TestMetadataRegisters(TestBase): + NO_DEBUG_INFO_TESTCASE = True + + @skipUnlessDarwin + @skipIfRemote + def test_add_registers_via_metadata(self): + self.build() + self.aout_exe = self.getBuildArtifact("a.out") + lldb_corefile = self.getBuildArtifact("lldb.core") + metadata_corefile = self.getBuildArtifact("metadata.core") + add_lcnote = self.getBuildArtifact("add-lcnote") + + (target, process, t, bp) = lldbutil.run_to_source_breakpoint( + self, "break here", lldb.SBFileSpec("main.c") + ) + + self.assertTrue(process.IsValid()) + + if self.TraceOn(): + self.runCmd("bt") + self.runCmd("reg read -a") + + self.runCmd("process save-core " + lldb_corefile) + process.Kill() + target.Clear() + + cmd = ( + add_lcnote + + " " + + "-r" + + " " + + "-i '%s'" % lldb_corefile + + " " + + "-o '%s'" % metadata_corefile + + " " + + "-n 'process metadata' " + + "-s '" + + """{"threads":[{"register_info": + {"sets":["Special Registers", "General Purpose Registers"], + "registers":[ + {"name":"jar", "value":10, "bitsize": 32, "set": 0}, + {"name":"bar", "value":65537, "bitsize":16, "set":0}, + {"name":"mar", "value":65537, "bitsize":32, "set":0}, + {"name":"anotherpc", "value":55, "bitsize":64, "set": 1}]}}]}""" + + "'" + ) + + print(cmd) + call(cmd, shell=True) + + # Now load the corefile + target = self.dbg.CreateTarget("") + process = target.LoadCore(metadata_corefile) + self.assertTrue(process.IsValid()) + if self.TraceOn(): + self.runCmd("bt") + self.runCmd("reg read -a") + + thread = process.GetSelectedThread() + frame = thread.GetFrameAtIndex(0) + + # Register sets will be + # from LC_THREAD: + # General Purpose Registers + # Floating Point Registers + # Exception State Registers + # from LC_NOTE metadata: + # Special Registers + self.assertEqual(frame.registers[0].GetName(), "General Purpose Registers") + self.assertEqual(frame.registers[3].GetName(), "Special Registers") + + anotherpc = frame.registers[0].GetChildMemberWithName("anotherpc") + self.assertTrue(anotherpc.IsValid()) + self.assertEqual(anotherpc.GetValueAsUnsigned(), 0x37) + + jar = frame.registers[3].GetChildMemberWithName("jar") + self.assertTrue(jar.IsValid()) + self.assertEqual(jar.GetValueAsUnsigned(), 10) + self.assertEqual(jar.GetByteSize(), 4) + + bar = frame.registers[3].GetChildMemberWithName("bar") + self.assertTrue(bar.IsValid()) + self.assertEqual(bar.GetByteSize(), 2) + + mar = frame.registers[3].GetChildMemberWithName("mar") + self.assertTrue(mar.IsValid()) + self.assertEqual(mar.GetValueAsUnsigned(), 0x10001) + self.assertEqual(mar.GetByteSize(), 4) diff --git a/lldb/test/API/macosx/lc-note/additional-registers/add-lcnote.cpp b/lldb/test/API/macosx/lc-note/additional-registers/add-lcnote.cpp new file mode 100644 index 0000000000000..8d9d550a9ad72 --- /dev/null +++ b/lldb/test/API/macosx/lc-note/additional-registers/add-lcnote.cpp @@ -0,0 +1,384 @@ +#include <getopt.h> +#include <mach-o/loader.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> + +#include <iostream> +#include <optional> +#include <string> + +using namespace std; + +[[noreturn]] void print_help(void) { + fprintf(stderr, "Append an LC_NOTE to a corefile. Usage: \n"); + fprintf(stderr, " -i|--input <corefile>\n"); + fprintf(stderr, " -o|--output <corefile>\n"); + fprintf(stderr, " -n|--name <LC_NOTE name>\n"); + fprintf( + stderr, + " -r|--remove-dups remove existing LC_NOTEs with this same name\n"); + fprintf(stderr, " One of:\n"); + fprintf(stderr, " -f|--file <file to embed as LC_NOTE payload>\n"); + fprintf(stderr, " -s|--str <string to embed as LC_NOTE payload>\n"); + exit(1); +} + +void parse_args(int argc, char **argv, string &infile, string &outfile, + string ¬e_name, vector<uint8_t> &payload, + bool &remove_dups) { + const char *const short_opts = "i:o:n:f:s:hr"; + const option long_opts[] = {{"input", required_argument, nullptr, 'i'}, + {"output", required_argument, nullptr, 'o'}, + {"name", required_argument, nullptr, 'n'}, + {"file", required_argument, nullptr, 'f'}, + {"str", required_argument, nullptr, 's'}, + {"remove-dups", no_argument, nullptr, 'r'}, + {"help", no_argument, nullptr, 'h'}, + {nullptr, no_argument, nullptr, 0}}; + optional<string> infile_str, outfile_str, name_str, payload_file_str, + payload_str; + remove_dups = false; + while (true) { + const auto opt = getopt_long(argc, argv, short_opts, long_opts, nullptr); + if (opt == -1) + break; + switch (opt) { + case 'i': + infile_str = optarg; + break; + case 'o': + outfile_str = optarg; + break; + case 'n': + name_str = optarg; + break; + case 'f': + payload_file_str = optarg; + break; + case 's': + payload_str = optarg; + break; + case 'r': + remove_dups = true; + break; + case 'h': + print_help(); + } + } + + if (!infile_str || !outfile_str || !name_str || + (!payload_file_str && !payload_str)) + print_help(); + + infile = *infile_str; + outfile = *outfile_str; + note_name = *name_str; + if (payload_str) { + payload.resize(payload_str->size(), 0); + memcpy(payload.data(), payload_str->c_str(), payload_str->size()); + } else { + struct stat sb; + if (stat(payload_file_str->c_str(), &sb)) { + fprintf(stderr, "File '%s' does not exist.\n", payload_file_str->c_str()); + exit(1); + } + payload.resize(sb.st_size, 0); + FILE *f = fopen(payload_file_str->c_str(), "r"); + fread(payload.data(), 1, sb.st_size, f); + fclose(f); + } +} + +struct all_image_infos_header { + uint32_t version; // currently 1 + uint32_t imgcount; // number of binary images + uint64_t entries_fileoff; // file offset in the corefile of where the array of + // struct entry's begin. + uint32_t entry_size; // size of 'struct entry'. + uint32_t unused; // set to 0 +}; + +struct image_entry { + uint64_t filepath_offset; // corefile offset of the c-string filepath, + // if available, else this should be set + // to UINT64_MAX. + uuid_t uuid; // uint8_t[16]. should be set to all zeroes if + // uuid is unknown. + uint64_t + load_address; // virtual addr of mach-o header, UINT64_MAX if unknown. + uint64_t seg_addrs_offset; // corefile offset to the array of struct + // segment_vmaddr's, UINT64_MAX if none. + uint32_t segment_count; // The number of segments for this binary, 0 if none. + uint32_t + executing; // Set to 0 if executing status is unknown by corefile + // creator. + // Set to 1 if this binary was executing on any thread, + // so it can be force-loaded by the corefile reader. + // Set to 2 if this binary was not executing on any thread. +}; + +int count_lc_notes_with_name(FILE *in, std::string name) { + fseeko(in, 0, SEEK_SET); + + uint8_t magic[4]; + if (fread(magic, 1, 4, in) != 4) { + printf("Failed to read magic number\n"); + return 0; + } + uint8_t magic_32_le[] = {0xce, 0xfa, 0xed, 0xfe}; + uint8_t magic_64_le[] = {0xcf, 0xfa, 0xed, 0xfe}; + + if (memcmp(magic, magic_32_le, 4) != 0 && + memcmp(magic, magic_64_le, 4) != 0) { + return 0; + } + + fseeko(in, 0, SEEK_SET); + + int number_of_load_cmds = 0; + size_t size_of_mach_header = 0; + if (memcmp(magic, magic_64_le, 4) == 0) { + struct mach_header_64 mh; + size_of_mach_header = sizeof(mh); + if (fread(&mh, sizeof(mh), 1, in) != 1) { + fprintf(stderr, "unable to read mach header\n"); + return 0; + } + number_of_load_cmds = mh.ncmds; + } else { + struct mach_header mh; + size_of_mach_header = sizeof(mh); + if (fread(&mh, sizeof(mh), 1, in) != 1) { + fprintf(stderr, "unable to read mach header\n"); + return 0; + } + number_of_load_cmds = mh.ncmds; + } + + int notes_seen = 0; + fseeko(in, size_of_mach_header, SEEK_SET); + for (int i = 0; i < number_of_load_cmds; i++) { + off_t cmd_start = ftello(in); + uint32_t cmd, cmdsize; + fread(&cmd, sizeof(uint32_t), 1, in); + fread(&cmdsize, sizeof(uint32_t), 1, in); + + fseeko(in, cmd_start, SEEK_SET); + off_t next_cmd = cmd_start + cmdsize; + if (cmd == LC_NOTE) { + struct note_command note; + fread(¬e, sizeof(note), 1, in); + if (strncmp(name.c_str(), note.data_owner, 16) == 0) + notes_seen++; + } + fseeko(in, next_cmd, SEEK_SET); + } + return notes_seen; +} + +void copy_and_add_note(FILE *in, FILE *out, std::string lc_note_name, + vector<uint8_t> payload_data, bool remove_dups) { + int number_of_load_cmds = 0; + off_t header_start = ftello(in); + + int notes_to_remove = 0; + if (remove_dups) + notes_to_remove = count_lc_notes_with_name(in, lc_note_name); + fseeko(in, header_start, SEEK_SET); + + uint8_t magic[4]; + if (fread(magic, 1, 4, in) != 4) { + printf("Failed to read magic number\n"); + return; + } + uint8_t magic_32_le[] = {0xce, 0xfa, 0xed, 0xfe}; + uint8_t magic_64_le[] = {0xcf, 0xfa, 0xed, 0xfe}; + + if (memcmp(magic, magic_32_le, 4) != 0 && + memcmp(magic, magic_64_le, 4) != 0) { + return; + } + + fseeko(in, header_start, SEEK_SET); + + off_t end_of_infine_loadcmds; + size_t size_of_mach_header = 0; + if (memcmp(magic, magic_64_le, 4) == 0) { + struct mach_header_64 mh; + size_of_mach_header = sizeof(mh); + if (fread(&mh, sizeof(mh), 1, in) != 1) { + fprintf(stderr, "unable to read mach header\n"); + return; + } + number_of_load_cmds = mh.ncmds; + end_of_infine_loadcmds = sizeof(mh) + mh.sizeofcmds; + + mh.ncmds += 1; + mh.ncmds -= notes_to_remove; + mh.sizeofcmds += sizeof(struct note_command); + mh.sizeofcmds -= notes_to_remove * sizeof(struct note_command); + fseeko(out, header_start, SEEK_SET); + fwrite(&mh, sizeof(mh), 1, out); + } else { + struct mach_header mh; + size_of_mach_header = sizeof(mh); + if (fread(&mh, sizeof(mh), 1, in) != 1) { + fprintf(stderr, "unable to read mach header\n"); + return; + } + number_of_load_cmds = mh.ncmds; + end_of_infine_loadcmds = sizeof(mh) + mh.sizeofcmds; + + mh.ncmds += 1; + mh.ncmds -= notes_to_remove; + mh.sizeofcmds += sizeof(struct note_command); + mh.sizeofcmds -= notes_to_remove * sizeof(struct note_command); + fseeko(out, header_start, SEEK_SET); + fwrite(&mh, sizeof(mh), 1, out); + } + + off_t start_of_infile_load_cmds = ftello(in); + fseek(in, 0, SEEK_END); + off_t infile_size = ftello(in); + + // LC_SEGMENT may be aligned to 4k boundaries, let's maintain + // that alignment by putting 4096 minus the size of the added + // LC_NOTE load command after the output file's load commands. + off_t end_of_outfile_loadcmds = + end_of_infine_loadcmds - (notes_to_remove * sizeof(struct note_command)) + + 4096 - sizeof(struct note_command); + off_t slide = end_of_outfile_loadcmds - end_of_infine_loadcmds; + + off_t all_image_infos_infile_offset = 0; + + fseek(in, start_of_infile_load_cmds, SEEK_SET); + fseek(out, start_of_infile_load_cmds, SEEK_SET); + // Copy all the load commands from IN to OUT, updating any file offsets by + // SLIDE. + for (int cmd_num = 0; cmd_num < number_of_load_cmds; cmd_num++) { + off_t cmd_start = ftello(in); + uint32_t cmd, cmdsize; + fread(&cmd, sizeof(uint32_t), 1, in); + fread(&cmdsize, sizeof(uint32_t), 1, in); + + fseeko(in, cmd_start, SEEK_SET); + off_t next_cmd = cmd_start + cmdsize; + + switch (cmd) { + case LC_SEGMENT: { + struct segment_command segcmd; + fread(&segcmd, sizeof(segcmd), 1, in); + segcmd.fileoff += slide; + fwrite(&segcmd, cmdsize, 1, out); + } break; + case LC_SEGMENT_64: { + struct segment_command_64 segcmd; + fread(&segcmd, sizeof(segcmd), 1, in); + segcmd.fileoff += slide; + fwrite(&segcmd, cmdsize, 1, out); + } break; + case LC_NOTE: { + struct note_command notecmd; + fread(¬ecmd, sizeof(notecmd), 1, in); + if ((strncmp(lc_note_name.c_str(), notecmd.data_owner, 16) == 0) && + remove_dups) { + fseeko(in, next_cmd, SEEK_SET); + continue; + } + if (strncmp("all image infos", notecmd.data_owner, 16) == 0) + all_image_infos_infile_offset = notecmd.offset; + notecmd.offset += slide; + fwrite(¬ecmd, cmdsize, 1, out); + } break; + default: { + vector<uint8_t> buf(cmdsize); + fread(buf.data(), cmdsize, 1, in); + fwrite(buf.data(), cmdsize, 1, out); + } + } + fseeko(in, next_cmd, SEEK_SET); + } + + // Now add our additional LC_NOTE load command. + struct note_command note; + note.cmd = LC_NOTE; + note.cmdsize = sizeof(struct note_command); + memset(¬e.data_owner, 0, 16); + // data_owner may not be nul terminated if all 16 characters + // are used, intentionally using strncpy here. + strncpy(note.data_owner, lc_note_name.c_str(), 16); + note.offset = infile_size + slide; + note.size = payload_data.size(); + fwrite(¬e, sizeof(struct note_command), 1, out); + + fseeko(in, end_of_infine_loadcmds, SEEK_SET); + fseeko(out, end_of_outfile_loadcmds, SEEK_SET); + + // Copy the rest of the corefile contents + vector<uint8_t> data_buf(1024 * 1024); + while (!feof(in)) { + size_t read_bytes = fread(data_buf.data(), 1, data_buf.size(), in); + if (read_bytes > 0) { + fwrite(data_buf.data(), read_bytes, 1, out); + } else { + break; + } + } + + fwrite(payload_data.data(), payload_data.size(), 1, out); + + // The "all image infos" LC_NOTE payload has file offsets hardcoded + // in it, unfortunately. We've shifted the contents of the corefile + // and these offsets need to be updated in the ouput file. + // Re-copy them into the outfile with corrected file offsets. + off_t infile_image_entry_base = 0; + if (all_image_infos_infile_offset != 0) { + off_t all_image_infos_outfile_offset = + all_image_infos_infile_offset + slide; + fseeko(in, all_image_infos_infile_offset, SEEK_SET); + struct all_image_infos_header header; + fread(&header, sizeof(header), 1, in); + infile_image_entry_base = header.entries_fileoff; + header.entries_fileoff += slide; + fseeko(out, all_image_infos_outfile_offset, SEEK_SET); + fwrite(&header, sizeof(header), 1, out); + + for (int i = 0; i < header.imgcount; i++) { + off_t infile_entries_fileoff = header.entries_fileoff - slide; + off_t outfile_entries_fileoff = header.entries_fileoff; + + struct image_entry ent; + fseeko(in, infile_entries_fileoff + (header.entry_size * i), SEEK_SET); + fread(&ent, sizeof(ent), 1, in); + ent.filepath_offset += slide; + ent.seg_addrs_offset += slide; + fseeko(out, outfile_entries_fileoff + (header.entry_size * i), SEEK_SET); + fwrite(&ent, sizeof(ent), 1, out); + } + } +} + +int main(int argc, char **argv) { + string infile, outfile, name; + vector<uint8_t> payload; + bool remove_dups; + parse_args(argc, argv, infile, outfile, name, payload, remove_dups); + + FILE *in = fopen(infile.c_str(), "r"); + if (!in) { + fprintf(stderr, "Unable to open %s for reading\n", infile.c_str()); + exit(1); + } + FILE *out = fopen(outfile.c_str(), "w"); + if (!out) { + fprintf(stderr, "Unable to open %s for reading\n", outfile.c_str()); + exit(1); + } + + copy_and_add_note(in, out, name, payload, remove_dups); + + fclose(in); + fclose(out); +} diff --git a/lldb/test/API/macosx/lc-note/additional-registers/main.c b/lldb/test/API/macosx/lc-note/additional-registers/main.c new file mode 100644 index 0000000000000..70679942daf06 --- /dev/null +++ b/lldb/test/API/macosx/lc-note/additional-registers/main.c @@ -0,0 +1,11 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +int main(int argc, char **argv) { + + char *heap_buf = (char *)malloc(80); + strcpy(heap_buf, "this is a string on the heap"); + + return 0; // break here +} _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits