[vm/compiler] Fix relocator to take out-of-range backwards calls into acount

There was a missing check for whether a backwards call is out-of-range.

The CL also removes leftover code related to [max_offset_into_target_] -
which wasn't used. The intended use is already over approximated via
[max_instructions_size_].

The CL makes the pc-relative call/tail-call distances pluggable so a
newly added test can modify them for testing in-range/out-of-range
forward/backwards calls.

TEST=vm/cc/CodeRelocator_*

Fixes https://github.com/flutter/flutter/issues/80043

Change-Id: Id4bdb7176108b61235dafb7ffc125da4a2bf07fa
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/195682
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Vyacheslav Egorov <vegorov@google.com>
diff --git a/runtime/vm/compiler/compiler_sources.gni b/runtime/vm/compiler/compiler_sources.gni
index 9135f50..1b237e9 100644
--- a/runtime/vm/compiler/compiler_sources.gni
+++ b/runtime/vm/compiler/compiler_sources.gni
@@ -175,6 +175,7 @@
   "backend/typed_data_aot_test.cc",
   "backend/yield_position_test.cc",
   "cha_test.cc",
+  "relocation_test.cc",
   "ffi/native_type_vm_test.cc",
   "frontend/kernel_binary_flowgraph_test.cc",
   "write_barrier_elimination_test.cc",
diff --git a/runtime/vm/compiler/relocation.cc b/runtime/vm/compiler/relocation.cc
index ff5ea61..e2f44aa 100644
--- a/runtime/vm/compiler/relocation.cc
+++ b/runtime/vm/compiler/relocation.cc
@@ -20,6 +20,47 @@
             false,
             "Generate always trampolines (for testing purposes).");
 
+DEFINE_FLAG(int,
+            lower_tail_pc_relative_call_distance,
+            -1,
+            "Lower tail call distance.");
+DEFINE_FLAG(int,
+            upper_tail_pc_relative_call_distance,
+            -1,
+            "Upper tail call distance.");
+DEFINE_FLAG(int, lower_pc_relative_call_distance, -1, "Lower call distance.");
+DEFINE_FLAG(int, upper_pc_relative_call_distance, -1, "Upper call distance.");
+
+struct TailCallDistanceLimits {
+  static intptr_t Lower() {
+    if (FLAG_lower_tail_pc_relative_call_distance != -1) {
+      return FLAG_lower_tail_pc_relative_call_distance;
+    }
+    return PcRelativeTailCallPattern::kLowerCallingRange;
+  }
+  static intptr_t Upper() {
+    if (FLAG_upper_tail_pc_relative_call_distance != -1) {
+      return FLAG_upper_tail_pc_relative_call_distance;
+    }
+    return PcRelativeTailCallPattern::kUpperCallingRange;
+  }
+};
+
+struct CallDistanceLimits {
+  static intptr_t Lower() {
+    if (FLAG_lower_pc_relative_call_distance != -1) {
+      return FLAG_lower_pc_relative_call_distance;
+    }
+    return PcRelativeCallPattern::kLowerCallingRange;
+  }
+  static intptr_t Upper() {
+    if (FLAG_upper_pc_relative_call_distance != -1) {
+      return FLAG_upper_pc_relative_call_distance;
+    }
+    return PcRelativeCallPattern::kUpperCallingRange;
+  }
+};
+
 const intptr_t kTrampolineSize =
     Utils::RoundUp(PcRelativeTrampolineJumpPattern::kLengthInBytes,
                    compiler::target::Instructions::kBarePayloadAlignment);
@@ -46,7 +87,7 @@
   //    * the maximum number of calls
   //    * the maximum offset into a target instruction
   //
-  FindInstructionAndCallLimits();
+  FindLargestInstruction();
 
   // Emit all instructions and do relocations on the way.
   for (intptr_t i = 0; i < code_objects_->length(); ++i) {
@@ -65,7 +106,8 @@
 
     // If we have forward/backwards calls which are almost out-of-range, we'll
     // create trampolines now.
-    BuildTrampolinesForAlmostOutOfRangeCalls();
+    BuildTrampolinesForAlmostOutOfRangeCalls(
+        /*force=*/(i == (code_objects_->length() - 1)));
   }
 
   // We're guaranteed to have all calls resolved, since
