Kernel: Share Processor::capture_stack_trace() between architectures

This makes Processor::capture_stack_trace() work on all our
architectures. For this function to work on AArch64 and RISC-V, the
frame pointer has to be saved during context switches.

AArch64 and RISC-V don't support SMP yet, so the code for getting a
backtrace for processes running on other cores is guarded behind a
'#if ARCH(X86_64)'.
This commit is contained in:
Sönke Holz 2024-08-15 22:12:34 +02:00 committed by Nico Weber
parent fa39086922
commit e7b8eed005
5 changed files with 141 additions and 140 deletions

View file

@ -4,9 +4,12 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StackUnwinder.h>
#include <Kernel/Arch/Processor.h>
#include <Kernel/Arch/TrapFrame.h>
#include <Kernel/Interrupts/InterruptDisabler.h>
#include <Kernel/Library/StdLib.h>
#include <Kernel/Memory/ScopedAddressSpaceSwitcher.h>
#include <Kernel/Sections.h>
#include <Kernel/Tasks/Scheduler.h>
#include <Kernel/Tasks/Thread.h>
@ -132,4 +135,135 @@ void do_context_first_init(Thread* from_thread, Thread* to_thread)
Scheduler::leave_on_first_switch(InterruptsState::Disabled);
}
template<typename T>
ErrorOr<Vector<FlatPtr, 32>> ProcessorBase<T>::capture_stack_trace(Thread& thread, size_t max_frames)
{
FlatPtr frame_ptr = 0, pc = 0;
Vector<FlatPtr, 32> stack_trace;
auto walk_stack = [&](FlatPtr frame_ptr) -> ErrorOr<void> {
constexpr size_t max_stack_frames = 4096;
bool is_walking_userspace_stack = false;
TRY(stack_trace.try_append(pc));
TRY(AK::unwind_stack_from_frame_pointer(
frame_ptr,
[&is_walking_userspace_stack](FlatPtr address) -> ErrorOr<FlatPtr> {
if (!Memory::is_user_address(VirtualAddress { address })) {
if (is_walking_userspace_stack) {
dbgln("SHENANIGANS! Userspace stack points back into kernel memory");
return EFAULT;
}
} else {
is_walking_userspace_stack = true;
}
FlatPtr value;
if (Memory::is_user_range(VirtualAddress { address }, sizeof(FlatPtr))) {
TRY(copy_from_user(&value, bit_cast<FlatPtr*>(address)));
} else {
void* fault_at;
if (!safe_memcpy(&value, bit_cast<FlatPtr*>(address), sizeof(FlatPtr), fault_at))
return EFAULT;
}
return value;
},
[&stack_trace, max_frames](AK::StackFrame stack_frame) -> ErrorOr<IterationDecision> {
if (stack_trace.size() >= max_stack_frames || (max_frames != 0 && stack_trace.size() >= max_frames))
return IterationDecision::Break;
TRY(stack_trace.try_append(stack_frame.return_address));
return IterationDecision::Continue;
}));
return {};
};
auto capture_current_thread = [&]() {
frame_ptr = bit_cast<FlatPtr>(__builtin_frame_address(0));
pc = bit_cast<FlatPtr>(__builtin_return_address(0));
return walk_stack(frame_ptr);
};
// Since the thread may be running on another processor, there
// is a chance a context switch may happen while we're trying
// to get it. It also won't be entirely accurate and merely
// reflect the status at the last context switch.
SpinlockLocker lock(g_scheduler_lock);
if (&thread == Processor::current_thread()) {
VERIFY(thread.state() == Thread::State::Running);
// Leave the scheduler lock. If we trigger page faults we may
// need to be preempted. Since this is our own thread it won't
// cause any problems as the stack won't change below this frame.
lock.unlock();
TRY(capture_current_thread());
} else if (thread.is_active()) {
#if ARCH(X86_64)
VERIFY(thread.cpu() != Processor::current_id());
// If this is the case, the thread is currently running
// on another processor. We can't trust the kernel stack as
// it may be changing at any time. We need to probably send
// an IPI to that processor, have it walk the stack and wait
// until it returns the data back to us
auto& proc = Processor::current();
ErrorOr<void> result;
Processor::smp_unicast(
thread.cpu(),
[&]() {
dbgln("CPU[{}] getting stack for cpu #{}", Processor::current_id(), proc.id());
ScopedAddressSpaceSwitcher switcher(thread.process());
VERIFY(&Processor::current() != &proc);
VERIFY(&thread == Processor::current_thread());
// NOTE: Because the other processor is still holding the
// scheduler lock while waiting for this callback to finish,
// the current thread on the target processor cannot change
// TODO: What to do about page faults here? We might deadlock
// because the other processor is still holding the
// scheduler lock...
result = capture_current_thread();
},
false);
TRY(result);
#elif ARCH(AARCH64) || ARCH(RISCV64)
VERIFY_NOT_REACHED(); // We don't support SMP on AArch64 and RISC-V yet, so this should be unreachable.
#else
# error Unknown architecture
#endif
} else {
switch (thread.state()) {
case Thread::State::Running:
VERIFY_NOT_REACHED(); // should have been handled above
case Thread::State::Runnable:
case Thread::State::Stopped:
case Thread::State::Blocked:
case Thread::State::Dying:
case Thread::State::Dead: {
ScopedAddressSpaceSwitcher switcher(thread.process());
auto& regs = thread.regs();
pc = regs.ip();
frame_ptr = regs.frame_pointer();
// TODO: We need to leave the scheduler lock here, but we also
// need to prevent the target thread from being run while
// we walk the stack
lock.unlock();
TRY(walk_stack(frame_ptr));
break;
}
default:
dbgln("Cannot capture stack trace for thread {} in state {}", thread, thread.state_string());
break;
}
}
return stack_trace;
}
template ErrorOr<Vector<FlatPtr, 32>> ProcessorBase<Processor>::capture_stack_trace(Thread&, size_t);
}

