gdbstub_plugin/src/debugger.cpp

946 lines
21 KiB
C++
Raw Normal View History

2018-09-24 10:43:20 +02:00
#include "cafe/coreinit.h"
#include "cafe/nsysnet.h"
#include "cafe/vpad.h"
#include "kernel.h"
#include "debugger.h"
#include "exceptions.h"
#include "screen.h"
#include "input.h"
#include <cstring>
bool BreakPoint::isRange(uint32_t addr, uint32_t length) {
return address >= addr && address <= addr + length - 1;
}
BreakPoint *BreakPointMgr::find(uint32_t addr, bool includeSpecial) {
BreakPoint *bp = breakpoints.find(addr);
if (!bp && includeSpecial) {
bp = special.find(addr);
}
return bp;
}
BreakPoint *BreakPointMgr::findRange(uint32_t addr, uint32_t length, int *index, bool includeSpecial) {
BreakPoint *bp = breakpoints.findRange(addr, length, index);
if (bp) {
return bp;
}
if (includeSpecial) {
int temp = *index - breakpoints.size();
bp = special.findRange(addr, length, &temp);
*index = temp + breakpoints.size();
return bp;
}
return nullptr;
}
SpecialBreakPoint *BreakPointMgr::findSpecial(uint32_t addr, OSThread *thread) {
int index = 0;
SpecialBreakPoint *bp = special.findRange(addr, 4, &index);
while (bp) {
if (bp->thread == thread) {
return bp;
}
bp = special.findRange(addr, 4, &index);
}
return nullptr;
}
bool BreakPointMgr::isCustom(uint32_t addr) {
return find(addr, false);
}
bool BreakPointMgr::isSpecial(uint32_t addr) {
return find(addr, true) && !find(addr, false);
}
bool BreakPointMgr::isSoftware(uint32_t addr) {
uint32_t instr = getInstr(addr);
uint32_t opcode = instr >> 26;
if (opcode == 3) { //twi
return true;
}
else if (opcode == 31) {
return (instr & 0x7FF) == 8; //tw
}
return false;
}
void BreakPointMgr::disable(BreakPoint *bp) {
uint32_t address = bp->address;
if (bp->address) {
bp->address = 0;
if (!find(address, true)) {
KernelWriteU32(address, bp->instruction);
}
bp->instruction = 0;
}
}
void BreakPointMgr::enable(BreakPoint *bp, uint32_t addr) {
BreakPoint *other = find(addr, true);
if (other) {
bp->instruction = other->instruction;
}
else {
bp->instruction = *(uint32_t *)addr;
KernelWriteU32(addr, TRAP);
}
bp->address = addr;
}
void BreakPointMgr::init() {
OSInitMutex(&mutex);
}
void BreakPointMgr::lock() {
OSLockMutex(&mutex);
}
void BreakPointMgr::unlock() {
OSUnlockMutex(&mutex);
}
void BreakPointMgr::cleanup() {
lock();
breakpoints.cleanup();
special.cleanup();
unlock();
}
void BreakPointMgr::read(void *buffer, uint32_t addr, uint32_t length) {
lock();
memcpy(buffer, (void *)addr, length);
int index = 0;
BreakPoint *bp = findRange(addr, length, &index, true);
while (bp) {
uint32_t offset = bp->address - addr;
char *bufptr = (char *)buffer + offset;
if (bp->address > addr + length - 4) {
uint32_t value = bp->instruction;
for (int i = 0; i < length - offset; i++) {
bufptr[i] = value >> 24;
value <<= 8;
}
}
else {
*(uint32_t *)bufptr = bp->instruction;
}
bp = findRange(addr, length, &index, true);
}
unlock();
}
void BreakPointMgr::write(const void *buffer, uint32_t addr, uint32_t length) {
lock();
length = length & ~3;
int index = 0;
if (!findRange(addr, length, &index, true)) {
KernelWrite(addr, buffer, length);
}
else {
for (uint32_t i = 0; i < length; i += 4) {
uint32_t value = *(uint32_t *)((char *)buffer + i);
int index = 0;
BreakPoint *bp = findRange(addr + i, 4, &index, true);
if (!bp) {
KernelWriteU32(addr + i, value);
}
else {
while (bp) {
bp->instruction = value;
bp = findRange(addr + i, 4, &index, true);
}
}
}
}
unlock();
}
void BreakPointMgr::toggle(uint32_t addr) {
lock();
BreakPoint *bp = find(addr, false);
if (bp) {
disable(bp);
}
else {
BreakPoint *bp = breakpoints.alloc();
bp->isSpecial = false;
enable(bp, addr);
}
unlock();
}
uint32_t BreakPointMgr::getInstr(uint32_t addr) {
lock();
uint32_t instruction;
BreakPoint *bp = find(addr, true);
if (bp) {
instruction = bp->instruction;
}
else {
instruction = *(uint32_t *)addr;
}
unlock();
return instruction;
}
void BreakPointMgr::clearSpecial(OSThread *thread) {
lock();
for (int i = 0; i < special.size(); i++) {
SpecialBreakPoint *bp = special[i];
if (bp->thread == thread) {
disable(bp);
}
}
unlock();
}
void BreakPointMgr::predictStep(ExceptionState *state, bool stepOver) {
lock();
uint32_t address = state->context.srr0;
uint32_t instruction = getInstr(address);
uint32_t target1 = address + 4;
uint32_t target2 = 0;
uint8_t opcode = instruction >> 26;
if (opcode == 18) { //Branch
bool AA = instruction & 2;
bool LK = instruction & 1;
uint32_t LI = instruction & 0x3FFFFFC;
if (LI & 0x2000000) LI -= 0x4000000;
if (!LK || !stepOver) {
if (AA) target1 = LI;
else {
target1 = address + LI;
}
}
}
else if (opcode == 16) { //Conditional branch
bool AA = instruction & 2;
bool LK = instruction & 1;
uint32_t BD = instruction & 0xFFFC;
if (BD & 0x8000) BD -= 0x10000;
if (!LK || !stepOver) {
if (AA) target2 = BD;
else {
target2 = address + BD;
}
}
}
else if (opcode == 19) { //Conditional branch to lr/ctr
uint16_t XO = (instruction >> 1) & 0x3FF;
bool LK = instruction & 1;
if (!LK || !stepOver) {
if (XO == 16) target2 = state->context.lr;
else if (XO == 528) target2 = state->context.ctr;
}
}
SpecialBreakPoint *bp = special.alloc();
bp->isSpecial = true;
bp->thread = state->thread;
enable(bp, target1);
if (target2) {
SpecialBreakPoint *bp = special.alloc();
bp->isSpecial = true;
bp->thread = state->thread;
enable(bp, target2);
}
unlock();
}
bool ExceptionState::isBreakpoint() {
return type == PROGRAM && context.srr1 & 0x20000;
}
void ExceptionState::resume() {
OSLoadContext(&context);
}
void ExceptionMgr::init() {
OSInitMutex(&mutex);
}
void ExceptionMgr::lock() {
OSLockMutex(&mutex);
}
void ExceptionMgr::unlock() {
OSUnlockMutex(&mutex);
}
void ExceptionMgr::cleanup() {
OSMessage message;
message.message = Debugger::STEP_CONTINUE;
lock();
for (int i = 0; i < list.size(); i++) {
ExceptionState *state = list[i];
if (state->isPaused) {
OSSendMessage(&state->queue, &message, OS_MESSAGE_FLAGS_NONE);
}
2018-09-24 10:43:20 +02:00
}
unlock();
}
ExceptionState *ExceptionMgr::find(OSThread *thread) {
lock();
for (int i = 0; i < list.size(); i++) {
ExceptionState *state = list[i];
if (state->thread == thread) {
unlock();
return state;
}
}
unlock();
return nullptr;
}
ExceptionState *ExceptionMgr::findOrCreate(OSThread *thread) {
lock();
ExceptionState *state = find(thread);
if (!state) {
state = new ExceptionState();
state->thread = thread;
state->isPaused = false;
2018-09-24 10:43:20 +02:00
OSInitMessageQueue(&state->queue, &state->message, 1);
list.push_back(state);
}
unlock();
return state;
}
uint32_t StepMgr::buffer[96];
void StepMgr::init() {
OSInitMutex(&mutex);
usedMask = 0;
}
void StepMgr::lock() {
OSLockMutex(&mutex);
}
void StepMgr::unlock() {
OSUnlockMutex(&mutex);
}
uint32_t *StepMgr::alloc() {
lock();
int index = 0;
uint32_t mask = usedMask;
while (mask & 1) {
mask >>= 1;
index++;
}
usedMask |= 1 << index;
unlock();
if (index == 32) {
OSFatal("Displaced step allocation failed");
}
return &buffer[index * 3];
}
void StepMgr::free(int index) {
lock();
usedMask &= ~(1 << index);
unlock();
}
void StepMgr::branchConditional(ExceptionState *state, uint32_t instruction, uint32_t target, bool checkCtr) {
Bits<5> BO = (instruction >> 21) & 0x1F;
Bits<32> CR = state->context.cr;
uint32_t BI = (instruction >> 16) & 0x1F;
bool LK = instruction & 1;
if (LK) {
state->context.lr = state->context.srr0 + 4;
}
bool ctr_ok = true;
if (checkCtr) {
if (!BO[2]) {
state->context.ctr--;
}
ctr_ok = BO[2] || ((state->context.ctr != 0) ^ BO[3]);
}
bool cond_ok = BO[0] || (CR[BI] == BO[1]);
if (ctr_ok && cond_ok) {
state->context.srr0 = target;
}
}
void StepMgr::singleStep(ExceptionState *state, uint32_t instruction) {
uint8_t opcode = instruction >> 26;
if (opcode == 18) { //Branch
bool AA = instruction & 2;
bool LK = instruction & 1;
uint32_t LI = instruction & 0x3FFFFFC;
if (LI & 0x2000000) LI -= 0x4000000;
if (LK) {
state->context.lr = state->context.srr0 + 4;
}
if (AA) state->context.srr0 = LI;
else {
state->context.srr0 += LI;
}
return;
}
if (opcode == 16) { //Conditional branch
bool AA = instruction & 2;
uint32_t BD = instruction & 0xFFFC;
if (BD & 0x8000) BD -= 0x10000;
uint32_t target;
if (AA) target = BD;
else {
target = state->context.srr0 + BD;
}
branchConditional(state, instruction, target, true);
return;
}
if (opcode == 19) { //Conditional branch to lr/ctr
uint16_t XO = (instruction >> 1) & 0x3FF;
uint32_t target;
if (XO == 16) {
branchConditional(state, instruction, state->context.lr, true);
return;
}
else if (XO == 528) {
branchConditional(state, instruction, state->context.ctr, false);
return;
}
}
uint32_t *ptr = alloc();
ptr[0] = instruction;
ptr[1] = TRAP;
ptr[2] = state->context.srr0;
DCFlushRange(ptr, 12);
ICInvalidateRange(ptr, 12);
state->context.srr0 = (uint32_t)ptr;
}
void StepMgr::handleBreakPoint(ExceptionState *state) {
uint32_t start = (uint32_t)buffer;
uint32_t end = (uint32_t)buffer + sizeof(buffer);
uint32_t addr = state->context.srr0;
if (addr >= start && addr < end) {
int offset = addr - start;
if (offset % 12 != 4) {
char buffer[100];
snprintf(buffer, 100, "Unexpected srr0 after displaced step: %08X", addr);
OSFatal(buffer);
}
int index = offset / 12;
state->context.srr0 = buffer[index * 3 + 2] + 4;
free(index);
state->resume();
}
}
void StepMgr::adjustAddress(ExceptionState *state) {
uint32_t start = (uint32_t)buffer;
uint32_t end = (uint32_t)buffer + sizeof(buffer);
uint32_t addr = state->context.srr0;
if (addr >= start && addr < end) {
int index = (addr - start) / 12;
state->context.srr0 = buffer[index * 3 + 2];
}
}
bool Debugger::checkDataRead(uint32_t addr, uint32_t length) {
uint32_t memStart, memSize;
OSGetMemBound(MEM2, &memStart, &memSize);
uint32_t memEnd = memStart + memSize;
return addr >= 0x10000000 && addr + length <= memEnd;
}
Debugger::StepCommand Debugger::notifyBreak(ExceptionState *state) {
OSMessage message;
message.message = ExceptionState::PROGRAM;
message.args[0] = (uint32_t)&state->context;
message.args[1] = sizeof(OSContext);
message.args[2] = (uint32_t)state->thread;
OSSendMessage(&eventQueue, &message, OS_MESSAGE_FLAGS_BLOCKING);
state->isPaused = true;
2018-09-24 10:43:20 +02:00
OSReceiveMessage(&state->queue, &message, OS_MESSAGE_FLAGS_BLOCKING);
state->isPaused = false;
2018-09-24 10:43:20 +02:00
return (StepCommand)message.message;
}
void Debugger::resumeBreakPoint(ExceptionState *state) {
uint32_t address = state->context.srr0;
if (breakpoints.isSoftware(address)) {
state->context.srr0 += 4;
state->resume();
}
uint32_t instruction = breakpoints.getInstr(state->context.srr0);
stepper.singleStep(state, instruction);
state->resume();
}
void Debugger::processBreakPoint(ExceptionState *state) {
StepCommand command = notifyBreak(state);
if (command == STEP_INTO || command == STEP_OVER) {
breakpoints.predictStep(state, command == STEP_OVER);
}
}
void Debugger::handleBreakPoint(ExceptionState *state) {
if (firstTrap) {
firstTrap = false;
Screen screen;
screen.init();
screen.drawText(
0, 0, "Waiting for debugger connection.\n"
"Press the home button to continue without debugger.\n"
"You can still connect while the game is running."
);
screen.flip();
while (!connected) {
uint32_t buttons = GetInput(VPAD_BUTTON_HOME);
if (buttons) {
state->context.srr0 += 4;
state->resume();
}
}
}
stepper.handleBreakPoint(state);
if (!connected) {
handleFatalCrash(&state->context, state->type);
2018-09-24 10:43:20 +02:00
}
uint32_t addr = state->context.srr0;
bool trap;
breakpoints.lock();
trap = breakpoints.isCustom(addr) || breakpoints.isSoftware(addr) ||
breakpoints.findSpecial(addr, state->thread);
breakpoints.unlock();
breakpoints.clearSpecial(state->thread);
if (trap) {
processBreakPoint(state);
}
resumeBreakPoint(state);
}
void Debugger::handleCrash(ExceptionState *state) {
2018-09-24 10:43:20 +02:00
stepper.adjustAddress(state);
if (connected) {
OSMessage message;
message.message = state->type;
message.args[0] = (uint32_t)&state->context;
message.args[1] = sizeof(OSContext);
message.args[2] = (uint32_t)state->thread;
OSSendMessage(&eventQueue, &message, OS_MESSAGE_FLAGS_BLOCKING);
while (true) {
OSReceiveMessage(&state->queue, &message, OS_MESSAGE_FLAGS_BLOCKING);
}
}
else {
handleFatalCrash(&state->context, state->type);
}
}
void Debugger::handleFatalCrash(OSContext *context, ExceptionState::Type type) {
const char *name;
if (type == ExceptionState::DSI) name = "A DSI";
else if (type == ExceptionState::ISI) name = "An ISI";
else {
name = "A program";
2018-09-24 10:43:20 +02:00
}
DumpContext(context, name);
2018-09-24 10:43:20 +02:00
}
void Debugger::handleException(OSContext *context, ExceptionState::Type type) {
OSThread *thread = OSGetCurrentThread();
if (thread == serverThread) {
handleFatalCrash(context, type);
}
2018-09-24 10:43:20 +02:00
ExceptionState *state = exceptions.findOrCreate(thread);
memcpy(&state->context, context, sizeof(OSContext));
state->type = type;
delete context;
if (state->isBreakpoint()) {
handleBreakPoint(state);
}
else {
handleCrash(state);
2018-09-24 10:43:20 +02:00
}
}
void Debugger::exceptionHandler(OSContext *context, ExceptionState::Type type) {
debugger->handleException(context, type);
}
bool Debugger::dsiHandler(OSContext *context) {
OSContext *info = new OSContext();
memcpy(info, context, sizeof(OSContext));
context->srr0 = (uint32_t)exceptionHandler;
context->gpr[3] = (uint32_t)info;
context->gpr[4] = ExceptionState::DSI;
return true;
}
bool Debugger::isiHandler(OSContext *context) {
OSContext *info = new OSContext();
memcpy(info, context, sizeof(OSContext));
context->srr0 = (uint32_t)exceptionHandler;
context->gpr[3] = (uint32_t)info;
context->gpr[4] = ExceptionState::ISI;
return true;
}
bool Debugger::programHandler(OSContext *context) {
OSContext *info = new OSContext();
memcpy(info, context, sizeof(OSContext));
context->srr0 = (uint32_t)exceptionHandler;
context->gpr[3] = (uint32_t)info;
context->gpr[4] = ExceptionState::PROGRAM;
return true;
}
void Debugger::cleanup() {
breakpoints.cleanup();
exceptions.cleanup();
OSMessage message;
while (OSReceiveMessage(&eventQueue, &message, OS_MESSAGE_FLAGS_NONE));
}
void Debugger::mainLoop(Client *client) {
while (true) {
uint8_t cmd;
if (!client->recvall(&cmd, 1)) return;
if (cmd == COMMAND_CLOSE) return;
else if (cmd == COMMAND_READ) {
uint32_t addr, length;
if (!client->recvall(&addr, 4)) return;
if (!client->recvall(&length, 4)) return;
char *buffer = new char[length];
breakpoints.read(buffer, addr, length);
if (!client->sendall(buffer, length)) {
delete buffer;
return;
}
delete buffer;
}
else if (cmd == COMMAND_WRITE) {
uint32_t addr, length;
if (!client->recvall(&addr, 4)) return;
if (!client->recvall(&length, 4)) return;
if (!client->recvall((void *)addr, length)) return;
}
else if (cmd == COMMAND_WRITE_CODE) {
uint32_t addr, length;
if (!client->recvall(&addr, 4)) return;
if (!client->recvall(&length, 4)) return;
char *buffer = new char[length];
if (!client->recvall(buffer, length)) {
delete buffer;
return;
}
breakpoints.write(buffer, addr, length);
delete buffer;
}
else if (cmd == COMMAND_GET_MODULE_NAME) {
char name[0x40];
int length = 0x40;
OSDynLoad_GetModuleName(-1, name, &length);
length = strlen(name);
if (!client->sendall(&length, 4)) return;
if (!client->sendall(name, length)) return;
}
else if (cmd == COMMAND_GET_MODULE_LIST) {
OSLockMutex(OSDynLoad_gLoaderLock);
char buffer[0x1000]; //This should be enough
uint32_t offset = 0;
OSDynLoad_RPLInfo *current = FirstRPL;
while (current) {
OSDynLoad_NotifyData *info = current->notifyData;
uint32_t namelen = strlen(current->name);
if (offset + 0x18 + namelen > 0x1000) {
break;
}
uint32_t *infobuf = (uint32_t *)(buffer + offset);
infobuf[0] = info->textAddr;
infobuf[1] = info->textSize;
infobuf[2] = info->dataAddr;
infobuf[3] = info->dataSize;
infobuf[4] = (uint32_t)current->entryPoint;
infobuf[5] = namelen;
memcpy(&infobuf[6], current->name, namelen);
offset += 0x18 + namelen;
current = current->next;
}
OSUnlockMutex(OSDynLoad_gLoaderLock);
if (!client->sendall(&offset, 4)) return;
if (!client->sendall(buffer, offset)) return;
}
else if (cmd == COMMAND_GET_THREAD_LIST) {
int state = OSDisableInterrupts();
__OSLockScheduler(this);
char buffer[0x1000]; //This should be enough
uint32_t offset = 0;
OSThread *current = ThreadList;
while (current) {
const char *name = OSGetThreadName(current);
uint32_t namelen = 0;
if (name) {
namelen = strlen(name);
}
if (offset + 0x1C + namelen > 0x1000) {
break;
}
int priority = current->basePriority;
if (current->type == 1) {
priority -= 0x20;
}
else if (current->type == 2) {
priority -= 0x40;
}
uint32_t *infobuf = (uint32_t *)(buffer + offset);
infobuf[0] = (uint32_t)current;
infobuf[1] = current->attr & 7;
infobuf[2] = priority;
infobuf[3] = (uint32_t)current->stackBase;
infobuf[4] = (uint32_t)current->stackEnd;
infobuf[5] = (uint32_t)current->entryPoint;
infobuf[6] = namelen;
memcpy(&infobuf[7], name, namelen);
offset += 0x1C + namelen;
current = current->activeLink.next;
}
__OSUnlockScheduler(this);
OSRestoreInterrupts(state);
if (!client->sendall(&offset, 4)) return;
if (!client->sendall(buffer, offset)) return;
}
else if (cmd == COMMAND_GET_STACK_TRACE) {
OSThread *thread;
if (!client->recvall(&thread, 4)) return;
ExceptionState *state = exceptions.find(thread);
if (state) {
uint32_t sp = state->context.gpr[1];
uint32_t trace[100];
2018-09-24 10:43:20 +02:00
int index = 0;
while (checkDataRead(sp, 4) && index < 100) {
2018-09-24 10:43:20 +02:00
sp = *(uint32_t *)sp;
if (!checkDataRead(sp, 4)) break;
trace[index] = *(uint32_t *)(sp + 4);
index++;
}
if (!client->sendall(&index, 4)) return;
if (!client->sendall(trace, index * 4)) return;
}
else {
int index = 0;
if (!client->sendall(&index, 4)) return;
}
}
else if (cmd == COMMAND_TOGGLE_BREAKPOINT) {
uint32_t address;
if (!client->recvall(&address, 4)) return;
breakpoints.toggle(address);
}
else if (cmd == COMMAND_POKE_REGISTERS) {
OSThread *thread;
if (!client->recvall(&thread, 4)) return;
uint32_t gpr[32];
double fpr[32];
if (!client->recvall(gpr, 4 * 32)) return;
if (!client->recvall(fpr, 8 * 32)) return;
exceptions.lock();
ExceptionState *state = exceptions.find(thread);
if (state) {
memcpy(state->context.gpr, gpr, 4 * 32);
memcpy(state->context.fpr, fpr, 8 * 32);
}
exceptions.unlock();
}
else if (cmd == COMMAND_RECEIVE_MESSAGES) {
OSMessage messages[10];
int count = 0;
while (count < 10) {
if (!OSReceiveMessage(&eventQueue, &messages[count], OS_MESSAGE_FLAGS_NONE)) {
break;
}
count++;
}
if (!client->sendall(&count, 4)) return;
for (int i = 0; i < count; i++) {
if (!client->sendall(&messages[i], sizeof(OSMessage))) return;
if (messages[i].args[0]) {
void *data = (void *)messages[i].args[0];
size_t length = messages[i].args[1];
if (!client->sendall(data, length)) return;
}
}
}
else if (cmd == COMMAND_SEND_MESSAGE) {
OSMessage message;
if (!client->recvall(&message, sizeof(OSMessage))) return;
OSThread *thread = (OSThread *)message.args[0];
exceptions.lock();
ExceptionState *state = exceptions.find(thread);
if (state) {
OSSendMessage(&state->queue, &message, OS_MESSAGE_FLAGS_NONE);
}
exceptions.unlock();
}
}
}
void Debugger::threadFunc() {
int result = socket_lib_init();
if (result < 0) {
OSFatal("Failed to initialize socket library");
}
OSSetExceptionCallbackEx(OS_EXCEPTION_MODE_GLOBAL_ALL_CORES, OS_EXCEPTION_TYPE_DSI, dsiHandler);
OSSetExceptionCallbackEx(OS_EXCEPTION_MODE_GLOBAL_ALL_CORES, OS_EXCEPTION_TYPE_ISI, isiHandler);
OSSetExceptionCallbackEx(OS_EXCEPTION_MODE_GLOBAL_ALL_CORES, OS_EXCEPTION_TYPE_PROGRAM, programHandler);
Server server;
Client client;
initialized = true;
while (true) {
if (!server.init(Socket::TCP)) continue;
if (!server.bind(1560)) continue;
if (!server.accept(&client)) continue;
connected = true;
mainLoop(&client);
cleanup();
connected = false;
client.close();
server.close();
}
}
int Debugger::threadEntry(int argc, void *argv) {
debugger->threadFunc();
return 0;
}
void Debugger::start() {
initialized = false;
connected = false;
firstTrap = true;
OSInitMessageQueue(&eventQueue, eventMessages, MESSAGE_COUNT);
breakpoints.init();
exceptions.init();
stepper.init();
serverThread = new OSThread();
2018-09-24 10:43:20 +02:00
char *stack = new char[0x8000];
OSCreateThread(
serverThread, threadEntry, 0, 0,
2018-09-24 10:43:20 +02:00
stack + STACK_SIZE, STACK_SIZE,
0, 12
);
OSSetThreadName(serverThread, "Debug Server");
OSResumeThread(serverThread);
2018-09-24 10:43:20 +02:00
while (!initialized) {
OSSleepTicks(OSMillisecondsToTicks(20));
}
}
Debugger *debugger;