@@ -101,7 +143,7 @@
   // however we might need it to write information into V8 snapshot profile.
 }
 
-void CodeRelocator::FindInstructionAndCallLimits() {
+void CodeRelocator::FindLargestInstruction() {
   auto zone = thread_->zone();
   auto& current_caller = Code::Handle(zone);
   auto& call_targets = Array::Handle(zone);
@@ -122,48 +164,10 @@
         kind_type_and_offset_ = call.Get<Code::kSCallTableKindAndOffset>();
         const auto kind =
             Code::KindField::decode(kind_type_and_offset_.Value());
-        const auto return_pc_offset =
-            Code::OffsetField::decode(kind_type_and_offset_.Value());
-        const auto call_entry_point =
-            Code::EntryPointField::decode(kind_type_and_offset_.Value());
-
         if (kind == Code::kCallViaCode) {
           continue;
         }
-
-        destination_ = GetTarget(call);
         num_calls++;
-
-        // A call site can decide to jump not to the beginning of a function but
-        // rather jump into it at a certain (positive) offset.
-        int32_t offset_into_target = 0;
-        if (kind == Code::kPcRelativeCall || kind == Code::kPcRelativeTTSCall) {
-          const intptr_t call_instruction_offset =
-              return_pc_offset - PcRelativeCallPattern::kLengthInBytes;
-          PcRelativeCallPattern call(current_caller.PayloadStart() +
-                                     call_instruction_offset);
-          ASSERT(call.IsValid());
-          offset_into_target = call.distance();
-        } else {
-          ASSERT(kind == Code::kPcRelativeTailCall);
-          const intptr_t call_instruction_offset =
-              return_pc_offset - PcRelativeTailCallPattern::kLengthInBytes;
-          PcRelativeTailCallPattern call(current_caller.PayloadStart() +
-                                         call_instruction_offset);
-          ASSERT(call.IsValid());
-          offset_into_target = call.distance();
-        }
-
-        const uword destination_payload = destination_.PayloadStart();
-        const uword entry_point = call_entry_point == Code::kUncheckedEntry
-                                      ? destination_.UncheckedEntryPoint()
-                                      : destination_.EntryPoint();
-
-        offset_into_target += (entry_point - destination_payload);
-
-        if (offset_into_target > max_offset_into_target_) {
-          max_offset_into_target_ = offset_into_target;
-        }
       }
 
       if (num_calls > max_calls_) {
@@ -323,8 +327,11 @@
   auto map_entry = text_offsets_.Lookup(callee);
   if (map_entry == nullptr) return false;
 
-  ResolveCall(unresolved_call);
-  return true;
+  if (IsTargetInRangeFor(unresolved_call, map_entry->value)) {
+    ResolveCall(unresolved_call);
+    return true;
+  }
+  return false;
 }
 
 void CodeRelocator::ResolveUnresolvedCallsTargeting(
@@ -411,11 +418,11 @@
   const auto forward_distance =
       target_text_offset - unresolved_call->text_offset;
   if (unresolved_call->is_tail_call) {
-    return PcRelativeTailCallPattern::kLowerCallingRange < forward_distance &&
-           forward_distance < PcRelativeTailCallPattern::kUpperCallingRange;
+    return TailCallDistanceLimits::Lower() < forward_distance &&
+           forward_distance < TailCallDistanceLimits::Upper();
   } else {
-    return PcRelativeCallPattern::kLowerCallingRange < forward_distance &&
-           forward_distance < PcRelativeCallPattern::kUpperCallingRange;
+    return CallDistanceLimits::Lower() < forward_distance &&
+           forward_distance < CallDistanceLimits::Upper();
   }
 }
 
@@ -471,7 +478,7 @@
   return destination_.ptr();
 }
 
