| // Copyright 2025 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/lib/unwinder/arm_ehabi_module.h" |
| |
| #include <elf.h> |
| |
| #include "src/lib/unwinder/arm_ehabi_parser.h" |
| #include "src/lib/unwinder/elf_utils.h" |
| #include "src/lib/unwinder/error.h" |
| #include "src/lib/unwinder/registers.h" |
| |
| namespace unwinder { |
| |
| Error ArmEhAbiModule::Load() { |
| Elf32_Ehdr ehdr; |
| if (auto err = elf_->Read(elf_ptr_, ehdr); err.has_err()) { |
| return err; |
| } |
| |
| if (!elf_utils::VerifyElfIdentification<Elf32_Ehdr>(ehdr, elf_utils::ElfClass::k32Bit)) { |
| return Error("This doesn't look like an ELF module."); |
| } |
| |
| Elf32_Phdr phdr; |
| if (auto err = elf_utils::GetSegmentByType(elf_, elf_ptr_, PT_ARM_EXIDX, ehdr, phdr); |
| err.has_err()) { |
| return err; |
| } |
| |
| arm_exidx_start_ = elf_ptr_ + phdr.p_vaddr; |
| arm_exidx_end_ = arm_exidx_start_ + phdr.p_memsz; |
| |
| return Success(); |
| } |
| |
| Error ArmEhAbiModule::Search(uint32_t pc, IdxHeader& entry) { |
| uint32_t low = 0; |
| uint32_t high = (arm_exidx_end_ - arm_exidx_start_) / sizeof(IdxHeaderData); |
| |
| IdxHeaderData hdr; |
| |
| // When set, this will be the address of the most suitable entry we find in the table. If this is |
| // std::nullopt by the end of the loop below, there were no suitable matches in this module. |
| std::optional<uint32_t> best_entry_addr = std::nullopt; |
| |
| // Perform an Upper Bound search to find the largest function address not greater than |pc|. At |
| // the end of this loop |addr| will point to the first entry of the index whose function pointer |
| // is greater than |pc|. The best match is kept separately so we can better distinguish "not |
| // found" errors. Keep in mind the function addresses (the first word of the index entry) must be |
| // decoded before we can use them for comparison. |
| while (low + 1 < high) { |
| uint32_t mid = (low + high) / 2; |
| uint32_t addr = arm_exidx_start_ + mid * sizeof(IdxHeaderData); |
| uint32_t prel31_encoded_offset; |
| if (auto err = elf_->Read(addr, prel31_encoded_offset); err.has_err()) { |
| return err; |
| } |
| |
| int32_t fn_offset = DecodePrel31(prel31_encoded_offset); |
| // The function offset described in the Prel31 encoding is relative to the .ARM.exidx section, |
| // we have to account for the current offset into the table as well. |
| uint32_t decoded_fn_addr = addr + fn_offset; |
| |
| if (pc < decoded_fn_addr) { |
| high = mid; |
| } else { |
| low = mid; |
| // This is the new best entry for this PC value. Stash the decoded function address since |
| // we've already decoded it, and stash away the address of this entry so we can get the next |
| // word from the header at the end. |
| hdr.fn_addr = decoded_fn_addr; |
| best_entry_addr = addr; |
| } |
| } |
| |
| if (!best_entry_addr) { |
| return Error("PC not found in this module."); |
| } |
| |
| uint32_t data_addr = *best_entry_addr + sizeof(hdr.fn_addr); |
| |
| // Now we can get the associated unwinding data. |
| if (auto err = elf_->Read(data_addr, hdr.data); err.has_err()) { |
| return err; |
| } |
| |
| // The high bit of the data field indicates whether bits 0-30 are an offset to the ARM.extab |
| // section (which could either be the "generic model", or the "compact model" with too many |
| // entries to inline into the index table) or if they're inlined opcodes (the "compact [inline] |
| // model"). |
| if (hdr.data & 0x80000000) { |
| entry.type = IdxHeader::Type::kCompactInline; |
| } else { |
| entry.type = IdxHeader::Type::kCompact; |
| // The decoded relative address is an offset from the current position in the index, which |
| // happens to always be in the middle of an index entry since the relative address will always |
| // be the second entry. |
| // |
| // Note that we never actually need to do a section lookup on the .ARM.extab section because |
| // this address will be pointing directly to the unwind table that we need for this function. |
| // Since we don't know the precise starting address of the section, we cannot find the start of |
| // the table based on this offset without consulting the string table or section header string |
| // table which are both typically not mapped into a live process. |
| hdr.data = DecodePrel31(hdr.data) + data_addr; |
| } |
| |
| entry.header = hdr; |
| |
| return Success(); |
| } |
| |
| Error ArmEhAbiModule::Step(Memory* stack, const Registers& current, Registers& next, |
| bool pc_is_return_address) { |
| uint64_t pc; |
| if (auto err = current.GetPC(pc); err.has_err()) { |
| return err; |
| } |
| |
| // pc_is_return_address indicates whether pc in the current registers is a return address from a |
| // previous "Step". If it is, we need to subtract 1 to find the call site because "call" could |
| // be the last instruction of a nonreturn function and now the PC is pointing outside of the |
| // valid code boundary. |
| // |
| // Subtracting 1 is sufficient here because in |Search| above, we binary search function start |
| // addresses to find the unwinding instructions corresponding to this address. So it's still |
| // correct even if pc is not pointing to the beginning of an instruction. |
| if (pc_is_return_address) { |
| pc -= 1; |
| } |
| |
| IdxHeader entry; |
| if (auto err = Search(static_cast<uint32_t>(pc), entry); err.has_err()) { |
| return err; |
| } |
| |
| ArmEhAbiParser parser(elf_, entry); |
| |
| return parser.Step(stack, current, next); |
| } |
| |
| } // namespace unwinder |