| // |
| // Copyright (c) 2010-2024 Antmicro |
| // |
| // This file is licensed under the MIT License. |
| // Full license text is available in 'licenses/MIT.txt'. |
| // |
| using System; |
| using System.Collections.Generic; |
| using System.Linq; |
| using System.Text; |
| using System.Reflection; |
| using Antmicro.Renode.Core; |
| using Antmicro.Renode.Logging; |
| using Antmicro.Renode.Exceptions; |
| using Antmicro.Renode.Peripherals.CPU; |
| using Antmicro.Renode.Peripherals.Bus; |
| using Antmicro.Renode.Utilities; |
| |
| namespace Antmicro.Renode.Debug |
| { |
| public static class SeL4Extensions |
| { |
| public static void CreateSeL4(this ICpuSupportingGdb @this, ulong? debugThreadNameSyscallId = null) |
| { |
| EmulationManager.Instance.CurrentEmulation.ExternalsManager.AddExternal(new SeL4DebugHelper(@this, debugThreadNameSyscallId), "seL4"); |
| } |
| } |
| |
| public class SeL4DebugHelper : IExternal |
| { |
| public SeL4DebugHelper(ICpuSupportingGdb cpu, ulong? debugThreadNameSyscallId) |
| { |
| if(cpu is Arm) |
| { |
| this.callingConvention = new ArmCallingConvention(cpu); |
| } |
| else if(cpu is RiscV32) |
| { |
| this.callingConvention = new RiscVCallingConvention(cpu); |
| } |
| else |
| { |
| throw new RecoverableException("Only ARM and RV32 based platforms are supported by the seL4 extension"); |
| } |
| |
| this.debugThreadNameSyscall = debugThreadNameSyscallId ?? DefaultDebugThreadNameSyscall; |
| |
| this.cpu = cpu; |
| this.mapping = new Dictionary<ulong, string>(); |
| this.breakpoints = new Dictionary<ulong, HashSet<string>>(); |
| this.temporaryBreakpoints = new Dictionary<ulong, HashSet<string>>(); |
| |
| // Save restore_user_context as we will be using it pretty often |
| this.restoreUserContextAddress = cpu.Bus.GetSymbolAddress("restore_user_context"); |
| |
| // handleUnknownSyscall function is handling seL4_DebugThreadName syscall. |
| // We are using this hook to inspect thread's TCB after it was initialized |
| var handleUnknownSyscallAddress = cpu.Bus.GetSymbolAddress("handleUnknownSyscall"); |
| this.cpu.AddHook(handleUnknownSyscallAddress, HandleUnknownSyscall); |
| // When everything is set up and none of threads is working, this function will be called |
| // It seems to be always called after initialization of all CAmkES components |
| // so we can use it to check "readiness". |
| var idleThreadAddresss = cpu.Bus.GetSymbolAddress("idle_thread"); |
| this.cpu.AddHook(idleThreadAddresss, Finalize); |
| } |
| |
| public string CurrentThread() |
| { |
| if(callingConvention.PrivilegeMode == PrivilegeMode.Supervisor) |
| { |
| return "kernel"; |
| } |
| return CurrentThreadUnsafe(); |
| } |
| |
| public void BreakOnNamingThread(string threadName) |
| { |
| pendingThreadName = threadName; |
| } |
| |
| public void BreakOnExittingUserspace(ExitUserspaceMode mode) |
| { |
| if(mode == exitUserspaceMode) |
| { |
| return; |
| } |
| |
| if(exitUserspaceMode == ExitUserspaceMode.Never) |
| { |
| cpu.AddHook(callingConvention.SyscallTrapAddress, HandleExitUserspace); |
| } |
| else if(mode == ExitUserspaceMode.Never) |
| { |
| cpu.RemoveHook(callingConvention.SyscallTrapAddress, HandleExitUserspace); |
| } |
| |
| exitUserspaceMode = mode; |
| } |
| |
| // Sets the breakpoint on given address in chosen thread |
| // If address is not given, the breakpoint is set right after |
| // on the first instruction after context switch |
| public void SetBreakpoint(string threadName, ulong address = WildcardAddress) |
| { |
| SetBreakpointHelper(threadName, address, breakpoints); |
| } |
| |
| // Similiar to SetBreakpoint, but for temporary breakpoints |
| public void SetTemporaryBreakpoint(string threadName, ulong address = WildcardAddress) |
| { |
| SetBreakpointHelper(threadName, address, temporaryBreakpoints); |
| } |
| |
| // Removes existing breakpoint on given address in chosen thread |
| // If address is not given, then breakpoint which happens on context switch |
| // is removed (see SetBreakpoint). If removeAll is set to true, all breakpoints for |
| // given thread are removed. |
| public void RemoveBreakpoint(string threadName, ulong address = WildcardAddress) |
| { |
| RemoveBreakpointHelper(threadName, address, breakpoints); |
| } |
| |
| public void RemoveTemporaryBreakpoint(string threadName, ulong address = WildcardAddress) |
| { |
| RemoveBreakpointHelper(threadName, address, temporaryBreakpoints); |
| } |
| |
| public void RemoveAllBreakpoints(string threadName = null) |
| { |
| string realThreadName = null; |
| if(threadName != null && !TryGetRealThreadName(threadName, out realThreadName)) |
| { |
| return; |
| } |
| |
| foreach(var item in breakpoints.ToList()) |
| { |
| if(realThreadName != null) |
| { |
| item.Value.Remove(realThreadName); |
| } |
| if(realThreadName == null || item.Value.Count == 0) |
| { |
| breakpoints.Remove(item.Key); |
| } |
| if(GetBreakpointsCount(item.Key) == 0) |
| { |
| RemoveHook(item.Key); |
| } |
| } |
| foreach(var item in temporaryBreakpoints.ToList()) |
| { |
| if(realThreadName != null) |
| { |
| item.Value.Remove(realThreadName); |
| } |
| if(realThreadName == null || item.Value.Count == 0) |
| { |
| temporaryBreakpoints.Remove(item.Key); |
| } |
| if(GetBreakpointsCount(item.Key) == 0) |
| { |
| RemoveHook(item.Key); |
| } |
| } |
| } |
| |
| // Returns table with all the breakpoints. If threadName is set, |
| // returns only breakpoints set in given thread. |
| public string[,] GetBreakpoints(string threadName = null) |
| { |
| var entries = breakpoints.SelectMany(t => t.Value, (entry, thread) => new { Thread = thread, Address = entry.Key, Temporary = false }) |
| .Concat(temporaryBreakpoints.SelectMany(t => t.Value, (entry, thread) => new { Thread = thread, Address = entry.Key, Temporary = true })); |
| |
| if(threadName != null) |
| { |
| entries = entries.Where(x => x.Thread.Contains(threadName)); |
| } |
| var table = new Table().AddRow("Thread", "Address", "Temporary"); |
| table.AddRows(entries, |
| x => x.Thread == AnyThreadName ? "any" : x.Thread, |
| x => x.Address == WildcardAddress ? "any" : "0x{0:X}".FormatWith(x.Address), |
| x => x.Temporary.ToString()); |
| if(exitUserspaceMode != ExitUserspaceMode.Never) |
| { |
| table.AddRow("kernel", "any", (exitUserspaceMode == ExitUserspaceMode.Once).ToString()); |
| } |
| return table.ToArray(); |
| } |
| |
| // Returns list of all the breakpoints in script-friendly format: <THREAD_NAME>:<ADDRESS>\n. |
| // If threadName is set, returns only breakpoints set in given thread. |
| public string GetBreakpointsPlain(string threadName = null) |
| { |
| var entries = breakpoints.SelectMany(t => t.Value, (entry, thread) => new { Thread = thread, Address = entry.Key }) |
| .Concat(temporaryBreakpoints.SelectMany(t => t.Value, (entry, thread) => new { Thread = thread, Address = entry.Key })); |
| |
| if(threadName != null) |
| { |
| entries = entries.Where(x => x.Thread.Contains(threadName)); |
| } |
| var output = entries.Select(entry => "{0}:{1}".FormatWith( |
| entry.Thread, |
| entry.Address == WildcardAddress ? "any" : "0x{0:X}".FormatWith(entry.Address))); |
| return string.Join("\n", output); |
| } |
| |
| public string[] Threads => mapping.Values.ToArray(); |
| |
| public bool Ready { get; private set; } |
| |
| private ulong TryTranslateAddress(ICpuSupportingGdb cpu, ulong virtualAddress) |
| { |
| if(cpu is ICPUWithMMU cpuWithMmu) |
| { |
| virtualAddress = cpuWithMmu.TranslateAddress(virtualAddress, MpuAccess.Read); |
| } |
| return virtualAddress; |
| } |
| |
| private void HandleUnknownSyscall(ICpuSupportingGdb cpu, ulong address) |
| { |
| // Check if seL4_DebugThreadName was called |
| if((callingConvention.FirstArgument & 0xFFFFFFFF) != debugThreadNameSyscall) |
| { |
| return; |
| } |
| |
| // We are in seL4_DebugThreadName handler, we don't need this hook anymore |
| cpu.RemoveHook(address, HandleUnknownSyscall); |
| |
| // This function will now call lookupIPCBuffer and lookupCapAndSlot |
| // We can temporarily hook those functions, and save theirs |
| // return addresses (which will be somewhere in handleUnknownSyscall) |
| // so we can use them later to "scrape" thread information. |
| // Additionally, we are getting address of ksCurThread variable |
| // which stores address of TCB of current thread. |
| var ksCurThreadAddress = cpu.Bus.GetSymbolAddress("ksCurThread"); |
| var lookupIPCBufferAddress = cpu.Bus.GetSymbolAddress("lookupIPCBuffer"); |
| var lookupCapAndSlotAddress = cpu.Bus.GetSymbolAddress("lookupCapAndSlot"); |
| |
| // At this point we are sure, that we are in kernel context and ksCurrThread symbol vaddr |
| // will resolve properly. Therefore we can translate virtual address to physical address |
| // and use it to read memory. That allow us to check current TCB no matter in which |
| // context/privilege mode we are currently in, ignoring MMU completely. |
| ksCurThreadPhysAddress = TryTranslateAddress(cpu, ksCurThreadAddress); |
| |
| cpu.AddHook(lookupCapAndSlotAddress, HandleLookupCapAndSlotAddress); |
| cpu.AddHook(lookupIPCBufferAddress, HandleLookupIPCBuffer); |
| } |
| |
| private void Finalize(ICpuSupportingGdb cpu, ulong address) |
| { |
| cpu.RemoveHook(address, Finalize); |
| Ready = true; |
| this.Log(LogLevel.Info, "Initialization complete."); |
| } |
| |
| private void HandleRestoreUserContext(ICpuSupportingGdb cpu, ulong address) |
| { |
| var threadName = CurrentThreadUnsafe(); |
| if(!DoBreakpointExists(WildcardAddress, threadName)) |
| { |
| return; |
| } |
| |
| ulong tcbAddress = cpu.Bus.ReadDoubleWord(this.ksCurThreadPhysAddress, context: cpu); |
| if(!IsValidAddress(tcbAddress)) |
| { |
| this.Log(LogLevel.Debug, "Got invalid address for TCB, skipping"); |
| return; |
| } |
| |
| var nextPCAddress = TryTranslateAddress(cpu, tcbAddress + callingConvention.TCBNextPCOffset); |
| |
| if(!IsValidAddress(nextPCAddress)) |
| { |
| this.Log(LogLevel.Debug, "NextPC address in TCB is invalid, skipping"); |
| return; |
| } |
| |
| var pc = cpu.Bus.ReadDoubleWord(nextPCAddress, context: cpu); |
| cpu.AddHook(pc, HandleThreadSwitch); |
| } |
| |
| private void HandleThreadSwitch(ICpuSupportingGdb cpu, ulong address) |
| { |
| var threadName = CurrentThread(); |
| // Remove temporary breakpoint if exists |
| ClearTemporaryBreakpoint(WildcardAddress, threadName); |
| // We changed context, remove this hook as we don't need it anymore |
| cpu.RemoveHook(address, HandleThreadSwitch); |
| cpu.Pause(); |
| cpu.EnterSingleStepModeSafely(new HaltArguments(HaltReason.Breakpoint, cpu, address, BreakpointType.MemoryBreakpoint)); |
| } |
| |
| private void HandleBreakpoint(ICpuSupportingGdb cpu, ulong address) |
| { |
| var threadName = CurrentThread(); |
| if(!DoBreakpointExists(address, threadName)) |
| { |
| return; |
| } |
| |
| ClearTemporaryBreakpoint(address, threadName); |
| cpu.Pause(); |
| cpu.EnterSingleStepModeSafely(new HaltArguments(HaltReason.Breakpoint, cpu, address, BreakpointType.MemoryBreakpoint)); |
| } |
| |
| private void HandleExitUserspace(ICpuSupportingGdb cpu, ulong address) |
| { |
| if(callingConvention.PrivilegeMode != PrivilegeMode.Supervisor) |
| { |
| return; |
| } |
| |
| cpu.Pause(); |
| cpu.EnterSingleStepModeSafely(new HaltArguments(HaltReason.Breakpoint, cpu, address, BreakpointType.MemoryBreakpoint)); |
| if(exitUserspaceMode == ExitUserspaceMode.Once) |
| { |
| cpu.RemoveHook(address, HandleExitUserspace); |
| exitUserspaceMode = ExitUserspaceMode.Never; |
| } |
| } |
| |
| private void HandleLookupCapAndSlotAddress(ICpuSupportingGdb cpu, ulong address) |
| { |
| // Save address to instruction in handleUnknownSyscall after call to lookupCapAndSlot |
| cpu.RemoveHook(address, HandleLookupCapAndSlotAddress); |
| cpu.AddHook(callingConvention.ReturnAddress, HandlePostLookupCapAndSlotAddress); |
| } |
| |
| private void HandlePostLookupCapAndSlotAddress(ICpuSupportingGdb cpu, ulong address) |
| { |
| // Return value of lookupCapAndSlot is a structure |
| // with size of two machine words. We are interested in second value |
| // which is address of the capability (in this case TCB) |
| var luRet = callingConvention.ReturnValue; |
| var paddr = TryTranslateAddress(cpu, luRet + 0x4UL); |
| var underlying = cpu.Bus.ReadDoubleWord(paddr, context: cpu); |
| currentTCB = underlying & 0xffffffffffffff00; |
| } |
| |
| private void HandleLookupIPCBuffer(ICpuSupportingGdb cpu, ulong address) |
| { |
| // Save address to instruction in handleUnknownSyscall after call to lookupIPCBuffer |
| cpu.RemoveHook(address, HandleLookupIPCBuffer); |
| cpu.AddHook(callingConvention.ReturnAddress, HandlePostLookupIPCBuffer); |
| } |
| |
| private void HandlePostLookupIPCBuffer(ICpuSupportingGdb cpu, ulong address) |
| { |
| // In A0 register address to IPC buffer is returned. |
| // As seL4_DebugThreadName saves pointer to the string in IPC buffer, |
| // we can now just recover and read it. |
| var paddr = TryTranslateAddress(cpu, callingConvention.ReturnValue + 0x4UL); |
| var buffer = new List<byte>(); |
| |
| // Maximum string size is MaximumMesageLength * size of machine word - 1 |
| for(ulong i = 0; i < MaximumMessageLength * 4 - 1; ++i) |
| { |
| var c = cpu.Bus.ReadByte(paddr + i, context: cpu); |
| if(c == 0) |
| { |
| break; |
| } |
| buffer.Add(c); |
| } |
| |
| var threadName = System.Text.Encoding.ASCII.GetString(buffer.ToArray()); |
| |
| // This function is called _after_ lookupCapAndSlot, therefore we now |
| // have both TCB address and thread's name. We can add it to our list |
| // of known threads. |
| if(!mapping.ContainsKey(currentTCB) || threadName.Contains("_control")) |
| { |
| mapping[currentTCB] = threadName; |
| } |
| |
| // There was pendingThreadName set by WaitForThread function. As we have now all |
| // necessary information for requested thread, we can enter SingleStepMode |
| // (and thus return to prompt in GDB) so user can do something with it, |
| // e.g. create breakpoint on this thread. |
| if(pendingThreadName != null && threadName.Contains(pendingThreadName)) |
| { |
| pendingThreadName = null; |
| cpu.Pause(); |
| cpu.EnterSingleStepModeSafely(new HaltArguments(HaltReason.Breakpoint, cpu, address, BreakpointType.MemoryBreakpoint)); |
| } |
| } |
| |
| private int GetBreakpointsCount(ulong address) |
| { |
| breakpoints.TryGetValue(address, out var bp); |
| temporaryBreakpoints.TryGetValue(address, out var tbp); |
| return (bp?.Count ?? 0) + (tbp?.Count ?? 0); |
| } |
| |
| private bool TryGetRealThreadName(string threadName, out string realThreadName) |
| { |
| if(threadName == AnyThreadName) |
| { |
| realThreadName = AnyThreadName; |
| return true; |
| } |
| |
| realThreadName = mapping.Values.Where(thread => thread.Contains(threadName)).FirstOrDefault(); |
| if(String.IsNullOrEmpty(realThreadName)) |
| { |
| this.Log(LogLevel.Warning, "No thread with name '{0}' found.", threadName); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private bool DoBreakpointExists(ulong address, string threadName) |
| { |
| return (breakpoints.TryGetValue(address, out var bpList) && (bpList.Contains(AnyThreadName) || bpList.Contains(threadName))) || |
| (temporaryBreakpoints.TryGetValue(address, out var tbpList) && (tbpList.Contains(AnyThreadName) || tbpList.Contains(threadName))); |
| } |
| |
| private void AddContextSwitchHook() |
| { |
| cpu.AddHook(restoreUserContextAddress, HandleRestoreUserContext); |
| } |
| |
| private void RemoveContextSwitchHook() |
| { |
| cpu.RemoveHook(restoreUserContextAddress, HandleRestoreUserContext); |
| } |
| |
| private void ClearTemporaryBreakpoint(ulong address, string threadName) |
| { |
| if(!temporaryBreakpoints.ContainsKey(address)) |
| { |
| return; |
| } |
| |
| temporaryBreakpoints[address].Remove(threadName); |
| temporaryBreakpoints[address].Remove(AnyThreadName); |
| |
| if(GetBreakpointsCount(address) == 0) |
| { |
| RemoveHook(address); |
| } |
| } |
| |
| private void SetBreakpointHelper(string threadName, ulong address, Dictionary<ulong, HashSet<string>> breakpointsSource) |
| { |
| if(!TryGetRealThreadName(threadName, out var realThreadName)) |
| { |
| return; |
| } |
| |
| if(!breakpointsSource.ContainsKey(address)) |
| { |
| breakpointsSource.Add(address, new HashSet<string>()); |
| } |
| |
| if(!breakpointsSource[address].Add(realThreadName)) |
| { |
| this.Log(LogLevel.Warning, "This breakpoint already exists."); |
| return; |
| } |
| |
| var breakpointsNum = GetBreakpointsCount(address); |
| |
| // Ignore if we already registered breakpoint for this address |
| if(breakpointsNum != 1) |
| { |
| return; |
| } |
| |
| AddHook(address); |
| } |
| |
| private void RemoveBreakpointHelper(string threadName, ulong address, Dictionary<ulong, HashSet<string>> breakpointsSource) |
| { |
| if(!breakpointsSource.TryGetValue(address, out var breakpoint)) |
| { |
| return; |
| } |
| |
| if(!TryGetRealThreadName(threadName, out var realThreadName)) |
| { |
| return; |
| } |
| |
| breakpoint.Remove(realThreadName); |
| var breakpointsNum = GetBreakpointsCount(address); |
| if(breakpointsNum != 0) |
| { |
| return; |
| } |
| |
| RemoveHook(address); |
| } |
| |
| private void AddHook(ulong address) |
| { |
| if(address != WildcardAddress) |
| { |
| cpu.AddHook(address, HandleBreakpoint); |
| } |
| else |
| { |
| AddContextSwitchHook(); |
| } |
| } |
| |
| private void RemoveHook(ulong address) |
| { |
| if(address != WildcardAddress) |
| { |
| cpu.RemoveHook(address, HandleBreakpoint); |
| } |
| else |
| { |
| RemoveContextSwitchHook(); |
| } |
| } |
| |
| private bool IsValidAddress(ulong address) |
| { |
| return !(address == 0x00000000 || address == 0xFFFFFFFF); |
| } |
| |
| private string CurrentThreadUnsafe() |
| { |
| var tcb = cpu.Bus.ReadDoubleWord(this.ksCurThreadPhysAddress, context: cpu); |
| if(mapping.ContainsKey(tcb)) |
| { |
| return mapping[tcb]; |
| } |
| return "unknown"; |
| } |
| |
| public enum ExitUserspaceMode |
| { |
| Never, |
| Once, |
| Always, |
| } |
| |
| private const uint DefaultDebugThreadNameSyscall = 0xfffffff2; |
| private const string AnyThreadName = "<any>"; |
| private const uint WildcardAddress = 0x00000000; |
| private const uint MaximumMessageLength = 120; |
| |
| private readonly Dictionary<ulong, HashSet<string>> breakpoints; |
| private readonly Dictionary<ulong, HashSet<string>> temporaryBreakpoints; |
| private readonly Dictionary<ulong, string> mapping; |
| private readonly ICpuSupportingGdb cpu; |
| private readonly ICallingConvention callingConvention; |
| private readonly ulong debugThreadNameSyscall; |
| |
| private ExitUserspaceMode exitUserspaceMode; |
| private bool breakpointsEnabled; |
| private ulong ksCurThreadPhysAddress; |
| private ulong restoreUserContextAddress; |
| private string pendingThreadName; |
| private ulong currentTCB; |
| |
| private interface ICallingConvention |
| { |
| ulong FirstArgument { get; } |
| ulong ReturnValue { get; } |
| ulong ReturnAddress { get; } |
| ulong SyscallTrapAddress { get; } |
| ulong TCBNextPCOffset { get; } |
| PrivilegeMode PrivilegeMode { get; } |
| } |
| |
| private enum PrivilegeMode |
| { |
| Userspace, |
| Supervisor, |
| Other, |
| } |
| |
| private class RiscVCallingConvention : ICallingConvention |
| { |
| public RiscVCallingConvention(ICpuSupportingGdb cpu) |
| { |
| this.cpu = cpu; |
| // Assumes that symbols for kernel are loaded |
| syscallTrapAddress = cpu.Bus.GetSymbolAddress("trap_entry"); |
| } |
| |
| public ulong FirstArgument => cpu.A[0]; |
| public ulong ReturnValue => cpu.A[0]; |
| public ulong ReturnAddress => cpu.RA; |
| public ulong SyscallTrapAddress => syscallTrapAddress; |
| public PrivilegeMode PrivilegeMode |
| { |
| get |
| { |
| switch((byte)cpu.PRIV) |
| { |
| case 0b00: |
| return PrivilegeMode.Userspace; |
| case 0b01: |
| return PrivilegeMode.Supervisor; |
| default: |
| return PrivilegeMode.Other; |
| } |
| } |
| } |
| public ulong TCBNextPCOffset => 34 * 4; |
| |
| private readonly ulong syscallTrapAddress; |
| private readonly dynamic cpu; |
| } |
| |
| private class ArmCallingConvention : ICallingConvention |
| { |
| public ArmCallingConvention(ICpuSupportingGdb cpu) |
| { |
| this.cpu = (Arm)cpu; |
| // Assumes that symbols for kernel are loaded |
| syscallTrapAddress = cpu.Bus.GetSymbolAddress("arm_swi_syscall"); |
| } |
| |
| public ulong FirstArgument => cpu.R[0]; |
| public ulong ReturnValue => cpu.R[0]; |
| public ulong ReturnAddress => cpu.R[14]; |
| public ulong SyscallTrapAddress => syscallTrapAddress; |
| public PrivilegeMode PrivilegeMode |
| { |
| get |
| { |
| switch(cpu.CPSR & 0xfUL) |
| { |
| case 0b00: |
| return PrivilegeMode.Userspace; |
| case 0b11: |
| return PrivilegeMode.Supervisor; |
| default: |
| return PrivilegeMode.Other; |
| } |
| } |
| } |
| public ulong TCBNextPCOffset => 15 * 4; |
| |
| private readonly ulong syscallTrapAddress; |
| private readonly Arm cpu; |
| } |
| } |
| } |
| |