Skip to content

[lldb][swift] Prefer assembly unwind plans for async registers #9912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2206,6 +2206,21 @@ static llvm::Expected<addr_t> GetCFA(Process &process, RegisterContext &regctx,
cfa_loc.GetValueType());
}

static UnwindPlanSP GetUnwindPlanForAsyncRegister(FuncUnwinders &unwinders,
Target &target,
Thread &thread) {
// We cannot trust compiler emitted unwind plans, as they respect the
// swifttail calling convention, which assumes the async register is _not_
// restored and therefore it is not tracked by compiler plans. If LLDB uses
// those plans, it may take "no info" to mean "register not clobbered". For
// those reasons, always favour the assembly plan first, it will try to track
// the async register by assuming the usual arm calling conventions.
if (UnwindPlanSP asm_plan = unwinders.GetAssemblyUnwindPlan(target, thread))
return asm_plan;
// In the unlikely case the assembly plan is not available, try all others.
return unwinders.GetUnwindPlanAtNonCallSite(target, thread);
}

/// Attempts to use UnwindPlans that inspect assembly to recover the entry value
/// of the async context register. This is a simplified version of the methods
/// in RegisterContextUnwind, since plumbing access to those here would be
Expand All @@ -2223,7 +2238,7 @@ static llvm::Expected<addr_t> ReadAsyncContextRegisterFromUnwind(

Target &target = process.GetTarget();
UnwindPlanSP unwind_plan =
unwinders->GetUnwindPlanAtNonCallSite(target, regctx.GetThread());
GetUnwindPlanForAsyncRegister(*unwinders, target, regctx.GetThread());
if (!unwind_plan)
return llvm::createStringError(
"SwiftLanguageRuntime: Failed to find non call site unwind plan at "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SWIFT_SOURCES := main.swift
SWIFTFLAGS_EXTRAS := -parse-as-library -Xfrontend -Xllvm -Xfrontend --emit-dwarf-unwind=always
include Makefile.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import lldb
from lldbsuite.test.decorators import *
import lldbsuite.test.lldbtest as lldbtest
import lldbsuite.test.lldbutil as lldbutil


# This test is a best effort attempt at creating a program with high register
# pressure while also making the linker avoid compact unwind.
class TestCase(lldbtest.TestBase):

mydir = lldbtest.TestBase.compute_mydir(__file__)

def set_breakpoints_all_instructions(self, target):
funclet_name = "$s1a12ASYNC___1___SiyYaFTY0_"
breakpoints = set()

sym_ctx_list = target.FindFunctions(funclet_name)
self.assertEqual(
sym_ctx_list.GetSize(),
1,
f"failed to get symbol context for {funclet_name}",
)
function = sym_ctx_list[0].function

instructions = list(function.GetInstructions(target))
self.assertGreater(len(instructions), 0)
for instruction in instructions:
bp = target.BreakpointCreateBySBAddress(instruction.GetAddress())
self.assertTrue(
bp.IsValid(), f"failed to set bp inside funclet {funclet_name}"
)
breakpoints.add(bp.GetID())
return breakpoints

def check_unwind_ok(self, thread):
# Check that we see the virtual backtrace:
expected_funcnames = [
"ASYNC___1___",
"ASYNC___2___",
"ASYNC___3___",
"ASYNC___4___",
"ASYNC___5___",
]
frames = thread.frames
self.assertGreater(
len(frames), len(expected_funcnames), f"Invalid backtrace for {frames}"
)
actual_funcnames = [
frame.GetFunctionName() for frame in frames[: len(expected_funcnames)]
]
for expected_name, actual_name in zip(expected_funcnames, actual_funcnames):
self.assertIn(expected_name, actual_name, f"Unexpected backtrace: {frames}")

@swiftTest
@skipIf(oslist=["windows", "linux"])
def test(self):
"""Test that the debugger can unwind at all instructions of all funclets"""
self.build()

source_file = lldb.SBFileSpec("main.swift")
target, process, _, bp = lldbutil.run_to_source_breakpoint(
self, "BREAK HERE", source_file
)
target.DeleteAllBreakpoints()

breakpoints = self.set_breakpoints_all_instructions(target)
num_breakpoints = len(breakpoints)

# Reach most breakpoints and ensure we can unwind in that position.
while True:
process.Continue()
if process.GetState() == lldb.eStateExited:
break
thread = lldbutil.get_stopped_thread(process, lldb.eStopReasonBreakpoint)
self.assertTrue(thread.IsValid())
bpid = thread.GetStopReasonDataAtIndex(0)
breakpoints.remove(bpid)
target.FindBreakpointByID(bpid).SetEnabled(False)

self.check_unwind_ok(thread)

# We will never hit all breakpoints we set, because of things like
# overflow handling or other unreachable traps. However, it's good to
# have some sanity check that we have hit at least a decent chunk of
# them.
breakpoints_not_hit = len(breakpoints)
self.assertLess(breakpoints_not_hit / num_breakpoints, 0.25)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
func work(_ objects: Any...) {
for object in objects {
print("Processing object of type: \(type(of: object))")
}
}

func use(_ x: Int, _ y: Int) -> Int {
return x &* y &+ x &- y
}

var arr: [Int] = []

func ASYNC___1___() async -> Int {
var a1 = 1, a2 = 2, a3 = 3, a4 = 4, a5 = 5
print("BREAK HERE")
var a6 = 6, a7 = 7, a8 = 8, a9 = 9, a10 = 10
var a11 = 11, a12 = 12, a13 = 13, a14 = 14, a15 = 15
var a16 = 16, a17 = 17, a18 = 18, a19 = 19, a20 = 20
var a21 = 21, a22 = 22, a23 = 23, a24 = 24, a25 = 25
var a26 = 26, a27 = 27, a28 = 28, a29 = 29, a30 = 30
a1 = use(a1, a2)
a3 = use(a3, a4)
a5 = use(a5, a6)
a7 = use(a7, a8)
a9 = use(a9, a10)
a11 = use(a11, a12)
a13 = use(a13, a14)
a15 = use(a15, a16)
a17 = use(a17, a18)
a19 = use(a19, a20)
a21 = use(a21, a22)
a23 = use(a23, a24)
a25 = use(a25, a26)
a27 = use(a27, a28)
a29 = use(a29, a30)
work(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24,a25, a26,a27, a28, a29, a30)
arr = [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24,a25, a26,a27, a28, a29, a30]
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19
}

func ASYNC___2___() async -> Int {
let result = await ASYNC___1___()
return result
}

func ASYNC___3___() async -> Int {
let result = await ASYNC___2___()
return result
}

func ASYNC___4___() async -> Int {
let result = await ASYNC___3___()
return result
}

func ASYNC___5___() async -> Int {
let result = await ASYNC___4___()
return result
}

@main struct Main {
static func main() async {
let result = await ASYNC___5___()
print(result)
}
}