#include "Debuggers.h" #include #include "PrettyPrinters.brc" // we need those because we changed ArrayCtrl formats (including var types) static VectorMap DataMap2(const ArrayCtrl& data) { VectorMap m; for(int i = 0; i < data.GetCount(); i++) m.Add(data.Get(i, 0), data.Get(i, 2)); return m; } static void MarkChanged2(const VectorMap& m, ArrayCtrl& data) { for(int i = 0; i < data.GetCount(); i++) { int q = m.Find(data.Get(i, 0)); if(q >= 0 && m[q] != data.Get(i, 2)) data.SetDisplay(i, 2, Single()); else data.SetDisplay(i, 2, StdDisplay()); } } void WatchEdit::HighlightLine(int line, Vector& h, int pos) { Color cEven = Blend(SColorInfo, White, 220); Color cOdd = Blend(SColorInfo, White, 128); for(int i = 0; i < h.GetCount(); i++) h[i].paper = (line % 2 ? cOdd : cEven); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC IDE INTERFACE ///////////////////////////////////////////////////////////////////////////////////////////////////////////// void Gdb_MI2::DebugBar(Bar& bar) { bar.Add("Stop debugging", THISBACK(Stop)).Key(K_SHIFT_F5); bar.Separator(); // see note on Run() function -- crashes X on my machine, so removed by now // bar.Add(!stopped, "Asynchronous break", THISBACK(AsyncBrk)); bool b = !IdeIsDebugLock(); bar.Add(b, "Step into", DbgImg::StepInto(), THISBACK1(Step, disas.HasFocus() ? "exec-step-instruction" : "exec-step")).Key(K_F11); bar.Add(b, "Step over", DbgImg::StepOver(), THISBACK1(Step, disas.HasFocus() ? "exec-next-instruction" : "exec-next")).Key(K_F10); bar.Add(b, "Step out", DbgImg::StepOut(), THISBACK1(Step, "exec-finish")).Key(K_SHIFT_F11); bar.Add(b, "Run to", DbgImg::RunTo(), THISBACK(doRunTo)).Key(K_CTRL_F10); bar.Add(b, "Run", DbgImg::Run(), THISBACK(Run)).Key(K_F5); bar.Separator(); bar.Add(b, "Quick watch", THISBACK(QuickWatch)).Key(K_CTRL_Q); bar.MenuSeparator(); bar.Add(b, "Copy backtrace", THISBACK(CopyStack)); bar.Add(b, "Copy dissassembly", THISBACK(CopyDisas)); } static int CharFilterReSlash(int c) { return c == '\\' ? '/' : c; } bool Gdb_MI2::SetBreakpoint(const String& filename, int line, const String& bp) { String file = Filter(host->GetHostPath(NormalizePath(filename)), CharFilterReSlash); // gets all breakpoints MIValue bps = GetBreakpoints(); // line should start from 1... line++; // check wether we've got already a breakpoint here // and remove it MIValue brk = bps.FindBreakpoint(file, line); if(!brk.IsEmpty()) if(!MICmd(Format("break-delete %s", brk["number"].Get()))) { Exclamation(t_("Couldn't remove breakpoint")); return false; } if(bp.IsEmpty()) return true; else if(bp[0] == 0xe) return MICmd(Format("break-insert %s:%d", file, line)); else return MICmd(Format("break-insert -c \"%s\" %s:%d", bp, file, line)); } bool Gdb_MI2::RunTo() { String bi; bool df = disas.HasFocus(); bool res; // sets a temporary breakpoint on cursor location // it'll be cleared automatically on first stop if(df) { if(!disas.GetCursor()) return false; res = TryBreak(disas.GetCursor(), true); } else res = TryBreak(IdeGetFileName(), IdeGetFileLine() + 1, true); if(!res) { Exclamation(t_("No code at chosen location !")); return false; } Run(); if(df) disas.SetFocus(); return true; } void Gdb_MI2::Run() { MIValue val; if(firstRun) // GDB up to 7.1 has a bug that maps -exec-run ro run, not to run& // making so async mode useless; we use the console run& command instead // 2012-07-08 update : interrupting GDB in async mode without having // non-stop mode enabled crashes X...don't know if it's a GDB bug or Theide one. // anyways, by now we give up with async mode and remove 'Asynchronous break' function val = MICmd("exec-run"); // val = MICmd("interpreter-exec console run&"); else val = MICmd("exec-continue --all"); int i = 50; while(!started && --i) { GuiSleep(20); Ctrl::ProcessEvents(); ReadGdb(false); } if(!started) { Exclamation(t_("Failed to start application")); return; } Lock(); while(dbg && !stopped) { GuiSleep(20); Ctrl::ProcessEvents(); ReadGdb(false); } Unlock(); if(stopped) CheckStopReason(); started = stopped = false; firstRun = false; IdeActivateBottom(); } void Gdb_MI2::AsyncBrk() { // send an interrupt command to all running threads StopAllThreads(); // gdb usually returns to command prompt instantly, BEFORE // giving out stop reason, which we need. So, we wait some // milliseconds and re-read (non blocking) GDB output to get it /* for(int i = 0; i < 20 && !stopped; i++) { Sleep(100); ReadGdb(false); } */ // if target is correctly stopped, 'stopped' flag should be already set // so we don't care here. If not set, target can't be stopped. } void Gdb_MI2::Stop() { stopped = true; if(dbg && dbg->IsRunning()) dbg->Kill(); } bool Gdb_MI2::IsFinished() { return !dbg->IsRunning() && !IdeIsDebugLock(); } bool Gdb_MI2::Tip(const String& exp, CodeEditor::MouseTip& mt) { // quick exit if(exp.IsEmpty()) return false; // if local vars are empty, update them UpdateLocalVars(); // display tip just for local variables... it should be more than enough int i = localVarExpressions.Find(exp); if(i < 0) return false; mt.display = &StdDisplay(); mt.value = exp + "=" + localVarValues[i]; mt.sz = mt.display->GetStdSize(String(mt.value) + "X"); return true; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // CONSTRUCTOR / DESTRUCTOR ///////////////////////////////////////////////////////////////////////////////////////////////////////////// Gdb_MI2::Gdb_MI2() { CtrlLayout(regs); regs.Height(regs.GetLayoutSize().cy); AddReg(RPREFIX "ax", ®s.rax); AddReg(RPREFIX "bx", ®s.rbx); AddReg(RPREFIX "cx", ®s.rcx); AddReg(RPREFIX "dx", ®s.rdx); AddReg(RPREFIX "si", ®s.rsi); AddReg(RPREFIX "di", ®s.rdi); AddReg(RPREFIX "bp", ®s.rbp); AddReg(RPREFIX "sp", ®s.rsp); regs.Color(SColorLtFace); regs.AddFrame(TopSeparatorFrame()); regs.AddFrame(RightSeparatorFrame()); autos.NoHeader(); autos.AddColumn("", 1); autos.AddColumn("", 2); autos.AddColumn("", 6); autos.WhenLeftDouble = THISBACK1(onExploreExpr, &autos); autos.EvenRowColor(); autos.OddRowColor(); locals.NoHeader(); locals.AddColumn("", 1); locals.AddColumn("", 2); locals.AddColumn("", 6); locals.WhenLeftDouble = THISBACK1(onExploreExpr, &locals); locals.EvenRowColor(); locals.OddRowColor(); watches.NoHeader(); watches.AddColumn("", 1).Edit(watchedit); watches.AddColumn("", 2); watches.AddColumn("", 6); watches.Inserting().Removing(); watches.WhenLeftDouble = THISBACK1(onExploreExpr, &watches); watches.EvenRowColor(); watches.OddRowColor(); int c = EditField::GetStdHeight(); explorer.AddColumn("", 1); explorer.AddColumn("", 6) /*.SetDisplay(Single()) */; explorerPane.Add(explorerBackBtn.LeftPos(0, c).TopPos(0, c)); explorerPane.Add(explorerForwardBtn.LeftPos(c + 2, c).TopPos(0, c)); explorerPane.Add(explorerExprEdit.HSizePos(2 * c + 4).TopPos(0, c)); explorerPane.Add(explorer.HSizePos().VSizePos(EditField::GetStdHeight(), 0)); explorer.NoHeader(); explorer.WhenLeftDouble = THISBACK(onExplorerChild); explorer.WhenBar = THISBACK(ExplorerMenu); explorerBackBtn.SetImage(DbgImg::ExplorerBack()); explorerBackBtn <<= THISBACK(onExplorerBack); explorerForwardBtn.SetImage(DbgImg::ExplorerFw()); explorerForwardBtn <<= THISBACK(onExplorerForward); explorerBackBtn.Disable(); explorerForwardBtn.Disable(); explorerHistoryPos = -1; Add(tab.SizePos()); tab.Add(autos.SizePos(), "Autos"); tab.Add(locals.SizePos(), t_("Locals")); tab.Add(watches.SizePos(), t_("Watches")); tab.Add(explorerPane.SizePos(), t_("Explorer")); Add(threadSelector.LeftPos(FindTabsRight() + 10, StdFont().GetWidth('X') * 10).TopPos(2, EditField::GetStdHeight())); Add(frame.HSizePos(threadSelector.GetRect().right + 10, 0).TopPos(2, EditField::GetStdHeight())); frame.Ctrl::Add(dlock.SizePos()); dlock = " Running.."; dlock.SetFrame(BlackFrame()); dlock.SetInk(Red); dlock.NoTransparent(); dlock.Hide(); CtrlLayout(quickwatch, "Quick watch"); quickwatch.close.Cancel() <<= quickwatch.Breaker(IDCANCEL); quickwatch.evaluate.Ok() <<= quickwatch.Acceptor(IDOK); quickwatch.WhenClose = quickwatch.Breaker(IDCANCEL); quickwatch.value.SetReadOnly(); quickwatch.value.SetFont(Courier(12)); quickwatch.value.SetColor(TextCtrl::PAPER_READONLY, White); quickwatch.Sizeable().Zoomable(); quickwatch.NoCenter(); quickwatch.SetRect(0, 150, 300, 400); quickwatch.Icon(DbgImg::QuickWatch()); Transparent(); started = false; stopped = false; } Gdb_MI2::~Gdb_MI2() { IdeRemoveBottom(*this); IdeRemoveRight(disas); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE FUNCTIONS ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // find free space at right of tabs (we should probably add something to TabCtrl for that..) int Gdb_MI2::FindTabsRight(void) { if(!tab.GetCount()) return 0; int lastTab = tab.GetCount() - 1; int i1 = 0, i2 = 10000; // bisect up it finds a point on last tab... int iTab = -1; int i = 0; while(iTab != lastTab) { i = (i1 + i2) / 2; iTab = tab.GetTab(Point(i, 0)); if(iTab < 0) i2 = i; else i1 = i; } // now scan for righ tab edge i1 = i; i2 = i + 10000; while(abs(i1 - i2) > 2) { i = (i1 + i2) / 2; iTab = tab.GetTab(Point(i, 0)); if(iTab == -1) i2 = i; else i1 = i; } return i; } // lock/unlock debugger controls void Gdb_MI2::Lock() { IdeDebugLock(); watches.Disable(); locals.Disable(); frame.Disable(); threadSelector.Disable(); dlock.Show(); } void Gdb_MI2::Unlock() { if(IdeDebugUnLock()) { watches.Enable(); locals.Enable(); frame.Enable(); threadSelector.Enable(); dlock.Hide(); } } // read debugger output analyzing command responses and async output // things are quite tricky because debugger output seems to be // slow and we have almost no terminator to stop on -- (gdb) is not // so reliable as it can happen (strangely) in middle of nothing MIValue Gdb_MI2::ParseGdb(String const &output, bool wait) { MIValue res; // parse result data StringStream ss(output); int iSubstr; while(!ss.IsEof()) { String s = TrimBoth(ss.GetLine()); // check 'running' and 'stopped' async output if(s.StartsWith("*running")) { started = true; stopReason.Clear(); continue; } // 2012-07-08 -- In some cases, GDB output get mixed with app one // so we shall look for 'stop' record in all string, // not just at beginning as it was before // not a wanderful way, as debugger could be tricked by some text... /* else if(s.StartsWith("*stopped")) { stopped = true; s = '{' + s.Mid(9) + '}'; stopReason = MIValue(s); continue; } */ else if( (iSubstr = s.Find("*stopped,reason=")) >= 0) { stopped = true; s = '{' + s.Mid(iSubstr + 9) + '}'; stopReason = MIValue(s); continue; } // skip asynchronous responses // in future, we could be gather/use them if(s[0] == '*'|| s[0] == '=') continue; // here handling of command responses // we're not interested either, as we use MI interface if(s[0] == '~') continue; // here handling of target output // well, for now discard this one too, but it should go on console output if(s[0] == '~') continue; // here handling of gdb log/debug message // not interesting here if(s[0] == '&') continue; // now we SHALL have something starting with any of // // "^done", "^running", "^connected", "^error" or "^exit" records if(s.StartsWith("^done") || s.StartsWith("^running")) { // here we've got succesful command output in list form, if any // shall skip the comma; following can be a serie of pairs, // or directly an array of maps in form of : // [{key="value",key="value",...},{key="value"...}...] int i = 5; // just skip shortest, ^done while(s[i] && s[i] != ',') i++; if(!s[i]) continue; i++; if(!s[i]) continue; res = MIValue(s.Mid(i)); continue; } else if(s.StartsWith("^error")) { // first array element is reserved for command result status s = s.Right(12); // '^error,msg=\"' s = s.Left(s.GetCount() - 1); res.SetError(s); } else continue; } return res; } MIValue Gdb_MI2::ReadGdb(bool wait) { String output; MIValue res; if(wait) { // this path is for locking read -- we NEED cmd output int retries = 4; while(dbg) { String s; dbg->Read(s); // we really NEED an answer if(s.IsEmpty() && output.IsEmpty()) continue; // on first empty string, we check if we really // got a valid answer if(s.IsEmpty()) { res = ParseGdb(output); // if no valid answer (or, well, an empty answer..) // we retry up to 4 times if(res.IsEmpty() && --retries) { Sleep(20); continue; } return res; } output += s; } } else { // this path is for NON locking read -- we just cleanup // streaming output, mostly while(dbg) { String s; dbg->Read(s); if(s.IsEmpty()) break; output += s; } } if(output.IsEmpty()) return res; return ParseGdb(output); } // new-way commands using GDB MI interface // on input : MI interface command line // on output : an MIValue containing GDB output // STREAM OUTPUT // ~ command response // @ target output // & gdb log/debug messages // // RESULT RECORDS // "^done" [ "," results ] // "^running" same as "^done" // "^connected" gdb has connected to a remote target. // "^error" "," c-string The operation failed. The c-string contains the corresponding error message. // "^exit" gdb has terminate // // ASYNCHRONOUS RECORDS // *running,thread-id="thread" // *stopped,reason="reason",thread-id="id",stopped-threads="stopped",core="core" // =thread-group-added,id="id" // =thread-group-removed,id="id" // =thread-group-started,id="id",pid="pid" // =thread-group-exited,id="id"[,exit-code="code"] // =thread-created,id="id",group-id="gid" // =thread-exited,id="id",group-id="gid" // =thread-selected,id="id" // =library-loaded,... // =library-unloaded,... // =breakpoint-created,bkpt={...} // =breakpoint-modified,bkpt={...} // =breakpoint-deleted,bkpt={...} // // FRAME INFO INSIDE RESPONSES // level The level of the stack frame. The innermost frame has the level of zero. This field is always present. // func The name of the function corresponding to the frame. This field may be absent if gdb is unable to determine the function name. // addr The code address for the frame. This field is always present. // file The name of the source files that correspond to the frame's code address. This field may be absent. // line The source line corresponding to the frames' code address. This field may be absent. // from The name of the binary file (either executable or shared library) the corresponds to the frame's code address. This field may be absent. // THREAD INFO INSIDE RESPONSES // id The numeric id assigned to the thread by gdb. This field is always present. // target-id Target-specific string identifying the thread. This field is always present. // details Additional information about the thread provided by the target. It is supposed to be human-readable and not interpreted by the frontend. This field is optional. // state Either `stopped' or `running', depending on whether the thread is presently running. This field is always present. // core The value of this field is an integer number of the processor core the thread was last seen on. This field is optional. // // REMARKS : by now, we just handle synchronous output and check asynchronous one just to detect // debugger run/stop status -- all remaining asynchrnonous output is discarded MIValue Gdb_MI2::MICmd(const char *cmdLine) { // sends command to debugger and get result data // should handle dbg unexpected termination ? if(!dbg || !dbg->IsRunning() /* || IdeIsDebugLock() */) return MIValue(); // consume previous output from gdb... don't know why sometimes // is there and gives problems to MI interface. We shall maybe // parse and store it somewhere ReadGdb(false); dbg->Write(String("-") + cmdLine + "\n"); return ReadGdb(); } // format breakpoint line from ide file and line String Gdb_MI2::BreakPos(String const &file, int line) { return String().Cat() << Filter(host->GetHostPath(NormalizePath(file)), CharFilterReSlash) << ":" << line; } // set breakpoint MIValue Gdb_MI2::InsertBreakpoint(const char *file, int line) { return MICmd("break-insert " + BreakPos(file, line)); } // get breakpoints info MIValue Gdb_MI2::GetBreakpoints(void) { // issue a break-list command return MICmd("break-list")["BreakpointTable"]; } MIValue Gdb_MI2::GetBreakpoint(int id) { return MIValue(); } MIValue Gdb_MI2::GetBreakPoint(const char *file, int line) { return MIValue(); } bool Gdb_MI2::TryBreak(adr_t addr, bool temp) { String bp; if(temp) bp = "-t "; bp += Format("*0x%X", (int64)addr); MIValue res = MICmd(String("break-insert ") + bp); return !res.IsError(); } bool Gdb_MI2::TryBreak(String const &file, int line, bool temp) { String bp; if(temp) bp = "-t "; bp += BreakPos(file, line); MIValue res = MICmd(String("break-insert ") + bp); return !res.IsError(); } void Gdb_MI2::SyncDisas(MIValue &fInfo, bool fr) { if(!disas.IsVisible()) return; // get current frame's address adr_t adr = stou(~fInfo["addr"].Get().Mid(2), NULL, 16); if(!disas.InRange(adr)) { MIValue code; // if frame is inside a source file, disassemble current function if(fInfo.Find("file") >= 0 && fInfo.Find("line") >= 0) { String file = fInfo["file"]; String line = fInfo["line"]; code = MICmd(Format("data-disassemble -f %s -l %s -n -1 -- 0", file, line))["asm_insns"]; } else // otherwise disassemble some -100 ... +100 bytes around address code = MICmd(Format("data-disassemble -s %x -e %x -- 0", (void *)(adr - 100), (void *)(adr + 100)))["asm_insns"]; disas.Clear(); for(int iLine = 0; iLine < code.GetCount(); iLine++) { MIValue &line = code[iLine]; adr_t address = stou(~line["address"].Get().Mid(2), NULL, 16); String inst = line["inst"]; int spc = inst.Find(' '); String opCode, operand; if(spc >= 0) { opCode = inst.Left(spc); operand = TrimBoth(inst.Mid(spc)); } else opCode = inst; disas.Add(address, opCode, operand); } } // setup disassembler cursor disas.SetCursor(adr); disas.SetIp(adr, fr ? DbgImg::FrameLinePtr() : DbgImg::IpLinePtr()); // update registers MIValue rNames = MICmd("data-list-register-names")["register-names"]; IndexiNames; for(int i = 0; i < rNames.GetCount(); i++) iNames.Add(rNames[i]); MIValue rValues = MICmd("data-list-register-values x")["register-values"]; for(int iReg = 0; iReg < regname.GetCount(); iReg++) { int i = iNames.Find(regname[iReg]); String rValue = "****************"; if(i >= 0) rValue = "0000000000000000" + rValues[i]["value"].Get().Mid(2); #ifdef CPU_64 rValue = rValue.Right(16); #else rValue = rValue.Right(8); #endif if(reglbl[iReg]->GetText() != rValue) { reglbl[iReg]->SetLabel(rValue); reglbl[iReg]->SetInk(LtRed); } else reglbl[iReg]->SetInk(Black); } } // sync ide display with breakpoint position void Gdb_MI2::SyncIde(bool fr) { // setup threads droplist threadSelector.Clear(); MIValue tInfo = MICmd("thread-info"); int curThread = atoi(tInfo["current-thread-id"].Get()); MIValue &threads = tInfo["threads"]; for(int iThread = 0; iThread < threads.GetCount(); iThread++) { int id = atoi(threads[iThread]["id"].Get()); if(id == curThread) { threadSelector.Add(id, Format("#%03x(*)", id)); break; } } threadSelector <<= curThread; // get current frame info and level MIValue fInfo = MICmd("stack-info-frame")["frame"]; int level = atoi(fInfo["level"].Get()); // if we got file and line info, we can sync the source editor position autoLine = ""; if(fInfo.Find("file") >= 0 && fInfo.Find("line") >= 0) { String file = GetLocalPath(fInfo["file"]); int line = atoi(fInfo["line"].Get()); if(FileExists(file)) { IdeSetDebugPos(GetLocalPath(file), line - 1, fr ? DbgImg::FrameLinePtr() : DbgImg::IpLinePtr(), 0); IdeSetDebugPos(GetLocalPath(file), line - 1, disas.HasFocus() ? fr ? DbgImg::FrameLinePtr() : DbgImg::IpLinePtr() : Image(), 1); autoLine = IdeGetLine(line - 2) + ' ' + IdeGetLine(line - 1) + ' ' + IdeGetLine(line); } } // get the arguments for current frame MIValue fArgs = MICmd(Format("stack-list-arguments 1 %d %d", level, level))["stack-args"][0]["args"]; // setup frame droplist frame.Clear(); frame.Add(0, FormatFrame(fInfo, fArgs)); frame <<= 0; SyncData(); SyncDisas(fInfo, fr); } // logs frame data on console void Gdb_MI2::LogFrame(String const &msg, MIValue &frame) { String file = frame("file", ""); String line = frame("line", ""); String function = frame("function", ""); String addr = frame("addr", ""); PutConsole(Format(msg + " at %s, function '%s', file '%s', line %s", addr, function, file, line)); } // stop all running threads and re-select previous current thread void Gdb_MI2::StopAllThreads(void) { // get thread info for all threads MIValue tInfo = MICmd("thread-info"); MIValue &threads = tInfo["threads"]; bool someRunning = false; for(int iThread = 0; iThread < threads.GetCount(); iThread++) { if(threads[iThread]["state"].Get() != "stopped") { someRunning = true; break; } } // don't issue any stop command if no threads running // (brings problems....) if(!someRunning) return; // stores current thread id String current = tInfo("current-thread-id", ""); // stops all threads MICmd("exec-interrupt --all"); // just to be sure, reads out GDB output ReadGdb(false); // reselect current thread as it was before stopping all others if(current != "") MICmd("thread-select " + current); } // check for stop reason void Gdb_MI2::CheckStopReason(void) { // we need to store stop reason BEFORE interrupting all other // threads, otherwise it'll be lost MIValue stReason = stopReason; // get the reason string String reason; if(stReason.IsEmpty()) reason = "unknown reason"; else reason = stReason["reason"]; // as we are in non-stop mode, to allow async break to work // we shall stop ALL running threads here, otherwise we'll have // problems when single stepping a gui MT app // single step will be done so for a single thread, while other // are idle. Maybe we could make this behaviour optional StopAllThreads(); if(reason == "exited-normally") { Stop(); PutConsole("Program exited normally."); } else if(reason == "exited") { Stop(); PutConsole("Program exited with code "); } else if(reason == "breakpoint-hit") { LogFrame("Hit breakpoint", stReason["frame"]); SyncIde(); } else if(reason == "end-stepping-range") { LogFrame("End stepping range", stReason["frame"]); SyncIde(); } else if(reason == "unknown reason") { PutConsole("Stopped by unknown reason"); SyncIde(); } else { // weird stop reasons (i.e., signals, segfaults... may not have a frame // data inside if(stReason.Find("frame") < 0) PutConsole(Format("Stopped, reason '%s'", reason)); else LogFrame(Format("Stopped, reason '%s'", reason), stReason["frame"]); SyncIde(); } } void Gdb_MI2::Step(const char *cmd) { bool b = disas.HasFocus(); MIValue res = MICmd(cmd); int i = 50; while(!started && --i) { GuiSleep(20); Ctrl::ProcessEvents(); ReadGdb(false); } if(!started) { Stop(); Exclamation(t_("Step failed - terminating debugger")); return; } Lock(); ReadGdb(false); while(dbg && !stopped) { GuiSleep(20); Ctrl::ProcessEvents(); ReadGdb(false); } Unlock(); firstRun = false; if(stopped) CheckStopReason(); started = stopped = false; IdeActivateBottom(); // reset focus to disassembly pane if was there before if(b) disas.SetFocus(); } // setup ide cursor based on disassembler one void Gdb_MI2::DisasCursor() { if(!disas.HasFocus()) return; // temporary disable disas lost-focus handler // otherwise cursor will not be correctly set by following code disas.WhenFocus.Clear(); // to get info of corresponding file/line of an address, the only way // we found is to insert a breakpoint, note the position and remove it MIValue b = MICmd(Format("break-insert *0x%X", (int64)disas.GetCursor()))["bkpt"]; if(b.Find("file") >= 0 && b.Find("line") >= 0) IdeSetDebugPos(b["file"], atoi(b["line"].Get()) - 1, DbgImg::DisasPtr(), 1); if(b.Find("number") >= 0) MICmd(Format("break-delete %s", b["number"].Get())); disas.SetFocus(); // re-enable disas lost focus handler disas.WhenFocus = THISBACK(DisasFocus); } // reset ide default cursor image when disassembler loose focus void Gdb_MI2::DisasFocus() { if(!disas.HasFocus()) IdeSetDebugPos(IdeGetFileName(), IdeGetFileLine(), Null, 1); } // create a string representation of frame given its info and args String Gdb_MI2::FormatFrame(MIValue &fInfo, MIValue &fArgs) { int idx = atoi(fInfo("level", "-1")); if(idx < 0) return t_("invalid frame info"); if(!fArgs.IsArray()) return t_("invalid frame args"); String func = fInfo("func", ""); String file = fInfo("file", ""); String line = fInfo("line", ""); int nArgs = fArgs.GetCount(); String argLine; for(int iArg = 0; iArg < nArgs; iArg++) { argLine += fArgs[iArg]["name"].Get(); if(fArgs[iArg].Find("value") >= 0) argLine << "=" << fArgs[iArg]["value"]; argLine << ','; } if(!argLine.IsEmpty()) argLine = "(" + argLine.Left(argLine.GetCount() - 1) + ")"; return Format("%02d-%s%s at %s:%s", idx, func, argLine, file, line); } // re-fills frame's droplist when dropping it void Gdb_MI2::DropFrames() { int q = ~frame; frame.Clear(); // get a list of frames MIValue frameList = MICmd("stack-list-frames")["stack"]; if(frameList.IsError() || !frameList.IsArray()) { Exclamation("Couldn't get stack frame list"); return; } // get the arguments for all frames, values just for simple types MIValue frameArgs = MICmd("stack-list-arguments 1")["stack-args"]; if(frameArgs.IsError() || !frameArgs.IsArray()) { Exclamation("Couldn't get stack arguments list"); return; } // fill the droplist for(int iFrame = 0; iFrame < frameArgs.GetCount(); iFrame++) { MIValue &fInfo = frameList[iFrame]; MIValue &fArgs = frameArgs[iFrame]["args"]; frame.Add(iFrame, FormatFrame(fInfo, fArgs)); } frame <<= q; } // shows selected stack frame in editor void Gdb_MI2::ShowFrame() { int i = (int)~frame; if(!MICmd(Format("stack-select-frame %d", i))) { Exclamation(Format(t_("Couldn't select frame #%d"), i)); return; } SyncIde(i); } // re-fills thread selector droplist on drop void Gdb_MI2::dropThreads() { int q = ~threadSelector; threadSelector.Clear(); // get a list of all available threads MIValue tInfo = MICmd("thread-info"); MIValue &threads = tInfo["threads"]; if(!tInfo.IsTuple() || !threads.IsArray()) { Exclamation(t_("couldn't get thread info")); return; } int currentId = atoi(tInfo["current-thread-id"].Get()); for(int iThread = 0; iThread < threads.GetCount(); iThread++) { MIValue &t = threads[iThread]; int id = atoi(t["id"].Get()); String threadStr = Format("#%03x%s", id, (id == currentId ? "(*)" : "")); threadSelector.Add(id, threadStr); } threadSelector <<= q; } // selects current thread void Gdb_MI2::showThread(void) { int i = (int)~threadSelector; MICmd(Format("thread-select %d", i)); SyncIde(); } // update variables on demand (locals, watches....) void Gdb_MI2::UpdateVars(void) { MIValue iUpdated = MICmd("var-update 2 *"); if(!iUpdated.IsTuple() || iUpdated.Find("changelist") < 0) return; MIValue &updated = iUpdated["changelist"]; for(int iUpd = 0; iUpd < updated.GetCount(); iUpd++) { if(!updated[iUpd].IsTuple() || updated[iUpd].Find("name") < 0 || updated[iUpd].Find("value") < 0) return; String varName = updated[iUpd]["name"]; String value = updated[iUpd]["value"]; int iVar; // local variables if( (iVar = localVarNames.Find(varName)) >= 0) localVarValues[iVar] = value; // watches if( (iVar = watchesNames.Find(varName)) >= 0) watchesValues[iVar] = value; // autos if( (iVar = autosNames.Find(varName)) >= 0) autosValues[iVar] = value; } } struct CapitalLess { bool operator()(String const &a, String const &b) const { return ToUpper(a) < ToUpper(b); } }; // update local variables on demand void Gdb_MI2::UpdateLocalVars(void) { // we try to speed-up at most reading of local vars; this // because we need to keep them updated at each step and // re-reading as a whole is too time expensive. // So, at first we build a list of local variable NAMES only MIValue iLoc = MICmd("stack-list-variables 0"); if(!iLoc || iLoc.IsEmpty()) return; MIValue &loc = iLoc["variables"]; if(!loc.IsArray()) return; IndexlocIdx; for(int iLoc = 0; iLoc < loc.GetCount(); iLoc++) locIdx.Add(loc[iLoc]["name"]); // then we 'purge' stored local variables that are not present // anymore... that can happen, for example, exiting from a loop for(int iVar = localVarNames.GetCount() - 1; iVar >= 0; iVar--) { if(locIdx.Find(localVarExpressions[iVar]) < 0) { String varName = localVarNames.Pop(); MICmd("var-delete \"" + varName + "\""); localVarExpressions.Pop(); localVarValues.Pop(); localVarTypes.Pop(); } } // then we shall add missing variables for(int iLoc = 0; iLoc < locIdx.GetCount(); iLoc++) { if(localVarExpressions.Find(locIdx[iLoc]) < 0) { MIValue var = MICmd(String("var-create - @ \"") + locIdx[iLoc] + "\""); // sometimes it has problem creating vars... maybe because they're // still not active; we just skip them if(var.IsError() || var.IsEmpty()) continue; localVarNames.Add(var["name"]); localVarExpressions.Add(locIdx[iLoc]); localVarTypes.Add(var["type"]); localVarValues.Add(var["value"]); } } // unsorted local vars are ugly and difficult to look at... // so we sort them. As we've 4 structures, it's not so simple Vector keys; keys <<= localVarExpressions.GetKeys(); IndexSort(keys, localVarTypes, CapitalLess()); keys <<= localVarExpressions.GetKeys(); IndexSort(keys, localVarValues, CapitalLess()); Vector names; names <<= localVarNames.GetKeys(); keys <<= localVarExpressions.GetKeys(); IndexSort(keys, names, CapitalLess()); localVarNames = names; localVarExpressions = keys; // and finally, we do an update to refresh changed variables UpdateVars(); } // update stored watches values on demand void Gdb_MI2::UpdateWatches(void) { // gets variable names from control Index exprs; for(int i = 0; i < watches.GetCount(); i++) exprs.Add(watches.Get(i, 0)); // purge stored watches not available anymore for(int iVar = watchesNames.GetCount() - 1; iVar >= 0; iVar--) { if(exprs.Find(watchesExpressions[iVar]) < 0) { String varName = watchesNames.Pop(); MICmd("var-delete \"" + varName + "\""); watchesExpressions.Pop(); watchesValues.Pop(); watchesTypes.Pop(); } } // then we shall add missing variables for(int i = 0; i < exprs.GetCount(); i++) { if(watchesExpressions.Find(exprs[i]) < 0) { // the '@' means we create a dynamic variable MIValue var = MICmd(String("var-create - @ \"") + exprs[i] + "\""); // sometimes it has problem creating vars... maybe because they're // still not active; we just skip them if(!var || var.IsEmpty() || !var.IsTuple()) continue; watchesNames.Add(var["name"]); watchesExpressions.Add(exprs[i]); watchesTypes.Add(var["type"]); watchesValues.Add(var["value"]); } } // and finally, we do an update to refresh changed variables UpdateVars(); } // update stored auto values on demand void Gdb_MI2::UpdateAutos(void) { // gets variable names from control Index exprs; for(int i = 0; i < autos.GetCount(); i++) exprs.Add(autos.Get(i, 0)); // purge stored autos not available anymore for(int iVar = autosNames.GetCount() - 1; iVar >= 0; iVar--) { if(exprs.Find(autosExpressions[iVar]) < 0) { String varName = autosNames.Pop(); MICmd("var-delete \"" + varName + "\""); autosExpressions.Pop(); autosValues.Pop(); autosTypes.Pop(); } } // then we shall add missing variables for(int i = 0; i < exprs.GetCount(); i++) { if(autosExpressions.Find(exprs[i]) < 0) { // the '@' means we create a dynamic variable MIValue var = MICmd(String("var-create - @ \"") + exprs[i] + "\""); // sometimes it has problem creating vars... maybe because they're // still not active; we just skip them if(!var || var.IsEmpty() || !var.IsTuple()) continue; autosNames.Add(var["name"]); autosExpressions.Add(exprs[i]); autosTypes.Add(var["type"]); autosValues.Add(var["value"]); } } // and finally, we do an update to refresh changed variables UpdateVars(); } void Gdb_MI2::SyncLocals() { // update local vars cache, if needed UpdateLocalVars(); // reload ide control VectorMap prev = DataMap2(locals); locals.Clear(); for(int i = 0; i < localVarNames.GetCount(); i++) locals.Add(localVarExpressions[i], localVarTypes[i], localVarValues[i]); MarkChanged2(prev, locals); } // sync watches treectrl void Gdb_MI2::SyncWatches() { // update local watches cache, if needed UpdateWatches(); // re-fill the control VectorMap prev = DataMap2(watches); for(int i = 0; i < watches.GetCount(); i++) { String expr = watches.Get(i, 0); int idx = watchesExpressions.Find(expr); if(idx >= 0) { watches.Set(i, 1, watchesTypes[idx]); watches.Set(i, 2, watchesValues[idx]); } else { watches.Set(i, 1, ""); watches.Set(i, 2, t_("")); } } MarkChanged2(prev, watches); } // sync auto vars treectrl void Gdb_MI2::SyncAutos() { VectorMap prev = DataMap2(autos); autos.Clear(); // read expressions around cursor line CParser p(autoLine); while(!p.IsEof()) { if(p.IsId()) { String exp = p.ReadId(); for(;;) { if(p.Char('.') && p.IsId()) exp << '.'; else if(p.Char2('-', '>') && p.IsId()) exp << "->"; else break; exp << p.ReadId(); } if(autos.Find(exp) < 0) autos.Add(exp, ""); } p.SkipTerm(); } autos.Sort(); // update autos cache, if needed UpdateAutos(); for(int i = autos.GetCount() -1; i >= 0; i--) { String expr = autos.Get(i, 0); int idx = autosExpressions.Find(expr); if(idx >= 0) { autos.Set(i, 1, autosTypes[idx]); autos.Set(i, 2, autosValues[idx]); } else autos.Remove(i); } MarkChanged2(prev, autos); } // sync data tabs, depending on which tab is shown void Gdb_MI2::SyncData() { switch(tab.Get()) { case 0: SyncAutos(); break; case 1: SyncLocals(); break; case 2: SyncWatches(); break; } } // watches arrayctrl key handling bool Gdb_MI2::Key(dword key, int count) { if(key >= 32 && key < 65535 && tab.Get() == 2) { watches.DoInsertAfter(); Ctrl* f = GetFocusCtrl(); if(f && watches.HasChildDeep(f)) f->Key(key, count); return true; } if(key == K_ENTER && explorerExprEdit.HasFocus()) { onExploreExpr(); return true; } return Ctrl::Key(key, count); } // sends pretty-printing scripts void Gdb_MI2::SendPrettyPrinters(void) { String fName = GetTempFileName(); fName = fName.Left(fName.GetCount() - 3) + "py"; SaveFile(fName, (const char *)PrettyPrinters); MICmd("interpreter-exec console \"source " + fName + "\""); FileDelete(fName); } // format watch line String Gdb_MI2::FormatWatchLine(String exp, String const &val, int level) { if(exp.GetCount() < 20) exp = exp + String(' ', 20); else exp = exp.Left(17) + "..."; exp = exp.Left(20) + " = " + val; return String(' ', level * 4) + exp; } // deep watch current quickwatch variable void Gdb_MI2::WatchDeep0(String parentExp, String const &var, int level, int &maxRemaining) { // avoid endless recursion for circularly linked vars if(--maxRemaining <= 0) return; MIValue childInfo = MICmd("var-list-children 1 \"" + var + "\" 0 100"); if(!childInfo || !childInfo.IsTuple()) return; int nChilds = min(atoi(childInfo("numchild", "-1")), 100); if(nChilds <= 0) return; MIValue &childs = childInfo["children"]; for(int i = 0; i < childs.GetCount() && maxRemaining > 0; i++) { MIValue child = childs[i]; String exp = child["exp"]; // handle pseudo children... /* while(exp == "public" || exp == "private" || exp == "protected") { child = MICmd(String("var-list-children 1 \"") + child["name"] + "\"")["children"][0]; exp = child["exp"]; } */ if(isdigit(exp[0])) { exp = '[' + exp + ']'; if(parentExp.Mid(parentExp.GetCount() - 1, 1) == ".") parentExp = parentExp.Left(parentExp.GetCount() - 1); } if(exp[0] != '.' && exp[0] != '[') exp = '.' + exp; String type = child("type", ""); if(!type.IsEmpty()) type = "(" + type + ")"; String value = child["value"]; // try to format nicely results... quickwatch.value <<= (String)~quickwatch.value + "\n" + FormatWatchLine(parentExp + exp, type + value, level); // recursive deep watch WatchDeep0(exp, child["name"], level + 1, maxRemaining); } } void Gdb_MI2::WatchDeep(String parentExp, String const &name) { // this is to avoid circular endless recursion // we limit the total watched (sub)variables to this count int maxRemaining = 300; WatchDeep0(parentExp, name, 1, maxRemaining); } // opens quick watch dialog void Gdb_MI2::QuickWatch() { // try to figure out if we've got the cursor in some interesting // place... if it is, grab the expression from there // otherwise let it unchanged Ctrl *c = GetFocusCtrl(); if(typeid(*c) == typeid(AssistEditor)) { AssistEditor *a = dynamic_cast(c); String s = a->ReadIdBack(a->GetCursor()); quickwatch.expression <<= s; } else if(c == &autos) { int i = autos.GetCursor(); if(i >= 0) quickwatch.expression <<= autos.Get(i, 0); } else if(c == &locals) { int i = locals.GetCursor(); if(i >= 0) quickwatch.expression <<= locals.Get(i, 0); } else if(c == &watches) { int i = watches.GetCursor(); if(i >= 0) quickwatch.expression <<= watches.Get(i, 0); } else if(c == &explorer || c == &explorerExprEdit) { quickwatch.expression <<= ~explorerExprEdit; } for(;;) { String exp = ~quickwatch.expression; if(!exp.IsEmpty()) { MIValue v = MICmd("var-create - @ " + exp); if(!v.IsError()) { String type = v("type", ""); if(!type.IsEmpty()) type = "(" + type + ")"; String value = v["value"]; quickwatch.value <<= FormatWatchLine(exp, type + value, 0); quickwatch.expression.AddHistory(); String name = v["name"]; WatchDeep(exp, name); MICmd("var-delete " + name); } else quickwatch.value <<= t_(""); } else quickwatch.value.Clear(); int q = quickwatch.Run(); if(q == IDCANCEL) break; } quickwatch.Close(); } // copy stack frame list to clipboard void Gdb_MI2::CopyStack() { DropFrames(); String s; for(int i = 0; i < frame.GetCount(); i++) s << frame.GetValue(i) << "\n"; WriteClipboardText(s); } // copy disassembly listing to clipboard void Gdb_MI2::CopyDisas() { disas.WriteClipboard(); } ///////////////////////////////////// EXPLORER ////////////////////////////////////////////////////// void Gdb_MI2::doExplore(String const &expr, String var, bool isChild, bool appendHistory) { // set the expression inside expression editor explorerExprEdit = expr; // store current expr as "parent expr" to be used // when displaying childs... GDB is not reliable on this explorerParentExpr = expr; // try to find a suitable already created var inside history // if not, and if not a child, we shall create it if(var.IsEmpty()) { // empty var is allowed ONLY for non-childs exploring if(isChild) return; int iVar = explorerHistoryExpressions.Find(expr); if(iVar < 0) { MIValue v = MICmd("var-create - @ \"" + expr + "\""); if(v.IsEmpty() || v.IsError()) { explorer.Clear(); explorerChildVars.Clear(); explorer.Add(expr, ""); return; } var = v["name"]; } else var = explorerHistoryVars[iVar]; } // update the history : trim it from past current position // and append it at end if(appendHistory) { if(explorerHistoryPos >= 0) { // frees all non-child variables following current position for(int i = explorerHistoryPos + 1; i < explorerHistoryVars.GetCount(); i++) { if(!explorerHistoryChilds[i] && explorerHistoryVars[i] != var) { MICmd("var-delete \"" + explorerHistoryVars[i] + "\""); } } explorerHistoryPos++; explorerHistoryExpressions.Trim(explorerHistoryPos); explorerHistoryVars.Trim(explorerHistoryPos); explorerHistoryChilds.Trim(explorerHistoryPos); } else explorerHistoryPos = 0; explorerHistoryExpressions.Add(expr); explorerHistoryVars.Add(var); explorerHistoryChilds.Add(isChild); } // here we've finally got variable to explore, just read it // and set into first row of explorer ArrayCtrl String value = MICmd("var-evaluate-expression \"" + var + "\"")["value"]; String type = MICmd("var-info-type \"" + var + "\"")["type"]; explorer.Clear(); explorerChildVars.Clear(); explorer.Add("=", "(" + type + ")" + value, var); // now we shall add variable children... we limit them to a number of 100 // which should be anough. Alternative would be to display them in ranges, // but this over-complicates the application. MIValue childInfo = MICmd("var-list-children 1 \"" + var + "\" 0 100"); int nChilds = min(atoi(childInfo["numchild"].Get()), 100); if(nChilds) { MIValue &childs = childInfo["children"]; for(int i = 0; i < childs.GetCount(); i++) { MIValue child = childs[i]; String exp = child["exp"]; // handle pseudo children... /* while(exp == "public" || exp == "private" || exp == "protected") { child = MICmd(String("var-list-children 1 \"") + child["name"] + "\"")["children"][0]; exp = child["exp"]; } */ if(isdigit(exp[0])) exp = '[' + exp + ']'; String type = child("type", ""); if(!type.IsEmpty()) type = "(" + type + ")"; String value = child["value"]; explorer.Add(exp, type + value); explorerChildVars.Add(child["name"]); } } explorerBackBtn.Enable(explorerHistoryPos > 0); explorerForwardBtn.Enable(explorerHistoryPos < explorerHistoryVars.GetCount() - 1); } // void Gdb_MI2::onExploreExpr(ArrayCtrl *what) { String expr; if(!what) { // if expression don't come from another ArrayCtrl // we use the expression editbox expr = ~explorerExprEdit; } else { // otherwise, we use the expression from sending ArrayCtrl int line = what->GetCursor(); if(line >= 0) expr = what->Get(line, 0); } // nothing to do on empty expression if(expr == "") return; doExplore(expr, "", false, true); // activate explorer tab tab.Set(3); } void Gdb_MI2::onExplorerChild() { // click on first line (value line) does nothing int line = explorer.GetCursor(); if(line < 1) return; if(--line < explorerChildVars.GetCount()) { String var = explorerChildVars[line]; String varExp = var; String expr = MICmd("var-info-expression \"" + var + "\"")["exp"]; if(expr[0] != '[' && expr[0] != '.') if(isdigit(expr[0])) expr = '[' + expr + ']'; else expr = '.' + expr; expr = explorerParentExpr + expr; doExplore(expr, var, true, true); } } void Gdb_MI2::onExplorerBack() { if(explorerHistoryPos < 1) return; explorerHistoryPos--; String expr = explorerHistoryExpressions[explorerHistoryPos]; String var = explorerHistoryVars[explorerHistoryPos]; bool isChild = explorerHistoryChilds[explorerHistoryPos]; doExplore(expr, var, isChild, false); } void Gdb_MI2::onExplorerForward() { if(explorerHistoryPos >= explorerHistoryVars.GetCount() - 1) return; explorerHistoryPos++; String expr = explorerHistoryExpressions[explorerHistoryPos]; String var = explorerHistoryVars[explorerHistoryPos]; bool isChild = explorerHistoryChilds[explorerHistoryPos]; doExplore(expr, var, isChild, false); } void Gdb_MI2::ExplorerMenu(Bar& bar) { } ////////////////////////////////////////////////////////////////////////////////////////////////////// // create GDB process and initializes it bool Gdb_MI2::Create(One _host, const String& exefile, const String& cmdline) { host = _host; dbg = host->StartProcess("gdb " + GetHostPath(exefile) + " --interpreter=mi -q"); if(!dbg) { Exclamation(t_("Error invoking gdb !")); return false; } IdeSetBottom(*this); IdeSetRight(disas); disas.AddFrame(regs); disas.WhenCursor = THISBACK(DisasCursor); disas.WhenFocus = THISBACK(DisasFocus); frame.WhenDrop = THISBACK(DropFrames); frame <<= THISBACK(ShowFrame); threadSelector.WhenDrop = THISBACK(dropThreads); threadSelector <<= THISBACK(showThread); watches.WhenAcceptEdit = THISBACK(SyncData); tab <<= THISBACK(SyncData); // this one will allow asynchronous break of running app // 2012-07-08 -- DISABLED because of GDB bugs... // MICmd("gdb-set target-async 1"); MICmd("gdb-set pagination off"); // Don't enable this one -- brings every sort of bugs with // It was useful to issue Asynchronous break, but too many bugs // to be useable // MICmd("gdb-set non-stop on"); // MICmd("gdb-set interactive-mode off"); MICmd("gdb-set disassembly-flavor intel"); MICmd("gdb-set exec-done-display off"); MICmd("gdb-set annotate 1"); MICmd("gdb-set height 0"); MICmd("gdb-set width 0"); MICmd("gdb-set confirm off"); MICmd("gdb-set print asm-demangle"); // avoids debugger crash if caught inside ungrabbing function // (don't solves all cases, but helps...) MICmd("gdb-set unwindonsignal on"); if(!IsNull(cmdline)) MICmd("gdb-set args " + cmdline); // enable pretty printing SendPrettyPrinters(); MICmd("enable-pretty-printing"); firstRun = true; return true; } One Gdb_MI2Create(One host, const String& exefile, const String& cmdline) { Gdb_MI2 *dbg = new Gdb_MI2; if(!dbg->Create(host, exefile, cmdline)) { delete dbg; return NULL; } return dbg; }