// Copyright 2018 the V8 project 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/diagnostics/unwinder.h"

#include <algorithm>

#include "include/v8-unwinder.h"
#include "src/execution/frame-constants.h"
#include "src/execution/pointer-authentication.h"

namespace v8 {

// Architecture specific. Implemented in unwinder-<arch>.cc.
void GetCalleeSavedRegistersFromEntryFrame(void* fp,
                                           RegisterState* register_state);

i::Address Load(i::Address address) {
  return *reinterpret_cast<i::Address*>(address);
}

namespace {

const i::byte* CalculateEnd(const void* start, size_t length_in_bytes) {
  // Given that the length of the memory range is in bytes and it is not
  // necessarily aligned, we need to do the pointer arithmetic in byte* here.
  const i::byte* start_as_byte = reinterpret_cast<const i::byte*>(start);
  return start_as_byte + length_in_bytes;
}

bool PCIsInCodeRange(const v8::MemoryRange& code_range, void* pc) {
  return pc >= code_range.start &&
         pc < CalculateEnd(code_range.start, code_range.length_in_bytes);
}

// This relies on the fact that the code pages are ordered, and that they don't
// overlap.
bool PCIsInCodePages(size_t code_pages_length, const MemoryRange* code_pages,
                     void* pc) {
  DCHECK(std::is_sorted(code_pages, code_pages + code_pages_length,
                        [](const MemoryRange& a, const MemoryRange& b) {
                          return a.start < b.start;
                        }));

  MemoryRange fake_range{pc, 1};
  auto it =
      std::upper_bound(code_pages, code_pages + code_pages_length, fake_range,
                       [](const MemoryRange& a, const MemoryRange& b) {
                         return a.start < b.start;
                       });
  DCHECK_IMPLIES(it != code_pages + code_pages_length, pc < it->start);
  if (it == code_pages) return false;
  --it;
  return it->start <= pc && pc < CalculateEnd(it->start, it->length_in_bytes);
}

bool IsInJSEntryRange(const JSEntryStubs& entry_stubs, void* pc) {
  return PCIsInCodeRange(entry_stubs.js_entry_stub.code, pc) ||
         PCIsInCodeRange(entry_stubs.js_construct_entry_stub.code, pc) ||
         PCIsInCodeRange(entry_stubs.js_run_microtasks_entry_stub.code, pc);
}

bool IsInUnsafeJSEntryRange(const JSEntryStubs& entry_stubs, void* pc) {
  return IsInJSEntryRange(entry_stubs, pc);

  // TODO(petermarshall): We can be more precise by checking whether we are
  // in JSEntry but after frame setup and before frame teardown, in which case
  // we are safe to unwind the stack. For now, we bail out if the PC is anywhere
  // within JSEntry.
}

bool AddressIsInStack(const void* address, const void* stack_base,
                      const void* stack_top) {
  return address <= stack_base && address >= stack_top;
}

void* GetReturnAddressFromFP(void* fp, void* pc,
                             const JSEntryStubs& entry_stubs) {
  int caller_pc_offset = i::CommonFrameConstants::kCallerPCOffset;
// TODO(solanes): Implement the JSEntry range case also for x64 here and below.
#if V8_TARGET_ARCH_ARM64 || V8_TARGET_ARCH_ARM
  if (IsInJSEntryRange(entry_stubs, pc)) {
    caller_pc_offset = i::EntryFrameConstants::kDirectCallerPCOffset;
  }
#endif
  i::Address ret_addr =
      Load(reinterpret_cast<i::Address>(fp) + caller_pc_offset);
  return reinterpret_cast<void*>(i::PointerAuthentication::StripPAC(ret_addr));
}

void* GetCallerFPFromFP(void* fp, void* pc, const JSEntryStubs& entry_stubs) {
  int caller_fp_offset = i::CommonFrameConstants::kCallerFPOffset;
#if V8_TARGET_ARCH_ARM64 || V8_TARGET_ARCH_ARM
  if (IsInJSEntryRange(entry_stubs, pc)) {
    caller_fp_offset = i::EntryFrameConstants::kDirectCallerFPOffset;
  }
#endif
  return reinterpret_cast<void*>(
      Load(reinterpret_cast<i::Address>(fp) + caller_fp_offset));
}

void* GetCallerSPFromFP(void* fp, void* pc, const JSEntryStubs& entry_stubs) {
  int caller_sp_offset = i::CommonFrameConstants::kCallerSPOffset;
#if V8_TARGET_ARCH_ARM64 || V8_TARGET_ARCH_ARM
  if (IsInJSEntryRange(entry_stubs, pc)) {
    caller_sp_offset = i::EntryFrameConstants::kDirectCallerSPOffset;
  }
#endif
  return reinterpret_cast<void*>(reinterpret_cast<i::Address>(fp) +
                                 caller_sp_offset);
}

}  // namespace

bool Unwinder::TryUnwindV8Frames(const JSEntryStubs& entry_stubs,
                                 size_t code_pages_length,
                                 const MemoryRange* code_pages,
                                 RegisterState* register_state,
                                 const void* stack_base) {
  const void* stack_top = register_state->sp;

  void* pc = register_state->pc;
  if (PCIsInV8(code_pages_length, code_pages, pc) &&
      !IsInUnsafeJSEntryRange(entry_stubs, pc)) {
    void* current_fp = register_state->fp;
    if (!AddressIsInStack(current_fp, stack_base, stack_top)) return false;

    // Peek at the return address that the caller pushed. If it's in V8, then we
    // assume the caller frame is a JS frame and continue to unwind.
    void* next_pc = GetReturnAddressFromFP(current_fp, pc, entry_stubs);
    while (PCIsInV8(code_pages_length, code_pages, next_pc)) {
      current_fp = GetCallerFPFromFP(current_fp, pc, entry_stubs);
      if (!AddressIsInStack(current_fp, stack_base, stack_top)) return false;
      pc = next_pc;
      next_pc = GetReturnAddressFromFP(current_fp, pc, entry_stubs);
    }

    void* final_sp = GetCallerSPFromFP(current_fp, pc, entry_stubs);
    if (!AddressIsInStack(final_sp, stack_base, stack_top)) return false;
    register_state->sp = final_sp;

    // We don't check that the final FP value is within the stack bounds because
    // this is just the rbp value that JSEntryStub pushed. On platforms like
    // Win64 this is not used as a dedicated FP register, and could contain
    // anything.
    void* final_fp = GetCallerFPFromFP(current_fp, pc, entry_stubs);
    register_state->fp = final_fp;

    register_state->pc = next_pc;

    // Link register no longer valid after unwinding.
    register_state->lr = nullptr;

    if (IsInJSEntryRange(entry_stubs, pc)) {
      GetCalleeSavedRegistersFromEntryFrame(current_fp, register_state);
    }
    return true;
  }
  return false;
}

bool Unwinder::PCIsInV8(size_t code_pages_length, const MemoryRange* code_pages,
                        void* pc) {
  return pc && PCIsInCodePages(code_pages_length, code_pages, pc);
}

}  // namespace v8