-void CodeRelocator::BuildTrampolinesForAlmostOutOfRangeCalls() {
+void CodeRelocator::BuildTrampolinesForAlmostOutOfRangeCalls(bool force) {
   while (!all_unresolved_calls_.IsEmpty()) {
     UnresolvedCall* unresolved_call = all_unresolved_calls_.First();
 
@@ -484,7 +491,7 @@
         kTrampolineSize *
             (unresolved_calls_by_destination_.Length() + max_calls_);
     if (IsTargetInRangeFor(unresolved_call, future_boundary) &&
-        !FLAG_always_generate_trampolines_for_testing) {
+        !FLAG_always_generate_trampolines_for_testing && !force) {
       break;
     }
 
diff --git a/runtime/vm/compiler/relocation.h b/runtime/vm/compiler/relocation.h
index e9b6d44..77b914c 100644
--- a/runtime/vm/compiler/relocation.h
+++ b/runtime/vm/compiler/relocation.h
@@ -161,7 +161,7 @@
 
   void Relocate(bool is_vm_isolate);
 
-  void FindInstructionAndCallLimits();
+  void FindLargestInstruction();
 
   bool AddInstructionsToText(CodePtr code);
   void ScanCallTargets(const Code& code,
@@ -183,7 +183,7 @@
                                 intptr_t destination_text);
   void ResolveTrampoline(UnresolvedTrampoline* unresolved_trampoline);
 
-  void BuildTrampolinesForAlmostOutOfRangeCalls();
+  void BuildTrampolinesForAlmostOutOfRangeCalls(bool force);
 
   intptr_t FindDestinationInText(const InstructionsPtr destination,
                                  intptr_t offset_into_target);
@@ -207,7 +207,6 @@
   intptr_t max_instructions_size_ = 0;
   // The maximum number of pc-relative calls in an instructions object.
   intptr_t max_calls_ = 0;
-  intptr_t max_offset_into_target_ = 0;
 
   // Data structures used for relocation.
   intptr_t next_text_offset_ = 0;
diff --git a/runtime/vm/compiler/relocation_test.cc b/runtime/vm/compiler/relocation_test.cc
new file mode 100644
index 0000000..9e101cb
--- /dev/null
+++ b/runtime/vm/compiler/relocation_test.cc
@@ -0,0 +1,406 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+#include "platform/assert.h"
+
+#include "vm/allocation.h"
+#include "vm/code_patcher.h"
+#include "vm/compiler/assembler/assembler.h"
+#include "vm/compiler/relocation.h"
+#include "vm/instructions.h"
+#include "vm/longjump.h"
+#include "vm/unit_test.h"
+
+#define __ assembler->
+
+namespace dart {
+
+#if defined(DART_PRECOMPILER) && !defined(TARGET_ARCH_IA32)
+
+DECLARE_FLAG(bool, dual_map_code);
+DECLARE_FLAG(int, lower_pc_relative_call_distance);
+DECLARE_FLAG(int, upper_pc_relative_call_distance);
+
+struct RelocatorTestHelper {
+  explicit RelocatorTestHelper(Thread* thread)
+      : thread(thread),
+        locker(thread, thread->isolate_group()->program_lock()),
+        safepoint_and_growth_scope(thread) {
+    // So the relocator uses the correct instruction size layout.
+    FLAG_precompiled_mode = true;
+    FLAG_use_bare_instructions = true;
+
+    FLAG_lower_pc_relative_call_distance = -128;
+    FLAG_upper_pc_relative_call_distance = 128;
+  }
+  ~RelocatorTestHelper() {
+    FLAG_use_bare_instructions = false;
+    FLAG_precompiled_mode = false;
+  }
+
+  void CreateInstructions(std::initializer_list<intptr_t> sizes) {
+    for (auto size : sizes) {
+      codes.Add(&Code::Handle(AllocationInstruction(size)));
+    }
+  }
+
+  CodePtr AllocationInstruction(uintptr_t size) {
+    const auto& instructions = Instructions::Handle(
+        Instructions::New(size, /*has_monomorphic=*/false));
+
+    uword addr = instructions.PayloadStart();
+    for (uintptr_t i = 0; i < (size / 4); ++i) {
+      *reinterpret_cast<uint32_t*>(addr + 4 * i) =
+          static_cast<uint32_t>(kBreakInstructionFiller);
+    }
+
+    const auto& code = Code::Handle(Code::New(0));
+    code.SetActiveInstructions(instructions, 0);
+    code.set_instructions(instructions);
+    return code.ptr();
+  }
+
+  void EmitPcRelativeCallFunction(intptr_t idx, intptr_t to_idx) {
+    const Code& code = *codes[idx];
+    const Code& target = *codes[to_idx];
+
+    EmitCodeFor(code, [&](compiler::Assembler* assembler) {
+#if defined(TARGET_ARCH_ARM64)
+      __ SetupDartSP();
+      __ EnterFrame(0);
+#elif defined(TARGET_ARCH_ARM)
+    SPILLS_RETURN_ADDRESS_FROM_LR_TO_REGISTER(
+      __ EnterFrame((1 << LR), 0));
+#endif
+      __ GenerateUnRelocatedPcRelativeCall();
+      AddPcRelativeCallTargetAt(__ CodeSize(), code, target);
+#if defined(TARGET_ARCH_ARM64)
+      __ LeaveFrame();
+#elif defined(TARGET_ARCH_ARM)
+    RESTORES_RETURN_ADDRESS_FROM_REGISTER_TO_LR(
+      __ LeaveFrame((1 << LR)));
+#endif
+      __ Ret();
+    });
+  }
+
+  void EmitReturn42Function(intptr_t idx) {
+    const Code& code = *codes[idx];
+    EmitCodeFor(code, [&](compiler::Assembler* assembler) {
+#if defined(TARGET_ARCH_X64)
+      __ LoadImmediate(RAX, 42);
+#elif defined(TARGET_ARCH_ARM) || defined(TARGET_ARCH_ARM64)
+      __ LoadImmediate(R0, 42);
+#endif
+      __ Ret();
+    });
+  }
+
+  void EmitCodeFor(const Code& code,
+                   std::function<void(compiler::Assembler* assembler)> fun) {
+    const auto& inst = Instructions::Handle(code.instructions());
+
+    compiler::Assembler assembler(nullptr);
+    fun(&assembler);
+
+    const uword addr = inst.PayloadStart();
+    memmove(reinterpret_cast<void*>(addr),
+            reinterpret_cast<void*>(assembler.CodeAddress(0)),
+            assembler.CodeSize());
+
+    if (FLAG_write_protect_code && FLAG_dual_map_code) {
+      auto& instructions = Instructions::Handle(code.instructions());
+      instructions ^= OldPage::ToExecutable(instructions.ptr());
+      code.set_instructions(instructions);
+    }
+  }
+
+  void AddPcRelativeCallTargetAt(intptr_t offset,
+                                 const Code& code,
+                                 const Code& target) {
+    const auto& kind_and_offset = Smi::Handle(
+        Smi::New(Code::KindField::encode(Code::kPcRelativeCall) |
+                 Code::EntryPointField::encode(Code::kDefaultEntry) |
+                 Code::OffsetField::encode(offset)));
+    AddCall(code, target, kind_and_offset);
+  }
+
+  void AddCall(const Code& code,
+               const Code& target,
+               const Smi& kind_and_offset) {
+    auto& call_targets = Array::Handle(code.static_calls_target_table());
+    if (call_targets.IsNull()) {
+      call_targets = Array::New(Code::kSCallTableEntryLength);
+    } else {
+      call_targets = Array::Grow(
+          call_targets, call_targets.Length() + Code::kSCallTableEntryLength);
+    }
+
+    StaticCallsTable table(call_targets);
+    auto entry = table[table.Length() - 1];
+    entry.Set<Code::kSCallTableKindAndOffset>(kind_and_offset);
+    entry.Set<Code::kSCallTableCodeOrTypeTarget>(target);
+    entry.Set<Code::kSCallTableFunctionTarget>(
+        Function::Handle(Function::null()));
+    code.set_static_calls_target_table(call_targets);
+  }
+
+  void BuildImageAndRunTest(
+      std::function<void(const GrowableArray<ImageWriterCommand>&, uword*)>
+          fun) {
+    auto& image = Instructions::Handle();
+    uword entrypoint = 0;
+    {
+      GrowableArray<CodePtr> raw_codes;
+      for (auto code : codes) {
+        raw_codes.Add(code->ptr());
+      }
+
+      GrowableArray<ImageWriterCommand> commands;
+      CodeRelocator::Relocate(thread, &raw_codes, &commands,
+                              /*is_vm_isolate=*/false);
+
+      uword expected_offset = 0;
+      fun(commands, &expected_offset);
+
+      image = BuildImage(&commands);
+      entrypoint = image.EntryPoint() + expected_offset;
+
+      for (intptr_t i = 0; i < commands.length(); ++i) {
+        if (commands[i].op == ImageWriterCommand::InsertBytesOfTrampoline) {
+          delete[] commands[i].insert_trampoline_bytes.buffer;
+          commands[i].insert_trampoline_bytes.buffer = nullptr;
+        }
+      }
+    }
+    typedef intptr_t (*Fun)() DART_UNUSED;
+#if defined(TARGET_ARCH_X64)
+    EXPECT_EQ(42, reinterpret_cast<Fun>(entrypoint)());
+#elif defined(TARGET_ARCH_ARM)
+    EXPECT_EQ(42, EXECUTE_TEST_CODE_INT32(Fun, entrypoint));
+#elif defined(TARGET_ARCH_ARM64)
+    EXPECT_EQ(42, EXECUTE_TEST_CODE_INT64(Fun, entrypoint));
+#endif
+  }
+
+  InstructionsPtr BuildImage(GrowableArray<ImageWriterCommand>* commands) {
+    intptr_t size = 0;
+    for (intptr_t i = 0; i < commands->length(); ++i) {
+      switch ((*commands)[i].op) {
+        case ImageWriterCommand::InsertBytesOfTrampoline:
+          size += (*commands)[i].insert_trampoline_bytes.buffer_length;
+          break;
+        case ImageWriterCommand::InsertInstructionOfCode:
+          size += ImageWriter::SizeInSnapshot(Code::InstructionsOf(
+              (*commands)[i].insert_instruction_of_code.code));
+          break;
+      }
+    }
+
+    auto& instructions = Instructions::Handle(
+        Instructions::New(size, /*has_monomorphic=*/false));
+    {
+      uword addr = instructions.PayloadStart();
+      for (intptr_t i = 0; i < commands->length(); ++i) {
+        switch ((*commands)[i].op) {
+          case ImageWriterCommand::InsertBytesOfTrampoline: {
+            const auto entry = (*commands)[i].insert_trampoline_bytes;
+            const auto current_size = entry.buffer_length;
+            memmove(reinterpret_cast<void*>(addr), entry.buffer, current_size);
+            addr += current_size;
+            break;
+          }
+          case ImageWriterCommand::InsertInstructionOfCode: {
+            const auto entry = (*commands)[i].insert_instruction_of_code;
+            const auto current_size =
+                ImageWriter::SizeInSnapshot(Code::InstructionsOf(entry.code));
+            const auto alias_offset =
+                OldPage::Of(Code::InstructionsOf(entry.code))->AliasOffset();
+            memmove(
+                reinterpret_cast<void*>(addr),
+                reinterpret_cast<void*>(Instructions::PayloadStart(
+                                            Code::InstructionsOf(entry.code)) -
+                                        alias_offset),
+                current_size);
+            addr += current_size;
+            break;
+          }
+        }
+      }
+
+      if (FLAG_write_protect_code) {
+        const uword address = UntaggedObject::ToAddr(instructions.ptr());
+        const auto size = instructions.ptr()->untag()->HeapSize();
+        instructions =
+            Instructions::RawCast(OldPage::ToExecutable(instructions.ptr()));
+
+        const auto prot = FLAG_dual_map_code ? VirtualMemory::kReadOnly
+                                             : VirtualMemory::kReadExecute;
+        VirtualMemory::Protect(reinterpret_cast<void*>(address), size, prot);
+      }
+      CPU::FlushICache(instructions.PayloadStart(), instructions.Size());
+    }
+    return instructions.ptr();
+  }
+
+  Thread* thread;
+  SafepointWriteRwLocker locker;
+  ForceGrowthSafepointOperationScope safepoint_and_growth_scope;
+  GrowableArray<const Code*> codes;
+};
+
+ISOLATE_UNIT_TEST_CASE(CodeRelocator_DirectForwardCall) {
+  RelocatorTestHelper helper(thread);
+  helper.CreateInstructions({32, 36, 32});
+  helper.EmitPcRelativeCallFunction(0, 2);
+  helper.EmitReturn42Function(2);
+  helper.BuildImageAndRunTest(
+      [&](const GrowableArray<ImageWriterCommand>& commands,
+          uword* entry_point) {
+        EXPECT_EQ(3, commands.length());
+
+        // This makes an in-range forward call.
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[1].op);
+        // This is is the target of the forwards call.
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
+
+        *entry_point = commands[0].expected_offset;
+      });
+}
+
+ISOLATE_UNIT_TEST_CASE(CodeRelocator_OutOfRangeForwardCall) {
+  RelocatorTestHelper helper(thread);
+  helper.CreateInstructions(
+      {32, FLAG_upper_pc_relative_call_distance - 32 + 4, 32});
+  helper.EmitPcRelativeCallFunction(0, 2);
+  helper.EmitReturn42Function(2);
+  helper.BuildImageAndRunTest([&](const GrowableArray<ImageWriterCommand>&
+                                      commands,
+                                  uword* entry_point) {
+    EXPECT_EQ(4, commands.length());
+
+    // This makes an out-of-range forward call.
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
+    // This is the last change the relocator thinks it can ensure the
+    // out-of-range call above can call a trampoline - so it injets it here and
+    // no later.
+    EXPECT_EQ(ImageWriterCommand::InsertBytesOfTrampoline, commands[1].op);
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
+    // This is the target of the forwwards call.
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[3].op);
+
+    *entry_point = commands[0].expected_offset;
+  });
+}
+
+ISOLATE_UNIT_TEST_CASE(CodeRelocator_DirectBackwardCall) {
+  RelocatorTestHelper helper(thread);
+  helper.CreateInstructions({32, 32, 32});
+  helper.EmitReturn42Function(0);
+  helper.EmitPcRelativeCallFunction(2, 0);
+  helper.BuildImageAndRunTest(
+      [&](const GrowableArray<ImageWriterCommand>& commands,
+          uword* entry_point) {
+        EXPECT_EQ(3, commands.length());
+
+        // This is the backwards call target.
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[1].op);
+        // This makes an in-range backwards call.
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
+
+        *entry_point = commands[2].expected_offset;
+      });
+}
+
+ISOLATE_UNIT_TEST_CASE(CodeRelocator_OutOfRangeBackwardCall) {
+  RelocatorTestHelper helper(thread);
+  helper.CreateInstructions({32, 32, 32, 32 + 4, 32, 32, 32, 32, 32});
+  helper.EmitReturn42Function(0);
+  helper.EmitPcRelativeCallFunction(4, 0);
+  helper.BuildImageAndRunTest([&](const GrowableArray<ImageWriterCommand>&
+                                      commands,
+                                  uword* entry_point) {
+    EXPECT_EQ(10, commands.length());
+
+    // This is the backwards call target.
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[1].op);
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[3].op);
+    // This makes an out-of-range backwards call. The relocator will make the
+    // call go to a trampoline instead. It will delay insertion of the
+    // trampoline until it almost becomes out-of-range.
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[4].op);
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[5].op);
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[6].op);
+    // This is the last change the relocator thinks it can ensure the
+    // out-of-range call above can call a trampoline - so it injets it here and
+    // no later.
+    EXPECT_EQ(ImageWriterCommand::InsertBytesOfTrampoline, commands[7].op);
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[8].op);
+    EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[9].op);
+
+    *entry_point = commands[4].expected_offset;
+  });
+}
+
+ISOLATE_UNIT_TEST_CASE(CodeRelocator_OutOfRangeBackwardCall2) {
+  RelocatorTestHelper helper(thread);
+  helper.CreateInstructions({32, 32, 32, 32 + 4, 32});
+  helper.EmitReturn42Function(0);
+  helper.EmitPcRelativeCallFunction(4, 0);
+  helper.BuildImageAndRunTest(
+      [&](const GrowableArray<ImageWriterCommand>& commands,
+          uword* entry_point) {
+        EXPECT_EQ(6, commands.length());
+
+        // This is the backwards call target.
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[0].op);
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[1].op);
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[2].op);
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[3].op);
+        // This makes an out-of-range backwards call. The relocator will make
+        // the call go to a trampoline instead. It will delay insertion of the
+        // trampoline until it almost becomes out-of-range.
+        EXPECT_EQ(ImageWriterCommand::InsertInstructionOfCode, commands[4].op);
+        // There's no other instructions coming, so the relocator will resolve
+        // any pending out-of-range calls by inserting trampolines at the end.
+        EXPECT_EQ(ImageWriterCommand::InsertBytesOfTrampoline, commands[5].op);
+
+        *entry_point = commands[4].expected_offset;
+      });
+}
+
+UNIT_TEST_CASE(PCRelativeCallPatterns) {
+  {
+    uint8_t instruction[PcRelativeCallPattern::kLengthInBytes];
+
+    PcRelativeCallPattern pattern(reinterpret_cast<uword>(&instruction));
+
+    pattern.set_distance(PcRelativeCallPattern::kLowerCallingRange);
+    EXPECT_EQ(PcRelativeCallPattern::kLowerCallingRange, pattern.distance());
+
+    pattern.set_distance(PcRelativeCallPattern::kUpperCallingRange);
+    EXPECT_EQ(PcRelativeCallPattern::kUpperCallingRange, pattern.distance());
+  }
+  {
+    uint8_t instruction[PcRelativeTailCallPattern::kLengthInBytes];
+
+    PcRelativeTailCallPattern pattern(reinterpret_cast<uword>(&instruction));
+
+    pattern.set_distance(PcRelativeTailCallPattern::kLowerCallingRange);
+    EXPECT_EQ(PcRelativeTailCallPattern::kLowerCallingRange,
+              pattern.distance());
+
+    pattern.set_distance(PcRelativeTailCallPattern::kUpperCallingRange);
+    EXPECT_EQ(PcRelativeTailCallPattern::kUpperCallingRange,
+              pattern.distance());
+  }
+}
+
+#endif  // defined(DART_PRECOMPILER) && !defined(TARGET_ARCH_IA32)
+
+}  // namespace dart
diff --git a/runtime/vm/instructions_arm.h b/runtime/vm/instructions_arm.h
index a1e1f9b..5bfc9bd 100644
--- a/runtime/vm/instructions_arm.h
+++ b/runtime/vm/instructions_arm.h
@@ -193,8 +193,10 @@
 class PcRelativeCallPatternBase : public ValueObject {
  public:
   // 24 bit signed integer which will get multiplied by 4.
-  static const intptr_t kLowerCallingRange = -(1 << 25) + Instr::kPCReadOffset;
-  static const intptr_t kUpperCallingRange = (1 << 25) - 1;
+  static constexpr intptr_t kLowerCallingRange =
+      -(1 << 25) + Instr::kPCReadOffset;
+  static constexpr intptr_t kUpperCallingRange =
+      (1 << 25) - Instr::kInstrSize + Instr::kPCReadOffset;
 
   explicit PcRelativeCallPatternBase(uword pc) : pc_(pc) {}
 
diff --git a/runtime/vm/instructions_arm64.h b/runtime/vm/instructions_arm64.h
index b450522..ecfecb6 100644
--- a/runtime/vm/instructions_arm64.h
+++ b/runtime/vm/instructions_arm64.h
@@ -203,8 +203,8 @@
 class PcRelativePatternBase : public ValueObject {
  public:
   // 26 bit signed integer which will get multiplied by 4.
-  static const intptr_t kLowerCallingRange = -(1 << 27);
-  static const intptr_t kUpperCallingRange = (1 << 27) - 1;
+  static constexpr intptr_t kLowerCallingRange = -(1 << 27);
+  static constexpr intptr_t kUpperCallingRange = (1 << 27) - Instr::kInstrSize;
 
   explicit PcRelativePatternBase(uword pc) : pc_(pc) {}
 
diff --git a/runtime/vm/instructions_x64.h b/runtime/vm/instructions_x64.h
index efc8abd..ce23054 100644
--- a/runtime/vm/instructions_x64.h
+++ b/runtime/vm/instructions_x64.h
@@ -110,8 +110,8 @@
 // callq *[rip+offset]
 class PcRelativeCallPattern : public InstructionPattern<PcRelativeCallPattern> {
  public:
-  static const intptr_t kLowerCallingRange = -(DART_UINT64_C(1) << 31);
-  static const intptr_t kUpperCallingRange = (DART_UINT64_C(1) << 31) - 1;
+  static constexpr intptr_t kLowerCallingRange = -(DART_UINT64_C(1) << 31);
+  static constexpr intptr_t kUpperCallingRange = (DART_UINT64_C(1) << 31) - 1;
 
   explicit PcRelativeCallPattern(uword pc) : InstructionPattern(pc) {}
 
@@ -169,11 +169,11 @@
            kLengthInBytes;
   }
 
-  void set_distance(int32_t distance) {
+  void set_distance(intptr_t distance) {
     // [distance] is relative to the start of the instruction, x64 considers the
     // offset relative to next PC.
     StoreUnaligned(reinterpret_cast<int32_t*>(pattern_start_ + 1),
-                   distance - kLengthInBytes);
+                   static_cast<int32_t>(distance - kLengthInBytes));
   }
 
   bool IsValid() const {
@@ -187,8 +187,8 @@
 
 class PcRelativeTailCallPattern : public PcRelativeTrampolineJumpPattern {
  public:
-  static const intptr_t kLowerCallingRange = -(1ul << 31) + kLengthInBytes;
-  static const intptr_t kUpperCallingRange = (1ul << 31) - 1;
+  static constexpr intptr_t kLowerCallingRange = -(1ul << 31) + kLengthInBytes;
+  static constexpr intptr_t kUpperCallingRange = (1ul << 31) - 1;
 
   explicit PcRelativeTailCallPattern(uword pc)
       : PcRelativeTrampolineJumpPattern(pc) {}
diff --git a/runtime/vm/object.h b/runtime/vm/object.h
index 4f4fb6c..20558b3 100644
--- a/runtime/vm/object.h
+++ b/runtime/vm/object.h
@@ -5334,6 +5334,8 @@
   void set_stats(CodeStatistics* stats) const;
 
  private:
+  friend struct RelocatorTestHelper;
+
   void SetSize(intptr_t value) const {
     ASSERT(value >= 0);
     StoreNonPointer(&untag()->size_and_flags_,
@@ -6392,6 +6394,8 @@
 
   friend class UntaggedObject;  // For UntaggedObject::SizeFromClass().
   friend class UntaggedCode;
+  friend struct RelocatorTestHelper;
+
   enum {
     kOptimizedBit = 0,
     kForceOptimizedBit = 1,