View file

@ -30,6 +30,5 @@ template void ProcessorBase<Processor>::initialize_context_switching(Thread& ini
template void ProcessorBase<Processor>::switch_context(Thread*& from_thread, Thread*& to_thread);
template void ProcessorBase<Processor>::assume_context(Thread& thread, InterruptsState new_interrupts_state);
template FlatPtr ProcessorBase<Processor>::init_context(Thread& thread, bool leave_crit);
template ErrorOr<Vector<FlatPtr, 32>> ProcessorBase<Processor>::capture_stack_trace(Thread& thread, size_t max_frames);
template u32 ProcessorBase<Processor>::smp_wake_n_idle_processors(u32 wake_count);
}

View file

@ -165,6 +165,7 @@ void ProcessorBase<T>::switch_context(Thread*& from_thread, Thread*& to_thread)
"str x30, [sp, #(30 * 8)] \n"
"mov x0, sp \n"
"str x0, %[from_sp] \n"
"str fp, %[from_fp] \n"
"ldr x0, =1f \n"
"str x0, %[from_ip] \n"
@ -213,6 +214,7 @@ void ProcessorBase<T>::switch_context(Thread*& from_thread, Thread*& to_thread)
:
[from_ip] "=m"(from_thread->regs().elr_el1),
[from_sp] "=m"(from_thread->regs().sp_el0),
[from_fp] "=m"(from_thread->regs().x[29]),
"=m"(from_thread),
"=m"(to_thread)
@ -364,15 +366,6 @@ void ProcessorBase<T>::exit_trap(TrapFrame& trap)
check_invoke_scheduler();
}
template<typename T>
ErrorOr<Vector<FlatPtr, 32>> ProcessorBase<T>::capture_stack_trace(Thread& thread, size_t max_frames)
{
(void)thread;
(void)max_frames;
dbgln("FIXME: Implement Processor::capture_stack_trace() for AArch64");
return Vector<FlatPtr, 32> {};
}
NAKED void thread_context_first_enter(void)
{
asm(

View file

@ -258,6 +258,10 @@ void ProcessorBase<T>::switch_context(Thread*& from_thread, Thread*& to_thread)
// Store current sp as from_thread's sp.
"sd sp, %[from_sp] \n"
// Store current fp as from_thread's sp.
// This is needed to make capture_stack_trace() work.
"sd fp, %[from_fp] \n"
// Set from_thread's pc to label "1"
"la t0, 1f \n"
"sd t0, %[from_ip] \n"
@ -327,6 +331,7 @@ void ProcessorBase<T>::switch_context(Thread*& from_thread, Thread*& to_thread)
:
[from_ip] "=m"(from_thread->regs().pc),
[from_sp] "=m"(from_thread->regs().x[1]),
[from_fp] "=m"(from_thread->regs().x[7]),
"=m"(from_thread),
"=m"(to_thread)
@ -473,13 +478,6 @@ void ProcessorBase<T>::exit_trap(TrapFrame& trap)
check_invoke_scheduler();
}
template<typename T>
ErrorOr<Vector<FlatPtr, 32>> ProcessorBase<T>::capture_stack_trace(Thread&, size_t)
{
dbgln("FIXME: Implement Processor::capture_stack_trace() for riscv64");
return Vector<FlatPtr, 32> {};
}
extern "C" void context_first_init(Thread* from_thread, Thread* to_thread);
extern "C" void context_first_init(Thread* from_thread, Thread* to_thread)
{

View file

@ -758,129 +758,6 @@ DescriptorTablePointer const& Processor::get_gdtr()
return m_gdtr;
}
template<typename T>
ErrorOr<Vector<FlatPtr, 32>> ProcessorBase<T>::capture_stack_trace(Thread& thread, size_t max_frames)
{
FlatPtr frame_ptr = 0, ip = 0;
Vector<FlatPtr, 32> stack_trace;
auto walk_stack = [&](FlatPtr frame_ptr) -> ErrorOr<void> {
constexpr size_t max_stack_frames = 4096;
bool is_walking_userspace_stack = false;
TRY(stack_trace.try_append(ip));
TRY(AK::unwind_stack_from_frame_pointer(
frame_ptr,
[&is_walking_userspace_stack](FlatPtr address) -> ErrorOr<FlatPtr> {
if (!Memory::is_user_address(VirtualAddress { address })) {
if (is_walking_userspace_stack) {
dbgln("SHENANIGANS! Userspace stack points back into kernel memory");
return EFAULT;
}
} else {
is_walking_userspace_stack = true;
}
FlatPtr value;
if (Memory::is_user_range(VirtualAddress { address }, sizeof(FlatPtr))) {
TRY(copy_from_user(&value, bit_cast<FlatPtr*>(address)));
} else {
void* fault_at;
if (!safe_memcpy(&value, bit_cast<FlatPtr*>(address), sizeof(FlatPtr), fault_at))
return EFAULT;
}
return value;
},
[&stack_trace, max_frames](AK::StackFrame stack_frame) -> ErrorOr<IterationDecision> {
if (stack_trace.size() >= max_stack_frames || (max_frames != 0 && stack_trace.size() >= max_frames))
return IterationDecision::Break;
TRY(stack_trace.try_append(stack_frame.return_address));
return IterationDecision::Continue;
}));
return {};
};
auto capture_current_thread = [&]() {
frame_ptr = (FlatPtr)__builtin_frame_address(0);
ip = (FlatPtr)__builtin_return_address(0);
return walk_stack(frame_ptr);
};
// Since the thread may be running on another processor, there
// is a chance a context switch may happen while we're trying
// to get it. It also won't be entirely accurate and merely
// reflect the status at the last context switch.
SpinlockLocker lock(g_scheduler_lock);
if (&thread == Processor::current_thread()) {
VERIFY(thread.state() == Thread::State::Running);
// Leave the scheduler lock. If we trigger page faults we may
// need to be preempted. Since this is our own thread it won't
// cause any problems as the stack won't change below this frame.
lock.unlock();
TRY(capture_current_thread());
} else if (thread.is_active()) {
VERIFY(thread.cpu() != Processor::current_id());
// If this is the case, the thread is currently running
// on another processor. We can't trust the kernel stack as
// it may be changing at any time. We need to probably send
// an IPI to that processor, have it walk the stack and wait
// until it returns the data back to us
auto& proc = Processor::current();
ErrorOr<void> result;
Processor::smp_unicast(
thread.cpu(),
[&]() {
dbgln("CPU[{}] getting stack for cpu #{}", Processor::current_id(), proc.id());
ScopedAddressSpaceSwitcher switcher(thread.process());
VERIFY(&Processor::current() != &proc);
VERIFY(&thread == Processor::current_thread());
// NOTE: Because the other processor is still holding the
// scheduler lock while waiting for this callback to finish,
// the current thread on the target processor cannot change
// TODO: What to do about page faults here? We might deadlock
// because the other processor is still holding the
// scheduler lock...
result = capture_current_thread();
},
false);
TRY(result);
} else {
switch (thread.state()) {
case Thread::State::Running:
VERIFY_NOT_REACHED(); // should have been handled above
case Thread::State::Runnable:
case Thread::State::Stopped:
case Thread::State::Blocked:
case Thread::State::Dying:
case Thread::State::Dead: {
ScopedAddressSpaceSwitcher switcher(thread.process());
auto& regs = thread.regs();
ip = regs.ip();
frame_ptr = regs.rbp;
// TODO: We need to leave the scheduler lock here, but we also
// need to prevent the target thread from being run while
// we walk the stack
lock.unlock();
TRY(walk_stack(frame_ptr));
break;
}
default:
dbgln("Cannot capture stack trace for thread {} in state {}", thread, thread.state_string());
break;
}
}
return stack_trace;
}
ProcessorContainer& Processor::processors()
{
return s_processors;