Это продолжение моей прошлой статьи. В этой части мы коснемся сред разработки, а потом будем постепенно деградировать спускаться ниже по абстракциям. Приятного чтения.
Содержание:
Среды разработки
Теперь мы знаем о том как взаимодействуют отладчики с программой, но сегодня большая часть действий выполняется через IDE. Но как это происходит? Не может же IDE fork/exec'аться, а затем через stdin/stdout посылать команды интерактивному отладчику. Давайте разберемся как же устроен этот последний слой: IDE <-> отладчик.
VS Code
Начнем с Visual Studio Code. Чтобы понять, как реализована отладка в нем достаточно посмотреть на страницу документации связанной с разработкой расширения отладки.
VS Code это браузер и в качестве рантайма использует NodeJS. Как отлаживать последний мы уже рассмотрели, но ведь в нем мы можем отлаживать любую программу (и даже не программу). Почему так?
Дело в том как устроен сам процесс отладки. Расширение, которое именуется отладчиком, на самом деле является прослойкой между IDE и настоящим отладчиком. А главная его задача - это обеспечение взаимодействия между IDE и отладчиком по понятному (в первую очередь IDE) протоколу. И протокол этот называется DAP - Debug Adapter Protocol.
DAP - это протокол, созданный Microsoft. Как понятно из названия, главная его задача - абстрагироваться от деталей языка и предоставить общий интерфейс взаимодействия с отладчиком (шаги, исследование переменных, точки останова и т.д.). Этот протокол не привязан к конкретному IDE, но описание самого протокола дано в виде TypeScript объектов: interface
, типизация, комментарии. Предполагаю, что сделано это из-за того, что протокол в первую очередь был создан для VS Code, но сейчас им пользуются и другие IDE, например, NeoVIM. На сайте можно найти более полный список известных адаптеров для различных IDE.
Для взаимодействия между клиентом и адаптером используется простой текстовый протокол. Он похож на HTTP: сообщение состоит из заголовка и содержимого, которые разделяются CRLF, и все это в ASCII. Заголовок состоит из полей (таких же, как в HTTP), но сейчас поддерживается только 1 - Content-Length
. Содержимое же - это JSON объект сообщения.
Сообщений может быть 3 типа:
Request
- запрос, клиент -> адаптер, синхронныйResponse
- ответ, адаптер -> клиент, синхронныйEvent
- событие, адаптер -> клиент, асинхронный
Для различия этих объектов используется поле type
(принимает значения request
, response
, event
). Также и у каждого типа есть свои обязательные поля. Например, для request
- это command
, название команды, которую мы запрашиваем. Ей еще передаются аргументы, но как понятно, они зависят от самой команды. А для response
- флаг success
, успешно или нет выполнилась команда.
Мы уже видели, что многие события в отладчике происходят асинхронно. Аналогично это относится и к сообщениям в DAP. Чтобы решить некоторые проблемы связанные с асинхронностью, каждое сообщение снабжается полем seq
, числом монотонно инкрементирующимся с каждым сообщением. Благодаря этому такие сообщение можно идентифицировать и выстраивать их историю.
Но голая теория мало что даст. Получилось так, что я уже работал с этим протоколом задолго до написания этой секции. Мне необходимо было разработать расширение VS Code для взаимодействия с отладчиком - PostgreSQL Hacker Helper. Если вкратце, то в PostgreSQL (ядре) существует своя система типов, основанная на C-style наследовании: первое поле хранит тип (тэг), а дальше структура приводится к типу соответствующему этому тэгу. В коде на макросах это реализуется довольно просто, так как название тэга (enum
'а) составляется как T_
+ название структуры. Но в рантайме есть только числа, то есть мы не можем в окне выражений написать что-то по типу (substr(nodeTag(node)) *)node
, это приходилось делать отдельными шагами. Все что я хотел сделать - автоматизировать эту рутину. И спойлер - у меня получилось.
Если кого заинтересовала тема типов в PostgreSQL, то ранее я писал статьи по исходному коду PostgreSQL (вот в этой краткое их описание). Также у меня есть доклад на тему разработки PostgreSQL - в ней я рассказываю о разработке исходного кода и отладке (конечно с упоминанием расширения).
Если рассмотреть ядро логики расширения, то оно состоит из 4 простых шагов:
Получаем переменную -
(Node *)variable
Получаем ее тэг -
((Node *)variable)->type = T_SampleStruct
Убираем префикс
T_
-T_SampleStruct = struct SampleStruct
Приводим переменную к нужному типу -
((SampleStruct *)variable)
Это простая логика, но дьявол кроется в деталях. Во-первых, как нам получить первые переменные (не поля уже известных переменных)? Это решается за счет 3 дополнительных вызовов: threads
, stackTrace
и scopes
- получение информации о всех потоках, фреймы нужного потока и информация о scope'ах нужного фрейма соответственно. Последнее, scope - это какой-то именованный набор переменных. В моем случае, их было 2: locals
(локальные переменные) и registers
(регистры).
Во-вторых, как нам обращаться к ресурсам (указывать на конкретные переменные, потоки, фреймы и т.д.)? Для этого имеются специальные идентификаторы. У них всех имеется суффикс reference
. Например, variablesReference
- это ID набора переменных (см. ранее). И этот variablesReference
краеугольный камень моей логики, так как благодаря ему я и получаю информацию о переменных и полях: он передается в scopes
(ответе), а также каждая переменная ее имеет (в этом случае, полученные переменные считаются либо полями структуры, либо элементами массива).
Теперь переведем это все на язык DAP:
threads
- находим нужный нам потокstackTrace
- находим нужный нам фреймscopes
- получаемvariablesReference
области переменныхgetVariables
- получаем все переменные поvariablesReference
evaluate
- вычисляю значение тэга узла с помощью выражения((Node *)variable)->type
и нахожу настоящий тип структурыevaluate
- получаюvariablesReference
уже приведенного типа((RealType *)variable)
Повторяем 4 шаг уже с новым variablesReference
пока не будем получать пустой ответ. И да, как вы могли догадаться evaluate
- это вызов, с помощью которого можно динамически вычислять выражения, и он также возвращает variablesReference
.
И здесь стоит сказать о третьей детали - DAP описывает протокол, но он не формат возвращаемых данных. В моем случае, это означает что я жестко привязан к конкретному адаптеру так как моя логика завязана на парсинг его ответов. К примеру, чтобы понять, что getVariables
вернул простую структуру я проверяю не только тип (поле type
), но и значение (поле value
), так как для переменных значение равно {...}
, а вот для поля (с типом структуры, не указатель) он возвращает пустую строку. Это отладчик C/C++, но когда я попробовал использовать расширение CodeLLDB, то к моему удивлению значения практически всех полей были отформатированы по другому и не были совместимы с моей логикой. В частности, структуры в поле значения форматировались полностью со всеми полями.
GDB/MI
Ранее я уже сказал, что использовал расширение C/C++. Это всего лишь прослойка (вообще это прослойка прослойки, но об этом позже) между настоящим отладчиком и VS Code. В качестве настоящего отладчика используется gdb. Если редактировали файл launch.json
, то могли видеть такое поле как miMode
. mi в данном случае - это сокращение Machine Interface.
gdb mi (gdb Machine Interface) - текстовый интерфейс для взаимодействия с gdb. Его можно активировать запустив gdb с флагом --interpreter
и предназначен для окружений, где отладчик является частью большой системы, такой как IDE. На данный момент, существует 4 версии этого протокола.
Сам протокол сильно похож на стандартный протокол/алгоритм отладчика. Имеем команды, ответы и уведомления. Ответ присылаются при отправке команды, а уведомления приходят асинхронно. Из интересного - совместимость с командами интерактивного режима. Она есть, но оговаривается, что поведение может быть непредсказуемым. Чтобы использовать их без проблем есть отдельная команда - -interperter-exec
.
На странице документации есть примеры взаимодействия с gdb mi. Например, таким образом выставляется точка останова:
-break-insert main
^done,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x000000000000114c",func="main",file="main.c",fullname="/path/to/file/main.c",line="9",thread-groups=["i1"],times="0",original-location="main"}
Сразу можно заметить, что у названий команд в начале идет тире, а затем список их аргументов. Если смотрели внутрь launch.json
, то теперь знаете, что за команды в нем имеются при создании (-enable-pretty-printing
например).
Но давайте посмотрим на другую строку. Как можно понять, это ответ. На этой странице приводится синтаксис вывода (output), но мне кажется приводить его здесь лишнее, поэтому просто опишу в общем и приведу примеры.
Вывод может быть 2 видов: результат (Result) и асинхронные (Async) события. На примере выше мы видели результат. Он начинается с циркумфлекса (^
) и типа ответа - ^done
. На этом же примере, тип - завершение синхронной операции. После идет результат операции. Опять же, здесь это информация о точке останова. Кроме done
есть еще 4 типа ответа, но они не такие частые: запуск/остановка отладчика и ошибка.
И последнее - асинхронные события. Они разделяются на 3 категории: уведомления выполнения (изменение в состоянии), статусные и общие уведомления. Как и у результата, в начале идет специальный символ идентифицирующих их тип. Например, мы продолжили выполнение и наткнулись на точку останова:
^running
*running,thread-id="all"
(gdb)
=breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x0000555555555153",func="main",file="main.c",fullname="/path/to/file/main.c",line="10",thread-groups=["i1"],times="1",original-location="main.c:10"}
~"\n"
~"Breakpoint 2, main () at main.c:10\n"
~"10\t printf(\"%d\\n\", value);\n"
*stopped,reason="breakpoint-hit",disp="keep",bkptno="2",frame={addr="0x0000555555555153",func="main",args=[],file="main.c",fullname="/path/to/file/main.c",line="10",arch="i386:x86-64"},thread-id="1",stopped-threads="all",core="3"
Здесь можно заметить все 2 типа этих уведомлений:
*
- изменение состояния выполняемого процесса=
- статусное уведомление
Также есть ~
. Некоторые команды могут в ответе отправлять какой-либо текст. Если так, то для его отправки используется такой тип уведомления.
MIEngine
В VS Code я использую расширение C/C++ для разработки и мое расширение использует его как DAP адаптер. Но если посмотреть вглубь, то окажется, что C/C++ расширение не само реализует DAP. Она использует MIEngine - движок отладки Visual Studio, который умеет "говорить" на языке MI (того самого как у gdb). Но это еще не все: последняя деталь - OpenDebugAD7
, адаптер для DAP (его код в том же проекте, что и MIEngine). На этой схеме показано взаимодействие между этими частями:
AD7 - Active Debugging 7, фреймворк для создания отладчиков в Visual Studio
Для большей наглядности будем рассматривать путь запроса от пользователя до MIEngine и обратно, то есть включая действия OpenDebugAD7 и отправляемые в gdb команды. И начнем с запуска отладки.
За запуск отладки отвечает либо launch
, либо attach
. Я рассмотрю первый вариант.
Отладка производится сессиями. Такую сессию отладки представляет класс AD7DebugSession
. Он содержит в себе все методы, соответствующие запросам из DAP. Таким образом, за обработку launch
запроса отвечает метод HandleLaunchRequestAsync
. Но по факту, все что он делает - перенаправляет запросы другому объекту, движку MI. Это интерфейс IDebugEngineLaunch2
, который реализует AD7Engine
.
Запуск процесса для отладки
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/OpenDebugAD7/AD7DebugSession.cs#L1104 */
internal sealed class AD7DebugSession
{
private IDebugEngineLaunch2 m_engineLaunch;
protected override void HandleLaunchRequestAsync(IRequestResponder<LaunchArguments> responder)
{
/* ... */
hr = m_engineLaunch.LaunchSuspended(null,
m_port,
program,
null,
null,
null,
launchJson,
flags,
0,
0,
0,
this,
out m_process);
hr = m_engineLaunch.ResumeProcess(m_process);
/* ... */
}
/* ... */
}
sealed public class AE7Engine
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MIDebugEngine/AD7.Impl/AD7Engine.cs#L534 */
int IDebugEngineLaunch2.LaunchSuspended(string pszServer, IDebugPort2 port, string exe, string args, string dir, string env, string options, enum_LAUNCH_FLAGS launchFlags, uint hStdInput, uint hStdOutput, uint hStdError, IDebugEventCallback2 ad7Callback, out IDebugProcess2 process)
{
/* ... */
StartDebugging(launchOptions);
/* ... */
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MIDebugEngine/AD7.Impl/AD7Engine.cs#L575 */
private void StartDebugging(LaunchOptions launchOptions)
{
/* ... */
_pollThread = new WorkerThread(Logger);
var cancellationTokenSource = new CancellationTokenSource();
using (cancellationTokenSource)
{
_pollThread.RunOperation(ResourceStrings.InitializingDebugger, cancellationTokenSource, (HostWaitLoop waitLoop) =>
{
_debuggedProcess = new DebuggedProcess(true, launchOptions, _engineCallback, _pollThread, _breakpointManager, this, _configStore, waitLoop);
return _debuggedProcess.Initialize(waitLoop, cancellationTokenSource.Token);
});
}
/* ... */
}
}
internal class DebuggedProcess : MICore.Debugger
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs#L544 */
public async Task Initialize(HostWaitLoop waitLoop, CancellationToken token)
{
/* ... */
List<LaunchCommand> commands = await GetInitializeCommands();
_childProcessHandler?.Enable();
total = commands.Count;
var i = 0;
foreach (var command in commands)
{
token.ThrowIfCancellationRequested();
waitLoop.SetProgress(total, i++, command.Description);
if (command.IsMICommand)
{
Results results = await CmdAsync(command.CommandText, ResultClass.None);
}
else
{
string resultString = await ConsoleCmdAsync(command.CommandText, allowWhileRunning: false, ignoreFailures: command.IgnoreFailures);
}
}
/* ... */
}
}
Как можем видеть по этой цепочке вызовов - мы запускаем отладчик, инициализируем его и запускаем программу. Но где же сами MI команды? Да, я их пропустил, но намеренно, чтобы сконцентрироваться на логике, а сейчас посмотрим на то, как их отправляют. Первый пример - это команды инициализации, те что были в методе Initialize
. Они создаются в методе GetInitializeCommands
.
GetInitializeCommands
internal class DebuggedProcess : MIcore.Debugger
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs#L618 */
private async Task<List<LaunchCommand>> GetInitializeCommands()
{
List<LaunchCommand> commands = new List<LaunchCommand>();
commands.AddRange(_launchOptions.SetupCommands);
if (_launchOptions.DebuggerMIMode == MIMode.Gdb)
{
commands.Add(new LaunchCommand("-interpreter-exec console \"set pagination off\""));
}
commands.Add(new LaunchCommand("-gdb-set auto-solib-add " + (_launchOptions.CanAutoLoadSymbols() ? "on" : "off")));
if (!string.IsNullOrEmpty(_launchOptions.AbsolutePrefixSOLibSearchPath))
{
commands.Add(new LaunchCommand("-gdb-set solib-absolute-prefix " + _launchOptions.AbsolutePrefixSOLibSearchPath));
}
// On Windows ';' appears to correctly works as a path seperator and from the documentation, it is ':' on unix or cygwin envrionments
string pathEntrySeperator = (_launchOptions.UseUnixSymbolPaths || IsCygwin) ? ":" : ";";
string escapedSearchPath = string.Join(pathEntrySeperator, _launchOptions.GetSOLibSearchPath().Select(path => {
return EnsureProperPathSeparators(path, ignoreSpaces: true);
}));
if (!string.IsNullOrWhiteSpace(escapedSearchPath))
{
if (_launchOptions.DebuggerMIMode == MIMode.Gdb)
{
// Do not place quotes around so paths for gdb
commands.Add(new LaunchCommand("-gdb-set solib-search-path " + escapedSearchPath + pathEntrySeperator, ResourceStrings.SettingSymbolSearchPath));
}
else
{
// surround so lib path with quotes in other cases
commands.Add(new LaunchCommand("-gdb-set solib-search-path \"" + escapedSearchPath + pathEntrySeperator + "\"", ResourceStrings.SettingSymbolSearchPath));
}
}
if (MICommandFactory.SupportsChildProcessDebugging())
{
if (_launchOptions.DebugChildProcesses)
{
_childProcessHandler = new DebugUnixChild(this, this._launchOptions);
}
}
LocalLaunchOptions localLaunchOptions = _launchOptions as LocalLaunchOptions;
if (this.IsCoreDump)
{
// Load executable and core dump
this.AddExecutableAndCorePathCommand(commands);
// Important: this must occur after executable load but before anything else.
this.AddGetTargetArchitectureCommand(commands);
}
/* ... */
}
}
Как можете заметить, во-первых, оперируем простыми, иногда с интерполированными параметрами, строками (это ведь текстовый протокол), и, во-вторых, больше количество кода используется для кроссплатформенности: речь не только об ОС (Windows, Cygwin, Linux ...), но и отладчике (gdb, lldb ...).
Теперь, посмотрим, как эти самые команды отправляются в сам отладчик. Для отправки данных на отладчик используется абстракция ITransport
и зачем она нужна ставится понятно, если понять, что отладчик может находиться не на нашем ПК, а где-нибудь на сервере и взаимодействовать с ним нужно через сеть. Поэтому и реализаций этого транспорта несколько: pipe, tcp, shell, stream, local. Если мы хотим запустить локальный gdb, то будем использовать LocalTransport
- он настраивает окружение, находим файл конфигурации (.gdbinit
) и запускает сам gdb
, а далее, для взаимодействия с ним используются абстракции C# - StreamReader
и StreamWriter
(можно сказать, простые дескрипторы stdin/stdout).
Вот мы выполнили свою работу - теперь нужно ответить клиенту DAP. Для реализации DAP используется собственный фреймворк и в самом его ядре находится класс DebugProtocol
. Он содержит основную логику: последний seq
, заголовок ответа, поток ввода/вывода (для взаимодействия), сериализация и т.д. Клиенту просто необходимо реализовать бизнес-логику, то есть взаимодействие с отладчиком. За ответ отвечает метод SendMessageCore
- в нем находится практически вся необходимая инфраструктурная логика: увеличение seq
, сериализация, выставление заголовка:
SendMessageCore
Это дизассемблированные исходники
public abstract class DebugProtocol
{
internal void SendMessageCore(ProtocolMessage message)
{
if (message.Seq == 0)
{
message.Seq = GetNextSequenceNumber();
}
TraceMessage(message, null, isSend: true);
string s = JsonConvert.SerializeObject(message, jsonSettings);
byte[] bytes = ProtocolEncoding.GetBytes(s);
string s2 = "Content-Length: {0}\r\n\r\n".FormatInvariantWithArgs(bytes.Length);
byte[] bytes2 = ProtocolEncoding.GetBytes(s2);
byte[] array = new byte[bytes2.Length + bytes.Length];
Buffer.BlockCopy(bytes2, 0, array, 0, bytes2.Length);
Buffer.BlockCopy(bytes, 0, array, bytes2.Length, bytes.Length);
lock (outgoingSyncObj)
{
outgoingQueue.Enqueue(array);
if (!isSendingMessages)
{
isSendingMessages = true;
messagesPendingEvent.Reset();
Task.Run(delegate
{
SendQueuedMessages();
});
}
}
}
}
Подобная обобщенная машинерия используется и для вызова соответствующего обработчика запроса на основании переданной команды. Но располагается она в классе DebugAdapterBase
:
Вызов соответствующего обработчика
public abstract class DebugAdapterBase
{
protected virtual ResponseBody HandleProtocolRequest(string requestType, object requestArgs)
{
return requestType switch
{
"addBreakpoint" => HandleAddBreakpointRequest((AddBreakpointArguments)requestArgs),
"addFavorite" => HandleAddFavoriteRequest((AddFavoriteArguments)requestArgs),
"attach" => HandleAttachRequest((AttachArguments)requestArgs),
"breakpointLocations" => HandleBreakpointLocationsRequest((BreakpointLocationsArguments)requestArgs),
"cancel" => HandleCancelRequest((CancelArguments)requestArgs),
"completions" => HandleCompletionsRequest((CompletionsArguments)requestArgs),
"configurationDone" => HandleConfigurationDoneRequest((ConfigurationDoneArguments)requestArgs),
"continue" => HandleContinueRequest((ContinueArguments)requestArgs),
"createObjectId" => HandleCreateObjectIdRequest((CreateObjectIdArguments)requestArgs),
"dataBreakpointInfo" => HandleDataBreakpointInfoRequest((DataBreakpointInfoArguments)requestArgs),
"destroyObjectId" => HandleDestroyObjectIdRequest((DestroyObjectIdArguments)requestArgs),
"disassemble" => HandleDisassembleRequest((DisassembleArguments)requestArgs),
"disconnect" => HandleDisconnectRequest((DisconnectArguments)requestArgs),
"evaluate" => HandleEvaluateRequest((EvaluateArguments)requestArgs),
"exceptionInfo" => HandleExceptionInfoRequest((ExceptionInfoArguments)requestArgs),
"exceptionStackTrace" => HandleExceptionStackTraceRequest((ExceptionStackTraceArguments)requestArgs),
"goto" => HandleGotoRequest((GotoArguments)requestArgs),
"gotoTargets" => HandleGotoTargetsRequest((GotoTargetsArguments)requestArgs),
"initialize" => HandleInitializeRequest((InitializeArguments)requestArgs),
"launch" => HandleLaunchRequest((LaunchArguments)requestArgs),
"loadedSources" => HandleLoadedSourcesRequest((LoadedSourcesArguments)requestArgs),
"loadSymbols" => HandleLoadSymbolsRequest((LoadSymbolsArguments)requestArgs),
"modules" => HandleModulesRequest((ModulesArguments)requestArgs),
"moduleSymbolSearchLog" => HandleModuleSymbolSearchLogRequest((ModuleSymbolSearchLogArguments)requestArgs),
"next" => HandleNextRequest((NextArguments)requestArgs),
"pause" => HandlePauseRequest((PauseArguments)requestArgs),
"readMemory" => HandleReadMemoryRequest((ReadMemoryArguments)requestArgs),
"removeBreakpoint" => HandleRemoveBreakpointRequest((RemoveBreakpointArguments)requestArgs),
"removeFavorite" => HandleRemoveFavoriteRequest((RemoveFavoriteArguments)requestArgs),
"restartFrame" => HandleRestartFrameRequest((RestartFrameArguments)requestArgs),
"restart" => HandleRestartRequest((RestartArguments)requestArgs),
"reverseContinue" => HandleReverseContinueRequest((ReverseContinueArguments)requestArgs),
"scopes" => HandleScopesRequest((ScopesArguments)requestArgs),
"setBreakpoints" => HandleSetBreakpointsRequest((SetBreakpointsArguments)requestArgs),
"setDataBreakpoints" => HandleSetDataBreakpointsRequest((SetDataBreakpointsArguments)requestArgs),
"setDebuggerProperty" => HandleSetDebuggerPropertyRequest((SetDebuggerPropertyArguments)requestArgs),
"setExceptionBreakpoints" => HandleSetExceptionBreakpointsRequest((SetExceptionBreakpointsArguments)requestArgs),
"setExpression" => HandleSetExpressionRequest((SetExpressionArguments)requestArgs),
"setFunctionBreakpoints" => HandleSetFunctionBreakpointsRequest((SetFunctionBreakpointsArguments)requestArgs),
"setHitCount" => HandleSetHitCountRequest((SetHitCountArguments)requestArgs),
"setInstructionBreakpoints" => HandleSetInstructionBreakpointsRequest((SetInstructionBreakpointsArguments)requestArgs),
"setSymbolOptions" => HandleSetSymbolOptionsRequest((SetSymbolOptionsArguments)requestArgs),
"setVariable" => HandleSetVariableRequest((SetVariableArguments)requestArgs),
"source" => HandleSourceRequest((SourceArguments)requestArgs),
"stackTrace" => HandleStackTraceRequest((StackTraceArguments)requestArgs),
"stepBack" => HandleStepBackRequest((StepBackArguments)requestArgs),
"stepIn" => HandleStepInRequest((StepInArguments)requestArgs),
"stepInTargets" => HandleStepInTargetsRequest((StepInTargetsArguments)requestArgs),
"stepOut" => HandleStepOutRequest((StepOutArguments)requestArgs),
"terminate" => HandleTerminateRequest((TerminateArguments)requestArgs),
"terminateThreads" => HandleTerminateThreadsRequest((TerminateThreadsArguments)requestArgs),
"threads" => HandleThreadsRequest((ThreadsArguments)requestArgs),
"updateBreakpoint" => HandleUpdateBreakpointRequest((UpdateBreakpointArguments)requestArgs),
"variables" => HandleVariablesRequest((VariablesArguments)requestArgs),
"vsCustomMessage" => HandleVsCustomMessageRequest((VsCustomMessageArguments)requestArgs),
"writeMemory" => HandleWriteMemoryRequest((WriteMemoryArguments)requestArgs),
_ => throw new InvalidOperationException("Unknown request type '{0}'!".FormatInvariantWithArgs(requestType)),
};
}
}
Таким образом, можно сказать, что инфраструктурная часть реализуется с помощью паттерна Шаблонный метод.
Точки останова
Теперь перейдем к точкам останова. В DAP они разделяются на 3 вида: function (на входе в функцию), data (точки останова на данные, watchpoint) и обычные. Я рассмотрю самые обычные точки останова. За них отвечает метод HandleSetBreakpointsRequestAsync
и запрос setBreakpoints
в DAP.
Здесь адаптер выступает в роли кэша - он запоминает какие точки останова уже были выставлены и при необходимости добавляет или удаляет.
HandleSetBreakpointsRequestAsync
internal sealed class AD7DebugSession
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/OpenDebugAD7/AD7DebugSession.cs#L2349 */
protected override void HandleSetBreakpointsRequestAsync(IRequestResponder<SetBreakpointsArguments, SetBreakpointsResponse> responder)
{
string path = null;
string name = null;
if (responder.Arguments.Source != null)
{
string p = responder.Arguments.Source.Path;
if (p != null && p.Trim().Length > 0)
{
path = p;
}
string nm = responder.Arguments.Source.Name;
if (nm != null && nm.Trim().Length > 0)
{
name = nm;
}
}
var source = new Source()
{
Name = name,
Path = path,
SourceReference = 0
};
List<SourceBreakpoint> breakpoints = responder.Arguments.Breakpoints;
bool sourceModified = responder.Arguments.SourceModified.GetValueOrDefault(false);
string convertedPath = m_pathConverter.ConvertClientPathToDebugger(source.Path);
HashSet<int> lines = new HashSet<int>(breakpoints.Select((b) => b.Line));
Dictionary<int, IDebugPendingBreakpoint2> dict = null;
if (m_breakpoints.ContainsKey(convertedPath))
{
dict = m_breakpoints[convertedPath];
var keys = new int[dict.Keys.Count];
dict.Keys.CopyTo(keys, 0);
foreach (var l in keys)
{
// Delete all breakpoints that are no longer listed.
// In the case of modified source, delete everything.
if (!lines.Contains(l) || sourceModified)
{
var bp = dict[l];
bp.Delete();
dict.Remove(l);
}
}
}
else
{
dict = new Dictionary<int, IDebugPendingBreakpoint2>();
m_breakpoints[convertedPath] = dict;
}
foreach (var bp in breakpoints)
{
if (dict.ContainsKey(bp.Line))
{
// already created
IDebugBreakpointRequest2 breakpointRequest;
if (dict[bp.Line].GetBreakpointRequest(out breakpointRequest) == 0 &&
breakpointRequest is AD7BreakPointRequest ad7BPRequest)
{
// Check to see if this breakpoint has a condition that has changed.
if (!StringComparer.Ordinal.Equals(ad7BPRequest.Condition, bp.Condition))
{
// Condition has been modified. Delete breakpoint so it will be recreated with the updated condition.
var toRemove = dict[bp.Line];
toRemove.Delete();
dict.Remove(bp.Line);
}
// Check to see if tracepoint changed
else if (!StringComparer.Ordinal.Equals(ad7BPRequest.LogMessage, bp.LogMessage))
{
ad7BPRequest.ClearTracepoint();
var toRemove = dict[bp.Line];
toRemove.Delete();
dict.Remove(bp.Line);
}
else
{
if (ad7BPRequest.BindResult != null)
{
// use the breakpoint created from IDebugBreakpointErrorEvent2 or IDebugBreakpointBoundEvent2
resBreakpoints.Add(ad7BPRequest.BindResult);
}
else
{
resBreakpoints.Add(new Breakpoint()
{
Id = (int)ad7BPRequest.Id,
Verified = true,
Line = bp.Line
});
}
continue;
}
}
}
// Create a new breakpoint
if (!dict.ContainsKey(bp.Line))
{
IDebugPendingBreakpoint2 pendingBp;
AD7BreakPointRequest pBPRequest = new AD7BreakPointRequest(m_sessionConfig, convertedPath, m_pathConverter.ConvertClientLineToDebugger(bp.Line), bp.Condition);
try
{
bool verified = true;
if (!string.IsNullOrEmpty(bp.LogMessage))
{
// Make sure tracepoint is valid.
verified = pBPRequest.SetLogMessage(bp.LogMessage);
}
if (verified)
{
eb.CheckHR(m_engine.CreatePendingBreakpoint(pBPRequest, out pendingBp));
eb.CheckHR(pendingBp.Bind());
dict[bp.Line] = pendingBp;
resBreakpoints.Add(new Breakpoint()
{
Id = (int)pBPRequest.Id,
Verified = verified,
Line = bp.Line
});
}
else
{
resBreakpoints.Add(new Breakpoint()
{
Id = (int)pBPRequest.Id,
Verified = verified,
Line = bp.Line,
Message = string.Format(CultureInfo.CurrentCulture, AD7Resources.Error_UnableToParseLogMessage)
});
}
}
catch (Exception e)
{
e = Utilities.GetInnerMost(e);
if (Utilities.IsCorruptingException(e))
{
Utilities.ReportException(e);
}
resBreakpoints.Add(new Breakpoint()
{
Id = (int)pBPRequest.Id,
Verified = false,
Line = bp.Line,
Message = eb.GetMessageForException(e)
});
}
}
}
}
}
Ответственность за создание и управление (выставление/удаление) точкой останова лежит на разных сущностях. Создание - это движок (MIEngine), а управление - на самой точке останова.
За создание отвечает метод CreatePendingBreakpoint
. По факту, что он делает - создает новый объект, выставляет необходимые поля и добавляет ее в свой список. Поэтому опустим реализацию и сконцентрируемся на управлении.
Управление точкой останова проблема самой точки останова. Перед тем как выполнить целевое действие она инициализирует и проверяет свое состояние: условие срабатывания, расположение и т.д. Когда приходит время выставить или удалить точку останова (отправить команду), то это делегируется классу MICommandFactory
.
Из названия становится понятно, что это вспомогательный класс, который используется работы с командами MI. Так и есть, этот класс предоставляет программный интерфейс для команд MI протокола, а сам внутри собирает строки этих команд. Например, для выставления точки останова используется метод BreakInsert
:
BreakInsert
Их 3 версии - на строку в файле, функцию и адрес инструкции.
public abstract class MICommandFactory
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L482 */
public virtual async Task<Results> BreakInsert(string filename, bool useUnixFormat, uint line, string condition, bool enabled, IEnumerable<Checksum> checksums = null, ResultClass resultClass = ResultClass.done)
{
StringBuilder cmd = await BuildBreakInsert(condition, enabled);
if (checksums != null && checksums.Any())
{
cmd.Append(Checksum.GetMIString(checksums));
cmd.Append(' ');
}
string filenameMI;
bool quotes = PreparePath(filename, useUnixFormat, out filenameMI);
if (quotes)
{
cmd.Append('\"');
}
cmd.Append(filenameMI);
cmd.Append(':');
cmd.Append(line.ToString(CultureInfo.InvariantCulture));
if (quotes)
{
cmd.Append('\"');
}
return await _debugger.CmdAsync(cmd.ToString(), resultClass);
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L509 */
public virtual async Task<Results> BreakInsert(string functionName, string condition, bool enabled, ResultClass resultClass = ResultClass.done)
{
StringBuilder cmd = await BuildBreakInsert(condition, enabled);
cmd.Append(functionName);
return await _debugger.CmdAsync(cmd.ToString(), resultClass);
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L517 */
public virtual async Task<Results> BreakInsert(ulong codeAddress, string condition, bool enabled, ResultClass resultClass = ResultClass.done)
{
StringBuilder cmd = await BuildBreakInsert(condition, enabled);
cmd.Append('*');
cmd.Append(codeAddress);
return await _debugger.CmdAsync(cmd.ToString(), resultClass);
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L445 */
public virtual Task<StringBuilder> BuildBreakInsert(string condition, bool enabled)
{
StringBuilder cmd = new StringBuilder("-break-insert -f ");
if (condition != null)
{
cmd.Append("-c \"");
cmd.Append(EscapeQuotes(condition));
cmd.Append("\" ");
}
if (!enabled)
{
cmd.Append("-d ");
}
if (_debugger.LaunchOptions.RequireHardwareBreakpoints)
{
cmd.Append("-h ");
}
return Task<StringBuilder>.FromResult(cmd);
}
}
Удаление происходит аналогично и метод для MI - BreakDelete
:
BreakDelete
public abstract class MICommandFactory
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L555 */
public virtual async Task BreakDelete(string bkptno, ResultClass resultClass = ResultClass.done)
{
await _debugger.CmdAsync("-break-delete " + bkptno, resultClass);
}
}
Шаги
Переходим к шагам. В DAP за них отвечают команды next
, stepIn
и stepOut
- step over, step into и step out соответственно.
Обработка этих запросов происходит похожим образом как видели ранее - все они обрабатываются единственным методом, в который просто передается тип шага, - StepInternal
. И вся его логика состоит в том, чтобы вызвать метод Step
у объекта движка. Который в свою очередь просто вызовет нужный метод у MICommandfFactory. Никакой другой логики кроме валидации и маппинга особо нет (в сравнении с другим кодом).
Step XXX
internal sealed class AD7DebugSession
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/OpenDebugAD7/AD7DebugSession.cs#L1576 */
protected override void HandleStepInRequestAsync(IRequestResponder<StepInArguments> responder)
{
try
{
var granularity = responder.Arguments.Granularity.GetValueOrDefault();
StepInternal(responder.Arguments.ThreadId, enum_STEPKIND.STEP_INTO, granularity, AD7Resources.Error_Scenario_Step_In);
responder.SetResponse(new StepInResponse());
}
catch (AD7Exception e)
{
responder.SetError(new ProtocolException(e.Message));
}
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/OpenDebugAD7/AD7DebugSession.cs#L1590 */
protected override void HandleNextRequestAsync(IRequestResponder<NextArguments> responder)
{
try
{
var granularity = responder.Arguments.Granularity.GetValueOrDefault();
StepInternal(responder.Arguments.ThreadId, enum_STEPKIND.STEP_OVER, granularity, AD7Resources.Error_Scenario_Step_Next);
responder.SetResponse(new NextResponse());
}
catch (AD7Exception e)
{
responder.SetError(new ProtocolException(e.Message));
}
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/OpenDebugAD7/AD7DebugSession.cs#L1604 */
protected override void HandleStepOutRequestAsync(IRequestResponder<StepOutArguments> responder)
{
try
{
var granularity = responder.Arguments.Granularity.GetValueOrDefault();
StepInternal(responder.Arguments.ThreadId, enum_STEPKIND.STEP_OUT, granularity, AD7Resources.Error_Scenario_Step_Out);
responder.SetResponse(new StepOutResponse());
}
catch (AD7Exception e)
{
responder.SetError(new ProtocolException(e.Message));
}
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/OpenDebugAD7/AD7DebugSession.cs#L877 */
private void StepInternal(int threadId, enum_STEPKIND stepKind, SteppingGranularity granularity, string errorMessage)
{
// If we are already running ignore additional step requests
if (!m_isStopped)
return;
IDebugThread2 thread = null;
lock (m_threads)
{
if (!m_threads.TryGetValue(threadId, out thread))
{
throw new AD7Exception(errorMessage);
}
}
ErrorBuilder builder = new ErrorBuilder(() => errorMessage);
m_isStepping = true;
enum_STEPUNIT stepUnit = enum_STEPUNIT.STEP_STATEMENT;
switch (granularity)
{
case SteppingGranularity.Statement:
default:
break;
case SteppingGranularity.Line:
stepUnit = enum_STEPUNIT.STEP_LINE;
break;
case SteppingGranularity.Instruction:
stepUnit = enum_STEPUNIT.STEP_INSTRUCTION;
break;
}
try
{
builder.CheckHR(m_program.Step(thread, stepKind, stepUnit));
}
catch (AD7Exception)
{
m_isStopped = true;
throw;
}
// The program should now be stepping, so it is safe to discard the
// cached program state.
BeforeContinue();
m_isStepping = true;
}
}
sealed public class AD7Engine
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MIDebugEngine/AD7.Impl/AD7Engine.cs#L965 */
public int Step(IDebugThread2 pThread, enum_STEPKIND kind, enum_STEPUNIT unit)
{
AD7Thread thread = (AD7Thread)pThread;
try
{
if (null == thread || null == thread.GetDebuggedThread())
{
return Constants.E_FAIL;
}
_debuggedProcess.WorkerThread.RunOperation(() => _debuggedProcess.Step(thread.GetDebuggedThread().Id, kind, unit));
}
catch (InvalidCoreDumpOperationException)
{
return AD7_HRESULT.E_CRASHDUMP_UNSUPPORTED;
}
catch (Exception e)
{
_engineCallback.OnError(EngineUtils.GetExceptionDescription(e));
return Constants.E_ABORT;
}
return Constants.S_OK;
}
}
internal class DebuggedProcess
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs#L1725 */
public async Task Step(int threadId, enum_STEPKIND kind, enum_STEPUNIT unit)
{
this.VerifyNotDebuggingCoreDump();
await ExceptionManager.EnsureSettingsUpdated();
if ((unit == enum_STEPUNIT.STEP_LINE) || (unit == enum_STEPUNIT.STEP_STATEMENT))
{
switch (kind)
{
case enum_STEPKIND.STEP_INTO:
await MICommandFactory.ExecStep(threadId);
break;
case enum_STEPKIND.STEP_OVER:
await MICommandFactory.ExecNext(threadId);
break;
case enum_STEPKIND.STEP_OUT:
await MICommandFactory.ExecFinish(threadId);
break;
default:
throw new NotImplementedException();
}
}
else if (unit == enum_STEPUNIT.STEP_INSTRUCTION)
{
switch (kind)
{
case enum_STEPKIND.STEP_INTO:
await MICommandFactory.ExecStepInstruction(threadId);
break;
case enum_STEPKIND.STEP_OVER:
await MICommandFactory.ExecNextInstruction(threadId);
break;
case enum_STEPKIND.STEP_OUT:
await MICommandFactory.ExecFinish(threadId);
break;
default:
throw new NotImplementedException();
}
}
else
{
throw new NotImplementedException();
}
}
}
public abstract class MICommandFactory
{
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L232 */
public async Task ExecStep(int threadId, ResultClass resultClass = ResultClass.running)
{
string command = "-exec-step";
string args = string.Empty;
await ThreadFrameCmdAsync(command, args, resultClass, threadId, 0);
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L239 */
public async Task ExecNext(int threadId, ResultClass resultClass = ResultClass.running)
{
string command = "-exec-next";
string args = string.Empty;
await ThreadFrameCmdAsync(command, args, resultClass, threadId, 0);
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L246 */
public async Task ExecFinish(int threadId, ResultClass resultClass = ResultClass.running)
{
string command = "-exec-finish";
string args = string.Empty;
await ThreadFrameCmdAsync(command, args, resultClass, threadId, 0);
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L253 */
public async Task ExecStepInstruction(int threadId, ResultClass resultClass = ResultClass.running)
{
string command = "-exec-step-instruction";
string args = string.Empty;
await ThreadFrameCmdAsync(command, args, resultClass, threadId, 0);
}
/* https://github.com/microsoft/MIEngine/blob/7a111c9fe46107969b4ffa674df059def7124364/src/MICore/CommandFactories/MICommandFactory.cs#L260 */
public async Task ExecNextInstruction(int threadId, ResultClass resultClass = ResultClass.running)
{
string command = "-exec-next-instruction";
string args = string.Empty;
await ThreadFrameCmdAsync(command, args, resultClass, threadId, 0);
}
}
Code::Blocks
Code::Blocks - это кроссплатформенная IDE для C/C++/D/Fortran. Для визуальной части используется wxWidgets (что важно).
Отладка поддерживается IDE, но сама же функциональность реализована с помощью плагинов (отладчики реализуют интерфейс cbDebuggerPlugin
). В частности, плагин debuggergdb
- добавляет поддержку отладки с помощью GDB и CDB (Microsoft Console Debugger, для Windows).
Код визуальной части не тема статьи, но обойти не могу, поэтому вкратце. Имеется класс DebuggerManager
и он тот самый менеджер - ничего не делает, а только дает доступ к другим. И под другими, в основном говорю об окнах. Таким окном, например, является cbBreakpointsDlg
- окно точек останова. И вот эти самые окна содержат методы-обработчики кнопок, которые и запускают функциональность. Для того же окна точек останова есть метод AddBreakpoints
, который как можно догадаться и вызывает логику добавления точки останова.
Для начала поговорим о главном - кто же отвечает за логику отладки. Уже заспойлерил - сами отладчики, а IDE просто с ними взаимодействует. Для взаимодействия с отладчиком используются методы класса cbDebuggerPlugin
. В debuggergdb
находится единственная реализация этого интерфейса - DebuggerGDB
. И вот тут еще одна интересная деталь - это класс также является простым фасадом. Почему? Как минимум потому что поддерживается не только gdb, но и cdb, а интерфейс у них разный. Поэтому логика адресуется интерфейсу DebuggerDriver
. И этих драйверов 2: GDB_driver
и CDB_driver
.
Так как же происходит взаимодействие с ними? Все просто - запускаем через консоль, а затем взаимодействуем с помощью stdin/stdout/stderr. Причем для запуска используется функция wxExecute
(из wxWidgets, как можно догадаться). А строку запуска создает уже сам драйвер, так как он сам знает где лежит его исполняемый файл.
Запуск отладчика
/* https://svn.code.sf.net/p/codeblocks/code/tags/20.03/src/plugins/debuggergdb/debuggergdb.cpp */
int DebuggerGDB::DoDebug(bool breakOnEntry)
{
/* ... */
wxString cmdline;
if (m_PidToAttach == 0)
{
/* ... */
cmdline = m_State.GetDriver()->GetCommandLine(cmdexe, debuggee, GetActiveConfigEx().GetUserArguments());
}
else // m_PidToAttach != 0
cmdline = m_State.GetDriver()->GetCommandLine(cmdexe, m_PidToAttach, GetActiveConfigEx().GetUserArguments());
/* ... */
// start the gdb process
wxString wdir = m_State.GetDriver()->GetDebuggersWorkingDirectory();
if (wdir.empty())
wdir = m_pProject ? m_pProject->GetBasePath() : _T(".");
int ret = LaunchProcess(cmdline, wdir);
/* ... */
} // Debug
int DebuggerGDB::LaunchProcess(const wxString& cmd, const wxString& cwd)
{
if (m_pProcess)
return -1;
// start the gdb process
m_pProcess = new PipedProcess(&m_pProcess, this, idGDBProcess, true, cwd);
Log(_("Starting debugger: ") + cmd);
m_Pid = LaunchProcessWithShell(cmd, m_pProcess, cwd);
if (!m_Pid)
{
delete m_pProcess;
m_pProcess = 0;
Log(_("failed"), Logger::error);
return -1;
}
else if (!m_pProcess->GetOutputStream())
{
delete m_pProcess;
m_pProcess = 0;
Log(_("failed (to get debugger's stdin)"), Logger::error);
return -2;
}
else if (!m_pProcess->GetInputStream())
{
delete m_pProcess;
m_pProcess = 0;
Log(_("failed (to get debugger's stdout)"), Logger::error);
return -2;
}
else if (!m_pProcess->GetErrorStream())
{
delete m_pProcess;
m_pProcess = 0;
Log(_("failed (to get debugger's stderr)"), Logger::error);
return -2;
}
return 0;
}
/* https://svn.code.sf.net/p/codeblocks/code/tags/20.03/src/plugins/debuggergdb/gdb_driver.cpp */
wxString GDB_driver::GetCommandLine(const wxString& debugger, const wxString& debuggee, const wxString &userArguments)
{
wxString cmd;
cmd << debugger;
if (m_pDBG->GetActiveConfigEx().GetFlag(DebuggerConfiguration::DisableInit))
cmd << _T(" -nx"); // don't run .gdbinit
cmd << _T(" -fullname"); // report full-path filenames when breaking
cmd << _T(" -quiet"); // don't display version on startup
cmd << wxT(" ") << userArguments;
cmd << _T(" -args ") << debuggee;
return cmd;
}
wxString GDB_driver::GetCommandLine(const wxString& debugger, cb_unused int pid, const wxString &userArguments)
{
wxString cmd;
cmd << debugger;
if (m_pDBG->GetActiveConfigEx().GetFlag(DebuggerConfiguration::DisableInit))
cmd << _T(" -nx"); // don't run .gdbinit
cmd << _T(" -fullname"); // report full-path filenames when breaking
cmd << _T(" -quiet"); // don't display version on startup
cmd << wxT(" ") << userArguments;
return cmd;
}
/* https://svn.code.sf.net/p/codeblocks/code/tags/20.03/src/plugins/debuggergdb/cdb_driver.cpp */
wxString CDB_driver::GetCommandLine(const wxString& debugger, const wxString& debuggee, cb_unused const wxString &userArguments)
{
wxString cmd = GetCommonCommandLine(debugger);
cmd << _T(' ');
// finally, add the program to debug
wxFileName debuggeeFileName(debuggee);
if (debuggeeFileName.IsAbsolute())
cmd << debuggee;
else
cmd << m_Target->GetParentProject()->GetBasePath() << wxT("/") << debuggee;
return cmd;
}
wxString CDB_driver::GetCommonCommandLine(const wxString& debugger)
{
wxString cmd;
cmd << debugger;
// cmd << _T(" -g"); // ignore starting breakpoint
cmd << _T(" -G"); // ignore ending breakpoint
cmd << _T(" -lines"); // line info
if (m_Target->GetTargetType() == ttConsoleOnly)
cmd << wxT(" -2"); // tell the debugger to launch a console for us
if (m_Dirs.GetCount() > 0)
{
// add symbols dirs
cmd << _T(" -y ");
for (unsigned int i = 0; i < m_Dirs.GetCount(); ++i)
cmd << m_Dirs[i] << wxPATH_SEP;
// add source dirs
cmd << _T(" -srcpath ");
for (unsigned int i = 0; i < m_Dirs.GetCount(); ++i)
cmd << m_Dirs[i] << wxPATH_SEP;
}
return cmd;
}
wxString CDB_driver::GetCommandLine(const wxString& debugger, int pid, cb_unused const wxString &userArguments)
{
wxString cmd = GetCommonCommandLine(debugger);
// finally, add the PID
cmd << _T(" -p ") << wxString::Format(_T("%d"), pid);
return cmd;
}
Но самая интересная часть - это обработка ответа. Ранее мы использовали DAP или GDB/MI. Но здесь мы запускаем интерактивный клиент, он ориентирован на человека. Писать кастомные парсеры? Почти - использовать регулярные выражения. Да, если заглянуть в код gdb_driver.cpp
, то первое, что мы увидим, это регулярные выражения для парсинга ответов, например, результат после команды выставления точки останова. И эта функциональность (regex) также поставляется wxWidgets:
Регулярки парсинга ответа
/* https://svn.code.sf.net/p/codeblocks/code/tags/20.03/src/plugins/debuggergdb/gdb_driver.cpp */
// the ">>>>>>" is a hack: sometimes, especially when watching uninitialized char*
// some random control codes in the stream (like 'delete') will mess-up our prompt and the debugger
// will seem like frozen (only "stop" button available). Using this dummy prefix,
// we allow for a few characters to be "eaten" this way and still get our
// expected prompt back.
#define GDB_PROMPT _T("cb_gdb:")
#define FULL_GDB_PROMPT _T(">>>>>>") GDB_PROMPT
//[Switching to thread 2 (Thread 1082132832 (LWP 12298))]#0 0x00002aaaac5a2aca in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/libpthread.so.0
static wxRegEx reThreadSwitch(_T("^\\[Switching to thread .*\\]#0[ \t]+(0x[A-Fa-f0-9]+) in (.*) from (.*)"));
static wxRegEx reThreadSwitch2(_T("^\\[Switching to thread .*\\]#0[ \t]+(0x[A-Fa-f0-9]+) in (.*) from (.*):([0-9]+)"));
// Regular expresion for breakpoint. wxRegEx don't want to recognize '?' command, so a bit more general rule is used
// here.
// ([A-Za-z]*[:]*) corresponds to windows disk name. Under linux it can be none empty in crosscompiling sessions;
// ([^:]+) corresponds to the path in linux or to the path within windows disk in windows to current file;
// ([0-9]+) corresponds to line number in current file;
// (0x[0-9A-Fa-f]+) correponds to current memory address.
static wxRegEx reBreak(_T("\032*([A-Za-z]*[:]*)([^:]+):([0-9]+):[0-9]+:[begmidl]+:(0x[0-9A-Fa-f]+)"));
static wxRegEx reBreak2(_T("^(0x[A-Fa-f0-9]+) in (.*) from (.*)"));
static wxRegEx reBreak3(_T("^(0x[A-Fa-f0-9]+) in (.*)"));
// Catchpoint 1 (exception thrown), 0x00007ffff7b982b0 in __cxa_throw () from /usr/lib/gcc/x86_64-pc-linux-gnu/4.4.4/libstdc++.so.6
static wxRegEx reCatchThrow(_T("^Catchpoint ([0-9]+) \\(exception thrown\\), (0x[0-9a-f]+) in (.+) from (.+)$"));
// Catchpoint 1 (exception thrown), 0x00401610 in __cxa_throw ()
static wxRegEx reCatchThrowNoFile(_T("^Catchpoint ([0-9]+) \\(exception thrown\\), (0x[0-9a-f]+) in (.+)$"));
// easily match cygwin paths
//static wxRegEx reCygwin(_T("/cygdrive/([A-Za-z])/"));
// Pending breakpoint "C:/Devel/libs/irr_svn/source/Irrlicht/CSceneManager.cpp:1077" resolved
#ifdef __WXMSW__
static wxRegEx rePendingFound(_T("^Pending[ \t]+breakpoint[ \t]+[\"]+([A-Za-z]:)([^:]+):([0-9]+)\".*"));
#else
static wxRegEx rePendingFound(_T("^Pending[ \t]+breakpoint[ \t]+[\"]+([^:]+):([0-9]+)\".*"));
#endif
// Breakpoint 2, irr::scene::CSceneManager::getSceneNodeFromName (this=0x3fa878, name=0x3fbed8 "MainLevel", start=0x3fa87c) at CSceneManager.cpp:1077
static wxRegEx rePendingFound1(_T("^Breakpoint[ \t]+([0-9]+),.*"));
// Temporary breakpoint 2, main () at /path/projects/tests/main.cpp:136
static wxRegEx reTempBreakFound(wxT("^[Tt]emporary[ \t]breakpoint[ \t]([0-9]+),.*"));
// [Switching to Thread -1234655568 (LWP 18590)]
// [New Thread -1234655568 (LWP 18590)]
static wxRegEx reChildPid1(_T("Thread[ \t]+[xA-Fa-f0-9-]+[ \t]+\\(LWP ([0-9]+)\\)]"));
// MinGW GDB 6.8 and later
// [New Thread 2684.0xf40] or [New thread 2684.0xf40]
static wxRegEx reChildPid2(_T("\\[New [tT]hread[ \t]+[0-9]+\\.[xA-Fa-f0-9-]+\\]"));
static wxRegEx reInferiorExited(wxT("^\\[Inferior[ \\t].+[ \\t]exited normally\\]$"), wxRE_EXTENDED);
static wxRegEx reInferiorExitedWithCode(wxT("^\\[[Ii]nferior[ \\t].+[ \\t]exited[ \\t]with[ \\t]code[ \\t]([0-9]+)\\]$"), wxRE_EXTENDED);
Но на этом инфраструктура отладчика не закончена. Мы рассмотрели запуск, но что же в рантайме? Как выполняются запросы? Проблема здесь заключается в IDE, а точнее оконном приложении. Они должны быть однопоточными, но многие команды отладчика могут выполняться долго и просто заблокироваться на его ожидании означает смерть зависание всего приложения.
Эту проблему решили просто - очередь команд/паттерн команда. Все действия, которые можно отправить отладчику представляются в виде класса DebuggerCmd
(базовый класс). У него 2 главных метода: Action
- выполнение действия и ParseOutput
- парсинг ответа. А для их выполнения используется абстракция очереди команд. При добавлении новой команды она выполняется, но вот ответ парсится тогда, когда будет доступен. Ответ передается с помощью обработчика события OnGDBOutput
(или OnGDBError
).
Зная это, мы можем понять как происходит взаимодействие с отладчиком:
Форматируем строку команды как мы бы это сделали в gdb
Отправляем ее по stdin процессу
Ждем ответа от gdb
Парсим ответ
На этом можно было бы и закончить, так как это и есть ответ на основной вопрос - а по какому протоколу ведется взаимодействие. Но, чтобы закрыть гештальты, предоставлю команды добавления точки останова:
GdbCmd_AddBreakpoint
/* https://svn.code.sf.net/p/codeblocks/code/tags/20.03/src/plugins/debuggergdb/gdb_commands.h */
// Breakpoint 1 at 0x4013d6: file main.cpp, line 8.
static wxRegEx reBreakpoint(_T("Breakpoint ([0-9]+) at (0x[0-9A-Fa-f]+)"));
// GDB7.4 and before will return:
// Breakpoint 1 ("/home/jens/codeblocks-build/codeblocks-1.0svn/src/plugins/debuggergdb/gdb_commands.h:125) pending.
// GDB7.5 and later will return:
// Breakpoint 4 ("E:/code/cb/test_code/DebugDLLTest/TestDLL/dllmain.cpp:29") pending.
static wxRegEx rePendingBreakpoint(_T("Breakpoint ([0-9]+)[ \t]\\(\"(.+):([0-9]+)(\"?)\\)[ \t]pending\\."));
// Hardware assisted breakpoint 1 at 0x4013d6: file main.cpp, line 8.
static wxRegEx reHWBreakpoint(_T("Hardware assisted breakpoint ([0-9]+) at (0x[0-9A-Fa-f]+)"));
// Hardware watchpoint 1: expr
static wxRegEx reDataBreakpoint(_T("Hardware watchpoint ([0-9]+):.*"));
// Temporary breakpoint 2 at 0x401203: file /home/obfuscated/projects/tests/_cb_dbg/watches/main.cpp, line 115.
static wxRegEx reTemporaryBreakpoint(wxT("^[Tt]emporary[ \t]breakpoint[ \t]([0-9]+)[ \t]at.*"));
class GdbCmd_AddBreakpoint : public DebuggerCmd
{
cb::shared_ptr<DebuggerBreakpoint> m_BP;
public:
/** @param bp The breakpoint to set. */
GdbCmd_AddBreakpoint(DebuggerDriver* driver, cb::shared_ptr<DebuggerBreakpoint> bp)
: DebuggerCmd(driver),
m_BP(bp)
{
// gdb doesn't allow setting the bp number.
// instead, we must read it back in ParseOutput()...
m_BP->index = -1;
if (m_BP->enabled)
{
if (m_BP->type == DebuggerBreakpoint::bptCode)//m_BP->func.IsEmpty())
{
wxString out = m_BP->filename;
// we add one to line, because scintilla uses 0-based line numbers, while gdb uses 1-based
if (!m_BP->temporary)
m_Cmd << _T("break ");
else
m_Cmd << _T("tbreak ");
m_Cmd << _T('"') << out << _T(":") << wxString::Format(_T("%d"), m_BP->line) << _T('"');
}
else if (m_BP->type == DebuggerBreakpoint::bptData)
{
if (m_BP->breakOnRead && m_BP->breakOnWrite)
m_Cmd << _T("awatch ");
else if (m_BP->breakOnRead)
m_Cmd << _T("rwatch ");
else
m_Cmd << _T("watch ");
m_Cmd << m_BP->breakAddress;
}
//GDB workaround
//Use function name if this is C++ constructor/destructor
else
{
// if (m_BP->temporary)
// cbThrow(_T("Temporary breakpoint on constructor/destructor is not allowed"));
m_Cmd << _T("rbreak ") << m_BP->func;
}
//end GDB workaround
m_BP->alreadySet = true;
// condition and ignore count will be set in ParseOutput, where we 'll have the bp number
}
}
void ParseOutput(const wxString& output)
{
// possible outputs (we 're only interested in 1st and 2nd samples):
//
// Hardware watchpoint 1: expr
// Breakpoint 1 at 0x4013d6: file main.cpp, line 8.
// No line 100 in file "main.cpp".
// No source file named main2.cpp.
if (reBreakpoint.Matches(output))
{
// m_pDriver->DebugLog(wxString::Format(_("Breakpoint added: file %s, line %d"), m_BP->filename.c_str(), m_BP->line + 1));
if (!m_BP->func.IsEmpty())
m_pDriver->Log(_("GDB workaround for constructor/destructor breakpoints activated."));
reBreakpoint.GetMatch(output, 1).ToLong(&m_BP->index);
reBreakpoint.GetMatch(output, 2).ToULong(&m_BP->address, 16);
// conditional breakpoint
if (m_BP->useCondition && !m_BP->condition.IsEmpty())
{
m_pDriver->QueueCommand(new GdbCmd_AddBreakpointCondition(m_pDriver, m_BP), DebuggerDriver::High);
}
// ignore count
if (m_BP->useIgnoreCount && m_BP->ignoreCount > 0)
{
wxString cmd;
cmd << _T("ignore ") << wxString::Format(_T("%d"), (int) m_BP->index) << _T(" ") << wxString::Format(_T("%d"), (int) m_BP->ignoreCount);
m_pDriver->QueueCommand(new DebuggerCmd(m_pDriver, cmd), DebuggerDriver::High);
}
}
else if (rePendingBreakpoint.Matches(output))
{
if (!m_BP->func.IsEmpty())
m_pDriver->Log(_("GDB workaround for constructor/destructor breakpoints activated."));
rePendingBreakpoint.GetMatch(output, 1).ToLong(&m_BP->index);
// conditional breakpoint
// condition can not be evaluated for pending breakpoints, so we only set a flag and do this later
if (m_BP->useCondition && !m_BP->condition.IsEmpty())
{
m_BP->wantsCondition = true;
}
// ignore count
if (m_BP->useIgnoreCount && m_BP->ignoreCount > 0)
{
wxString cmd;
cmd << _T("ignore ") << wxString::Format(_T("%d"), (int) m_BP->index) << _T(" ") << wxString::Format(_T("%d"), (int) m_BP->ignoreCount);
m_pDriver->QueueCommand(new DebuggerCmd(m_pDriver, cmd), DebuggerDriver::High);
}
}
else if (reDataBreakpoint.Matches(output))
{
reDataBreakpoint.GetMatch(output, 1).ToLong(&m_BP->index);
}
else if (reHWBreakpoint.Matches(output))
{
reHWBreakpoint.GetMatch(output, 1).ToLong(&m_BP->index);
reHWBreakpoint.GetMatch(output, 2).ToULong(&m_BP->address, 16);
}
else if (reTemporaryBreakpoint.Matches(output))
reTemporaryBreakpoint.GetMatch(output, 1).ToLong(&m_BP->index);
else
m_pDriver->Log(output); // one of the error responses
Manager::Get()->GetDebuggerManager()->GetBreakpointDialog()->Reload();
}
};
Не надо забывать и о CDB, у него тоже есть аналогичная команда:
CdbCmd_AddBreakpoint
/* https://svn.code.sf.net/p/codeblocks/code/tags/20.03/src/plugins/debuggergdb/cdb_commands.h */
class CdbCmd_AddBreakpoint : public DebuggerCmd
{
static int m_lastIndex;
public:
/** @param bp The breakpoint to set. */
CdbCmd_AddBreakpoint(DebuggerDriver* driver, cb::shared_ptr<DebuggerBreakpoint> bp)
: DebuggerCmd(driver),
m_BP(bp)
{
if (bp->enabled)
{
if (bp->index==-1)
bp->index = m_lastIndex++;
wxString out = m_BP->filename;
// DebuggerGDB::ConvertToGDBFile(out);
QuoteStringIfNeeded(out);
// we add one to line, because scintilla uses 0-based line numbers, while cdb uses 1-based
m_Cmd << _T("bu") << wxString::Format(_T("%ld"), (int) bp->index) << _T(' ');
if (m_BP->temporary)
m_Cmd << _T("/1 ");
if (bp->func.IsEmpty())
m_Cmd << _T('`') << out << _T(":") << wxString::Format(_T("%d"), bp->line) << _T('`');
else
m_Cmd << bp->func;
bp->alreadySet = true;
}
}
void ParseOutput(const wxString& output)
{
// possible outputs (only output lines starting with ***):
//
// *** WARNING: Unable to verify checksum for Win32GUI.exe
// *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\WINDOWS\system32\USER32.dll -
// *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\WINDOWS\system32\GDI32.dll -
wxArrayString lines = GetArrayFromString(output, _T('\n'));
for (unsigned int i = 0; i < lines.GetCount(); ++i)
{
if (lines[i].StartsWith(_T("*** ")))
m_pDriver->Log(lines[i]);
}
}
cb::shared_ptr<DebuggerBreakpoint> m_BP;
};
Eclipse
Eclipse - это кроссплатформенная IDE, разрабатываемая Eclipse Foundation. Изначально, разработана на Java для Java, но сегодня поддерживает много других ЯП.
Это очень модульная IDE и многая функциональность поставляется с помощью пакетов. Отладка не исключение. В пакете org.eclipse.debug.core
содержатся интерфейсы, которые необходимо реализовать отладчику (например, IBreakpoint
- это интерфейс точки останова), а также объекты бизнес-логики (например, BreakpointManager
- класс, ответственный за управление точками останова).
Таким образом, пакет с отладчиком должен реализовать все необходимые интерфейсы. И тот самый org.eclipse.jdt.debug
это делает (репозиторий). Например, IJavaBreakpoint
- это еще один интерфейс точки останова, но уже для Java, затем идет JavaBreakpoint
- это абстрактный класс Java точки останова реализующий этот интерфейс, а вот затем все конкретные точки останова его реализуют: JavaLineBreakpoint
, JavaClasPrepareBreakpoint
...
Я буду рассказывать об отладке Java (и его плагине), хотя Eclipse поддерживает и другие языки.
В инфраструктуре eclipse
, чтобы создать плагин, необходимо отнаследоваться от класса Plugin
. Это делает класс JDIDebugPlugin
и из названия становится понятно, что для отладки используется JDI, а также JDWP.
Реализация отладчика находится в 2 пакетах: самого плагина и com.sun.jdi
. По факту, расширение - это удобная обертка над com.sun.jdi
. Последний берет на себя инфраструктурные вопросы: запуск, подключение к JVM, отправка команд и т.д.
Взаимодействие с JVM реализуется с помощью класса com.sun.jdi.SocketListen
. То есть для подключения к JVM используется сокет. Далее, мы получаем инстанс класса com.sun.jdi.VirtualMachine
- прокси для взаимодействия с JVM.
Если говорить о самом расширении, то корнем всей иерархии является класс JDIDebugTarget
, он представляет отлаживаемую JVM и все операции проводятся над ней. Когда мы хотим выполнить какую-либо операцию, то вначале выполняется наша логика проверка (расширение), а затем вызывается код com.sun.jdi
. Причем каждый такой запрос представляется в виде объекта запроса (Request
), а затем он отправляется по протоколу.
Но давайте посмотрим как это все реализуется в коде. Для начала рассмотрим как выставляются точки останова. Как уже сказал, классов точек останова несколько, но логика их выставления везде одинакова - создаем запрос (BreakpointRequest
), а затем настраиваем его (выставляем нужные поля).
Стек вызовов примерно такой:
Выставление точки останова
public abstract class JavaBreakpoint {
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/breakpoints/JavaBreakpoint.java#L512 */
protected boolean createRequest(JDIDebugTarget target, ReferenceType type)
throws CoreException {
if (shouldSkipBreakpoint()) {
return false;
}
EventRequest[] requests = newRequests(target, type);
if (requests == null) {
return false;
}
fInstalledTypeName = type.name();
for (EventRequest request : requests) {
registerRequest(request, target);
}
return true;
}
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/breakpoints/JavaBreakpoint.java#L537 */
protected void configureRequest(EventRequest request, JDIDebugTarget target)
throws CoreException {
request.setSuspendPolicy(getJDISuspendPolicy());
request.putProperty(JAVA_BREAKPOINT_PROPERTY, this);
configureRequestThreadFilter(request, target);
configureRequestHitCount(request);
configureInstanceFilters(request, target);
// Important: only enable a request after it has been configured
updateEnabledState(request, target);
}
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/breakpoints/JavaBreakpoint.java#L226*/
protected void registerRequest(EventRequest request, JDIDebugTarget target)
throws CoreException {
if (request == null) {
return;
}
List<EventRequest> reqs = getRequests(target);
if (reqs.isEmpty()) {
fRequestsByTarget.put(target, reqs);
}
reqs.add(request);
target.addJDIEventListener(this, request);
// update the install attribute on the breakpoint
if (!(request instanceof ClassPrepareRequest)) {
incrementInstallCount();
// notification
fireInstalled(target);
}
}
}
public class JavaLineBreakpoint extends JavaBreakpoint
{
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/breakpoints/JavaLineBreakpoint.java#L289 */
@Override
protected EventRequest[] newRequests(JDIDebugTarget target,
ReferenceType type) throws CoreException {
int lineNumber = getLineNumber();
List<Location> locations = determineLocations(lineNumber, type, target);
locations = filterLocations(locations);
EventRequest[] requests = new EventRequest[locations.size()];
int i = 0;
for(Location location : locations) {
requests[i] = createLineBreakpointRequest(location, target);
i++;
}
return requests;
}
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/breakpoints/JavaLineBreakpoint.java#L345 */
protected BreakpointRequest createLineBreakpointRequest(Location location,
JDIDebugTarget target) throws CoreException {
EventRequestManager manager = target.getEventRequestManager();
/* ... */
BreakpointRequest request = manager.createBreakpointRequest(location);
/* ... */
configureRequest(request, target);
/* ... */
return request;
}
}
Но здесь только код создания запроса. Где же отправка по JDWP? Она в самом классе запроса. Базовая реализация запроса - класс EventRequestImpl
. Вот он то формирует и отправляет сетевые пакеты.
EventRequestImpl
public abstract class EventRequestImpl extends MirrorImpl implements
EventRequest {
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/jdi/org/eclipse/jdi/internal/request/EventRequestImpl.java#L262 */
@Override
public synchronized void enable() {
initJdwpRequest();
ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
DataOutputStream outData = new DataOutputStream(outBytes);
writeByte(eventKind(),
"event kind", EventImpl.eventKindMap(), outData); //$NON-NLS-1$
writeByte(
suspendPolicyJDWP(),
"suspend policy", EventRequestImpl.suspendPolicyMap(), outData); //$NON-NLS-1$
writeInt(modifierCount(), "modifiers", outData); //$NON-NLS-1$
writeModifiers(outData);
JdwpReplyPacket replyPacket = requestVM(JdwpCommandPacket.ER_SET, outBytes);
defaultReplyErrorHandler(replyPacket.errorCode());
DataInputStream replyData = replyPacket.dataInStream();
fRequestID = RequestID.read(this, replyData);
virtualMachineImpl().eventRequestManagerImpl().addRequestIDMapping(this);
}
}
Отправили запрос теперь нужно ждать ответа. Где его обработка? Вернемся назад, в тот момент, когда добавляли запрос в класс JDIDebugTarget
. Мы добавили не только запрос, но и специальный класс Listener
- это колбэк, который вызывается после выполнения запроса. Обработка же ответов возлагается на класс EventDispatcher
- для него запускается отдельный поток, в котором обрабатываются ответы от JVM.
Обработка результата запроса
/* org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIDebugTarget.java */
public class JDIDebugTarget
{
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIDebugTarget.java#L380 */
public JDIDebugTarget(ILaunch launch, VirtualMachine jvm, String name,
boolean supportTerminate, boolean supportDisconnect,
IProcess process, boolean resume) {
/* ... */
initialize();
/* ... */
}
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIDebugTarget.java#L524 */
protected synchronized void initialize() {
setEventDispatcher(new EventDispatcher(this));
/* ... */
plugin.asyncExec(() -> {
EventDispatcher dispatcher = getEventDispatcher();
if (dispatcher != null) {
Thread t = new Thread(
dispatcher,
JDIDebugModel.getPluginIdentifier()
+ JDIDebugModelMessages.JDIDebugTarget_JDI_Event_Dispatcher);
t.setDaemon(true);
t.start();
}
});
}
}
/* org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/EventDispatcher.java */
public class EventDispatcher implements Runnable {
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/EventDispatcher.java#L304 */
@Override
public void run() {
VirtualMachine vm = fTarget.getVM();
EventQueue q = vm.eventQueue();
while (!isShutdown()) {
EventSet eventSet;
try {
// Get the next event set.
eventSet = q.remove(1000);
} catch (VMDisconnectedException e) {
break;
}
dispatch(eventSet);
}
}
/* https://github.com/eclipse-jdt/eclipse.jdt.debug/blob/5b7b412f5388ec404a675a77b186a744bf0063bf/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/EventDispatcher.java#L105 */
private void dispatch(EventSet eventSet) {
EventIterator iter = eventSet.eventIterator();
IJDIEventListener[] listeners = new IJDIEventListener[eventSet.size()];
boolean vote = false;
boolean resume = true;
int index = -1;
List<Event> deferredEvents = null;
while (iter.hasNext()) {
index++;
Event event = iter.nextEvent();
// Dispatch events to registered listeners, if any
IJDIEventListener listener = fEventHandlers.get(event.request());
listeners[index] = listener;
if (listener != null) {
if (listener instanceof IJavaLineBreakpoint) {
// Event dispatch to conditional breakpoints is deferred
// until after
// other listeners vote.
try {
if (((IJavaLineBreakpoint) listener).isConditionEnabled()) {
if (deferredEvents == null) {
deferredEvents = new ArrayList<>(5);
}
deferredEvents.add(event);
continue;
}
} catch (CoreException exception) {
JDIDebugPlugin.log(exception);
}
}
vote = true;
try {
try {
resume = listener.handleEvent(event, fTarget, !resume, eventSet) && resume;
} finally {
enableGCForExceptionEvent(event);
}
} catch (Throwable t) {
logHandleEventError(listener, event, t);
}
continue;
}
// Dispatch VM start/end events
if (event instanceof VMDeathEvent) {
fTarget.handleVMDeath((VMDeathEvent) event);
shutdown(); // stop listening for events
} else if (event instanceof VMDisconnectEvent) {
fTarget.handleVMDisconnect((VMDisconnectEvent) event);
shutdown(); // stop listening for events
} else if (event instanceof VMStartEvent) {
fTarget.handleVMStart((VMStartEvent) event);
} else {
// not handled
}
}
/* ... */
}
}
Таким образом, процесс отладки такой:
UI вызывает обработчик и он доходит до
JDIDebugTarget
Находится объект, ответственный за логику (если это точка останова, то вызывается ее код)
Создается и регистрируется объект запроса
Запрос отправляется по протоколу JDWP
Регистрируется обработчик результата команды
EventDispatcher
получает ответ от JVMВызывается обработчик запроса
Spyder
Spyder - это научно-ориентированная IDE для Python. Соответственно, у нее есть интеграция с популярными Data Science библиотеками: NumPy, SciPy, Matplotlib, Pandas. Если когда-нибудь запускали PyCharm с Matplotlib, то справа высвечивались отформатированные таблицы и графики - здесь то же самое (бросив беглый взгляд, я разницы в этих таблицах/графиках не заметил).
Для запуска кода используется IPython
(интерактивная оболочка для выполнения кода). Его логика выполнения отличается от ванильного Python, но не сильно.
Поддержка отладки также реализуется за счет плагинов (но IDE знает об отладке и для отладчика выделен отдельный тип плагина). Есть 2 плагина: ipythonconsole
- интерактивная консоль IPython, и debugger
- сама функциональность отладчика. Первый предоставляет интерфейс для запуска команд отладчика (с помощью pdb
), а debugger
- вызывает команды отладчика (step over, breakpoint и т.д.).
Поддерживается 4 команды: next
(step over), step
(step into), return
(step out) и continue
(продолжить выполнение). Но вот как они выполняются отличается от рассмотренных ранее вариантов. Если уже работали с IPython, то знаете, что она запускает интерактивную консоль и для управляющих команд используется префикс !
. Так вот, плагин debugger
просто отправляет эти команды в консоль как текст. Например, когда мы нажимаем кнопку step over
, то !next
отправляется в консоль и выполняется.
Выполнение команд
class DebuggerWidget:
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/spyder/plugins/debugger/widgets/main_widget.py#L210
def setup(self):
# ...
next_action = self.create_action(
DebuggerWidgetActions.Next,
text=_("Execute current line"),
icon=self.create_icon('arrow-step-over'),
triggered=lambda: self.debug_command("next"),
register_shortcut=True
)
continue_action = self.create_action(
DebuggerWidgetActions.Continue,
text=_("Continue execution until next breakpoint"),
icon=self.create_icon('arrow-continue'),
triggered=lambda: self.debug_command("continue"),
register_shortcut=True
)
step_action = self.create_action(
DebuggerWidgetActions.Step,
text=_("Step into function or method"),
icon=self.create_icon('arrow-step-in'),
triggered=lambda: self.debug_command("step"),
register_shortcut=True
)
return_action = self.create_action(
DebuggerWidgetActions.Return,
text=_("Execute until function or method returns"),
icon=self.create_icon('arrow-step-out'),
triggered=lambda: self.debug_command("return"),
register_shortcut=True
)
# ...
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/spyder/plugins/debugger/widgets/main_widget.py#L672
def debug_command(self, command):
"""Debug actions"""
self.sig_unmaximize_plugin_requested.emit()
widget = self.current_widget()
if widget is None:
return
widget.shellwidget.pdb_execute_command(command)
class DebuggingWidget:
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/spyder/plugins/ipythonconsole/widgets/debugging.py#L282
def pdb_execute_command(self, command):
"""
Execute a pdb command
"""
self._pdb_take_focus = False
self.pdb_execute(
self._pdb_cmd_prefix() + command, hidden=False,
echo_stack_entry=False, add_history=False)
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/spyder/plugins/ipythonconsole/widgets/debugging.py#L317
def pdb_execute(self, line, hidden=False, echo_stack_entry=True,
add_history=True):
# ...
self.executing.emit(line)
# ...
Но мы просто переложили ответственность за исполнение команд на другую сущность. Давайте рассмотрим то, как эти команды выполняет IPython
. Для начала запуск.
Хоть за запуск отвечает ядро IPython
, но при отладке мы можем добавить свои хуки - magic
(деталь реализации IPython
). Такие магические команды начинаются с %
. Если IPython
встречает такую команду, то вызывает ассоциированный с ним обработчик. Для отладки IDE регистрирует команду debugfile
- магическую команду. При ее вызове управление передается Pdb
- отладчику Python, о котором уже говорили. Но не конкретно ему - IPython
и spyder
создают свои подклассы, наследующиеся от Pdb
(добавляют свою логику).
Таким образом, запуск кода под отладкой имеет следующий вид:
Запуск под отладкой
@magics_class
class SpyderCodeRunner(Magics):
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py#L179
@runfile_arguments
@needs_local_scope
@line_magic
def debugfile(self, line, local_ns=None):
"""
Debug a file.
"""
args, local_ns = self._parse_runfile_argstring(
self.debugfile, line, local_ns)
with self._debugger_exec(args.canonic_filename, True) as debug_exec:
self._exec_file(
filename=args.filename,
canonic_filename=args.canonic_filename,
args=args.args,
wdir=args.wdir,
exec_fun=debug_exec
)
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py#L238
@contextmanager
def _debugger_exec(self, filename, continue_if_has_breakpoints):
"""Get an exec function to use for debugging."""
if not self.shell.is_debugging():
debugger = SpyderPdb()
debugger.set_remote_filename(filename)
debugger.continue_if_has_breakpoints = continue_if_has_breakpoints
yield debugger.run
return
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py#L258
def _exec_file(
self,
filename=None,
args=None,
wdir=None,
current_namespace=False,
exec_fun=None
):
# ...
self._exec_code(
file_code,
filename,
ns_globals,
ns_locals,
exec_fun=exec_fun)
# ...
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/external-deps/spyder-kernels/spyder_kernels/customize/code_runner.py#L438
def _exec_code(
self,
code,
filename,
ns_globals,
ns_locals=None,
exec_fun=None
):
# ...
ast_code = ast.parse(self._transform_cell(code))
# ...
exec_encapsulate_locals(
ast_code, ns_globals, ns_locals, exec_fun, filename
)
# ...
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/external-deps/spyder-kernels/spyder_kernels/customize/utils.py#L132
def exec_encapsulate_locals(
code_ast, globals, locals, exec_fun=None, filename=None
):
# exec_fun == SpyderPdb.run
exec_fun(compile(code_ast, filename, "exec"), globals, None)
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py#L850
class SpyderPdb(ipyPdb):
def run(self, cmd, globals=None, locals=None):
"""Debug a statement executed via the exec() function.
globals defaults to __main__.dict; locals defaults to globals.
"""
with DebugWrapper(self):
# Вызывает Bdb.run()
super(SpyderPdb, self).run(cmd, globals, locals)
На самой вершине мы имеем класс SpyderCodeRunner
. У него есть тот самый магический метод debugfile
- отладка файла. Все уходит в общий метод _exec_code
: парсит текст в байт-код и выполняет его (напомню, что в шаблоне).
В самом низу видим строку super(SpyderPdb, self).run(cmd, globals, locals)
. Кому же передает управление SpyderPdb
? Bdb
- непосредственно отладчику Python. Таким образом, мы начинаем отладку.
Но вопрос еще без ответа - как мы все-таки вызываем step XXX, continue и т.д.? Сейчас мы просто делегируем это консоли IPython. Ответ прост - инфраструктура отладчика.
Bdb
- это базовый класс отладчика. Он содержит в себе, назовем это, кодовую часть. А вот Pdb
- это уже интерактивный отладчик. Он умеет обрабатывать команды пользователя, введенные им интерактивно. Но это не значит, что набор этих команд ограничен. Посмотрим на то, как обрабатываются команды пользователя.
У класса Cmd
(от которого наследуется Pdb
, но это не особо важно) есть метод cmdloop
- цикл обработки команд, которые читаются с помощью readline
. Затем эта команда парсится, причем:
Команда, начинающаяся с
!
интерпретируется оболочкой (например,!ls
).Имеется метод
default
, который должен возвращать обработчик по умолчанию для переданной команды. И главное - как же обработчики команд (next
,step
...) находятся? Также, с помощью инфраструктуры, но уже самого Python:getattr(self, 'do_' + cmd)
- таким образом мы получаем нужный нам обработчик, например,do_next
- обработчик step over (getattr
- это функция, возвращающая нам атрибут объекта по переданному названию, аcmd
- это название самой команды).
Если выполнить некоторые команды, то можно заметить, что отправляющиеся отладчиком команды в IPython
консоль имеют префикс !
, то есть должны отправляться в оболочку, но этого не происходит. Почему? Из-за SpyderPdb
- он переопределяет метод default
таким образом, что этот знак убирается. В результате, это то же самое, что и отправить next
самому Pdb
.
Но хватит слов, давайте посмотрим на код:
Обработка команд
Я немного обманул: cmdloop
- метод не самого Pdb
, а его базового класса Cmd
, но смысл прежний - обрабатываем пользовательский ввод в цикле.
class Cmd:
# https://github.com/python/cpython/blob/be8ae086874cac42eea104c1ff21ef5868d50bdd/Lib/cmd.py#L98
def cmdloop(self, intro=None):
stop = None
while not stop:
# ...
line = self.stdin.readline()
# ...
stop = self.onecmd(line)
# ...
# https://github.com/python/cpython/blob/be8ae086874cac42eea104c1ff21ef5868d50bdd/Lib/cmd.py#L200
def onecmd(self, line):
cmd, arg, line = self.parseline(line)
if not line:
return self.emptyline()
if cmd is None:
return self.default(line)
self.lastcmd = line
if line == 'EOF' :
self.lastcmd = ''
if cmd == '':
return self.default(line)
else:
try:
func = getattr(self, 'do_' + cmd)
except AttributeError:
return self.default(line)
return func(arg)
class SpyderPdb(ipyPdb):
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py#L134
def default(self, line):
if line[:1] == '!':
line = line[1:]
# ...
cmd, arg, line = self.parseline(line)
# ...
cmd_func = getattr(self, 'do_' + cmd, None)
# ...
return cmd_func(arg)
# ...
class Pdb(bdb.Bdb, cmd.Cmd):
# https://github.com/python/cpython/blob/be8ae086874cac42eea104c1ff21ef5868d50bdd/Lib/pdb.py#L1596
def do_next(self, arg):
self.set_next(self.curframe)
return 1
# https://github.com/python/cpython/blob/be8ae086874cac42eea104c1ff21ef5868d50bdd/Lib/pdb.py#L1582
def do_step(self, arg):
self.set_step()
return 1
# https://github.com/python/cpython/blob/be8ae086874cac42eea104c1ff21ef5868d50bdd/Lib/pdb.py#L1557
def do_until(self, arg):
if arg:
try:
lineno = int(arg)
except ValueError:
self.error('Error in argument: %r' % arg)
return
if lineno <= self.curframe.f_lineno:
self.error('"until" line number is smaller than current '
'line number')
return
else:
lineno = None
self.set_until(self.curframe, lineno)
return 1
За точки останова отвечает класс BreakpointsManager
. И обрабатываются они так же как и оговаривалось в секции про отладку Python - точки останова хранятся в поле самого Bdb
. Мы пишем туда же:
Выставление точек останова
class SpyderPdb(ipyPdb):
# https://github.com/spyder-ide/spyder/blob/49e81efdf9090d816cd11d7afc7cf2959273eeae/external-deps/spyder-kernels/spyder_kernels/customize/spyderpdb.py#L772
def set_spyder_breakpoints(self, breakpoints):
"""Set Spyder breakpoints."""
self.clear_all_breaks()
# -----Really deleting all breakpoints:
for bp in bdb.Breakpoint.bpbynumber:
if bp:
bp.deleteMe()
bdb.Breakpoint.next = 1
bdb.Breakpoint.bplist = {}
bdb.Breakpoint.bpbynumber = [None]
# -----
for fname, data in list(breakpoints.items()):
for linenumber, condition in data:
try:
self.set_break(self.canonic(fname), linenumber,
cond=condition)
except ValueError:
# Fixes spyder/issues/15546
# The file is not readable
pass
breakpoints = property(fset=set_spyder_breakpoints)
Отладка на ОС
Мы забрались слишком высоко - управляемые языки со своим рантаймом и IDE. Теперь пора вернуться опуститься и рассмотреть подробнее примеры каждого слоя. Начнем с операционных систем. Ранее мы рассмотрели только Linux - перейдем к Windows.
Windows
Отладка на Windows отличается как используемым API, так и инструментами (это ведь разные ОС).
Во-первых, API. На Linux мы использовали (швейцарский нож) ptrace
. Но в Windows пошли путем небольших функций делающих простые вещи. Для подключения функциональности отладки необходимо подключить соответствующий заголовок <debugapi.h>
. В нем заключается основная функциональность связанная с отладкой.
Цикл отладки
Если грубыми штрихами обрисовывать цикл отладки, то он похож на ранее увиденные:
Создаем отлаживаемый процесс:
CreateProcess(NAME, DEBUG_PROCESS)
.Заходим в вечный цикл ожидания очередного события:
WaitForDebugEvent(&event, TIMEOUT)
.Определяем, что за событие произошло:
switch (event.dwDebugEventCode)
.Продолжаем выполнение и идем на шаг 2:
ContinueDebugEvent(PID, TID, DBG_CONTINUE)
.
Теперь рассмотрим каждый шаг. В начале мы создаем отлаживаемый процесс. Заметьте, что это выполняется функцией CreateProcess
- в Linux мы отдельно создавали процесс и делали его отлаживаемым (PTRACE_TRACEME
). И причина в архитектуре.
На Linux все процессы создается дочерними и после fork
имеют какой-либо контроль над выполнением. Нам это время нужно для ptrace(PTRACE_TRACEME)
. Но в Windows все процессы равноправны и при запуске выполняется сразу готовый код (можно сказать сразу exec
выполняем). Теперь становится понятно, зачем флаг DEBUG_PROCESS
добавлен - это можно назвать шорткатом для fork + ptrace(PTRACE_TRACEME) + exec)
. Еще есть флаг DEBUG_ONLY_THIS_PROCESS
на случай, если запускаемый будет создавать новые процессы - указываем, если хотим отлаживать только непосредственно созданный нами.
Для подключения к уже запущенному процессу используется функция
DebugActiveProcess
На 2 шаге мы заходим в вечный цикл ожидания следующего события от отлаживаемого процесса. Это также похоже на Linux, только там мы не явно ожидали события для отладчика, а просто события дочернего процесса (с помощью waitpid
).
На этом шаге тоже есть разница с Linux. В Windows вызывать
WaitForDebugEvent
может только поток, который вызвалCreateProcess
. Но в Linux аналогичное требование отсутствует.
3 шаг - определение события. По моему мнению, в Windows это устроено проще: когда WaitForDebugEvent
завершается, то он заполняет структуру DEBUG_EVENT
. В ней довольно много информации, для получения которой не нужно сильно заморачиваться.
В начале мы также с помощью switch/case
определяем, что перед нами за событие. Их может быть 9: создание/завершение процесса/потока, загрузка/выгрузка DLL, OutputDebugString (дочерний процесс явно хочет передать отладчику какую-то строку), RIP_EVENT
(системная ошибка) и EXCEPTION_DEBUG_EVENT
(возникло исключение). Для каждого события есть своя структура, которая и заполняется. А хранится она в объединении (union
).
Когда мы обработали событие, отлаживаемый процесс необходимо продолжить. Это делается с помощью ContinueDebugEvent
. Кажется это также на Linux - ptrace(PTRACE_CONT)
, но не тут то было. Заметьте последний аргумент этой функции - DBG_CONTINUE
. Этот последний аргумент имеет смысл только если событие было типа EXCEPTION_DEBUG_EVENT
. Этот флаг принимает 2 значения: DBG_CONTINUE
и DBG_EXCEPTION_NOT_HANDLED
. Разница между ними проста - когда отладчик получает исключение от отлаживаемого, то это может быть как фатальная ошибка, либо как сигнал для отладчика (спойлер, точки останова). Поэтому, когда мы продолжаем выполнение этот флаг может сказать ОС что исключение обработано (DBG_CONTINUE
), иначе обработка уже произойдет на стороне отлаживаемого процесса (DBG_EXCEPTION_NOT_HANDLED
). Это чуть-чуть похоже на Linux с точки зрения точки останова - если отладчика нет, то SIGTRAP
приведет к core-dump'у.
Есть еще 3-ий флаг -
DBG_REPLY_LATER
. На сколько я понял, если указать этот флаг, то при возобновлении работы указанного потока это событие вернется, чтобы обработать заново.
Собирая все вместе, цикл работы отладчика можно представить следующим образом:
Цикл работы отладчика
int main(int argc, const char **argv) {
DEBUG_EVENT event;
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
GetStartupInfo(&si);
CreateProcess(NULL,
L"dir", // Запускаем 'dir' в качестве примера
NULL, NULL, FALSE,
DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS, // Отлаживаем только этот процесс
NULL, NULL,
&si, &pi);
child_pid = pi.dwProcessId;
child_tid = pi.dwThreadId;
child_pHandle = pi.hProcess;
child_tHandle = pi.hThread;
while (TRUE) {
BOOL should_stop = FALSE;
DWORD continue_flag = DBG_EXCEPTION_NOT_HANDLED;
if (!WaitForDebugEvent(&event, INFINITE)) {
break;
}
switch (event.dwDebugEventCode) {
case EXCEPTION_DEBUG_EVENT:
// В процессе случилось исключение.
// Вызывается также и тогда, когда достигнута инструкция остановки (int3)
EXCEPTION_DEBUG_INFO *edi = &event.u.Exception;
break;
case CREATE_PROCESS_DEBUG_EVENT:
// Создан новый процесс
CREATE_PROCESS_DEBUG_INFO *cpdi = &event.u.CreateProcessInfo;
break;
case EXIT_PROCESS_DEBUG_EVENT:
// Процесс завершается
should_stop = TRUE;
EXIT_PROCESS_DEBUG_INFO *epdi = &event.u.ExitProcess;
break;
case OUTPUT_DEBUG_STRING_EVENT:
// Отлаживаемый процесс вызывает DebugOutputString - посылает строку отладчику
OUTPUT_DEBUG_STRING_INFO *odsi = &event.u.DebugString;
break;
case CREATE_THREAD_DEBUG_EVENT:
// Создан новый поток
CREATE_THREAD_DEBUG_INFO *ctdi = &event.u.CreateThread;
break;
case EXIT_THREAD_DEBUG_EVENT:
// Поток завершается
EXIT_THREAD_DEBUG_INFO *etdi = &event.u.ExitThread;
break;
case LOAD_DLL_DEBUG_EVENT:
// Загружается DLL
LOAD_DLL_DEBUG_INFO *lddi = &event.u.LoadDll;
break;
case UNLOAD_DLL_DEBUG_EVENT:
// DDL выгружается
UNLOAD_DLL_DEBUG_INFO *uddi = &event.u.UnloadDll;
break;
case RIP_EVENT:
// Системная ошибка
RIP_INFO *ri = &event.u.RipInfo;
should_stop = TRUE;
break;
}
if (should_stop) {
break;
}
// Продолжаем выполнение
ContinueDebugEvent(event.dwProcessId, event.dwThreadId, continue_flag);
}
CloseHandle(child_pHandle);
CloseHandle(child_tHandle);
return 0;
}
Но это мой код. Давайте посмотрим на цикл отладки в lldb:
DebuggerThread::DebugLoop
Когда писал код выше, то в lldb не смотрел. На удивление, этот кусок сильно похож на мой.
// https://github.com/llvm/llvm-project/blob/ab51eccf88f5321e7c60591c5546b254b6afab99/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp#L232
void DebuggerThread::DebugLoop() {
DEBUG_EVENT dbe = {};
bool should_debug = true;
while (should_debug) {
BOOL wait_result = WaitForDebugEvent(&dbe, INFINITE);
if (wait_result) {
DWORD continue_status = DBG_CONTINUE;
bool shutting_down = m_is_shutting_down;
switch (dbe.dwDebugEventCode) {
default:
llvm_unreachable("Unhandle debug event code!");
case EXCEPTION_DEBUG_EVENT: {
ExceptionResult status = HandleExceptionEvent(
dbe.u.Exception, dbe.dwThreadId, shutting_down);
if (status == ExceptionResult::MaskException)
continue_status = DBG_CONTINUE;
else if (status == ExceptionResult::SendToApplication)
continue_status = DBG_EXCEPTION_NOT_HANDLED;
break;
}
case CREATE_THREAD_DEBUG_EVENT:
continue_status =
HandleCreateThreadEvent(dbe.u.CreateThread, dbe.dwThreadId);
break;
case CREATE_PROCESS_DEBUG_EVENT:
continue_status =
HandleCreateProcessEvent(dbe.u.CreateProcessInfo, dbe.dwThreadId);
break;
case EXIT_THREAD_DEBUG_EVENT:
continue_status =
HandleExitThreadEvent(dbe.u.ExitThread, dbe.dwThreadId);
break;
case EXIT_PROCESS_DEBUG_EVENT:
continue_status =
HandleExitProcessEvent(dbe.u.ExitProcess, dbe.dwThreadId);
should_debug = false;
break;
case LOAD_DLL_DEBUG_EVENT:
continue_status = HandleLoadDllEvent(dbe.u.LoadDll, dbe.dwThreadId);
break;
case UNLOAD_DLL_DEBUG_EVENT:
continue_status = HandleUnloadDllEvent(dbe.u.UnloadDll, dbe.dwThreadId);
break;
case OUTPUT_DEBUG_STRING_EVENT:
continue_status = HandleODSEvent(dbe.u.DebugString, dbe.dwThreadId);
break;
case RIP_EVENT:
continue_status = HandleRipEvent(dbe.u.RipInfo, dbe.dwThreadId);
if (dbe.u.RipInfo.dwType == SLE_ERROR)
should_debug = false;
break;
}
::ContinueDebugEvent(dbe.dwProcessId, dbe.dwThreadId, continue_status);
// We have to DebugActiveProcessStop after ContinueDebugEvent, otherwise
// the target process will crash
if (shutting_down) {
// A breakpoint that occurs while `m_pid_to_detach` is non-zero is a
// magic exception that we use simply to wake up the DebuggerThread so
// that we can close out the debug loop.
if (m_pid_to_detach != 0 &&
(dbe.u.Exception.ExceptionRecord.ExceptionCode ==
EXCEPTION_BREAKPOINT ||
dbe.u.Exception.ExceptionRecord.ExceptionCode ==
STATUS_WX86_BREAKPOINT)) {
// detaching with leaving breakpoint exception event on the queue may
// cause target process to crash so process events as possible since
// target threads are running at this time, there is possibility to
// have some breakpoint exception between last WaitForDebugEvent and
// DebugActiveProcessStop but ignore for now.
while (WaitForDebugEvent(&dbe, 0)) {
continue_status = DBG_CONTINUE;
if (dbe.dwDebugEventCode == EXCEPTION_DEBUG_EVENT &&
!(dbe.u.Exception.ExceptionRecord.ExceptionCode ==
EXCEPTION_BREAKPOINT ||
dbe.u.Exception.ExceptionRecord.ExceptionCode ==
STATUS_WX86_BREAKPOINT ||
dbe.u.Exception.ExceptionRecord.ExceptionCode ==
EXCEPTION_SINGLE_STEP))
continue_status = DBG_EXCEPTION_NOT_HANDLED;
::ContinueDebugEvent(dbe.dwProcessId, dbe.dwThreadId,
continue_status);
}
::DebugActiveProcessStop(m_pid_to_detach);
m_detached = true;
}
}
if (m_detached) {
should_debug = false;
}
} else {
should_debug = false;
}
}
FreeProcessHandles();
::SetEvent(m_debugging_ended_event);
}
Основная работа отладчика заключается в обработке этих событий и выполнении соответствующих действий при остановке (выставление точек останова, трейсинг и т.д.). Начнем с базы - точки останова.
Точки останова
Логика их обработки довольно проста. Для начала - как их обнаруживаем. Я уже заспойлерил, что за точки останова отвечает событие EXCEPTION_DEBUG_EVENT
. Но вообще, это событие, как можно догадаться из названия, отвечает за исключения. Они могут быть разными, но определены. Чтобы понять какое исключение перед нами, необходимо использовать поле ExceptionCode
у структуры его события.
Я насчитал 23 кода ошибки. Например, EXCEPTION_INT_DIVIDE_BY_ZERO
- ошибка деления на 0. Но в рамках отладки нас будут интересовать 2 исключения:
EXCEPTION_BREAKPOINT
- точка остановаEXCEPTION_SINGLE_STEP
- шаг инструкции
SEH
Эти исключения не относятся конкретно к отладчику, а к механизму SEH (Structured Exception Handling) в Windows. В __try/__except
блоке можно вызвать функцию GetExceptionCode
и получить код возникшей ошибки.
Он позволяет обрабатывать различные исключения - как hardware, так и software. Для их обработки он определяет 3* ключевых слова:
__try
-try
__except
-catch
/except
__finally
-finally
Тут стоит рассказать о концепции first-chance exception. Если коротко, то при возникновении исключения отладчику могут дать возможность первому попытаться обработать исключение (дают ему первый шанс). Но как понять, что исключение обработано? Тут в игру вступают флаги, передаваемые ContinueDebugEvent
: DBG_CONTINUE
- исключение обработано, DBG_EXCEPTION_NOT_HANDLED
- НЕ обработано. В первом случае, этого исключения как будто и не было, а во втором запустится стандартный поток обработки исключения в самом процессе.
Последний шаг: EXCEPTION_BREAKPOINT
И EXCEPTION_SINGLE_STEP
- это first-chance исключения. Мы в принципе сами может их обработать (пример из официальной документации Microsoft):
/* https://learn.microsoft.com/ru-ru/windows/win32/debug/using-an-exception-handler#example-2 */
BOOL CheckForDebugger()
{
__try
{
DebugBreak();
}
__except(GetExceptionCode() == EXCEPTION_BREAKPOINT ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
// No debugger is attached, so return FALSE
// and continue.
return FALSE;
}
return TRUE;
}
Я сказал 3 ключевых слова под звездочкой, так как согласно той же документации функцию
GetExceptionCode
компилятор Microsoft обрабатывает его как ключевое слово и выдает ошибку, если использовать вне__try/__except
блоков.
Логика выставления/удаления точек останова такая же как и в Linux, то есть ОС предоставляет нам возможность копаться в адресном пространстве отлаживаемого процесса, но что мы туда запишем - нам на откуп, мы сами должны знать инструкцию точки останова.
Для работы с адресным пространством Windows предоставляет 2 функции: ReadProcessMemory
и WriteProcessMemory
- чтение и запись соответственно. Это простые функции, которые читают непрерывный участок памяти (не векторизованное чтение).
Тут тоже можно заметить разницу
ptrace(PTRACE_PEEKDATA)
позволяет читать с гранулярностью в 1 слово (8/4 байт в зависимости от битности).ReadProcessMemory
иWriteProcessMemory
- с гранулярностью 1 байт. Но чтение из файла памяти илиprocess_vm_readv
/process_vm_writev
, которые рассмотрели в секции gdb, - в Linux уже позволяют по байту читать/писать.
Мы уже знаем как выставляются точки останова: запоминаем, что было по адресу и записываем туда 0xCC
:
Выставление точки останова
static BYTE set_breakpoint(LPVOID pAddress) {
BYTE saved;
SIZE_T numberOfBytes;
ReadProcessMemory(child_pHandle, pAddress, (LPVOID)&saved, sizeof(saved), &numberOfBytes);
BYTE br = 0xCC;
WriteProcessMemory(child_pHandle, pAddress, &br, sizeof(BYTE), &numberOfBytes);
FlushInstructionCache(child_pHandle, pAddress, sizeof(BYTE));
return saved;
}
Единственное замечание: использование функции FlushInstructionCache
. Как можно догадаться, она используется для того, чтобы сбросить сделанные нами изменения в инструкциях и CPU их подхватил.
Теперь посмотрим, как это реализуется в lldb. Он кроссплатформенный и его платформно-зависимые части реализованы в виде подклассов других абстрактных классов. И оно понятно: у разных платформ разные системные вызовы.
Логика выставления точки останова находится в методе NativeProcessProtocol::EnableSoftwareBreakpoint
.
EnableSoftwareBreakpoint
/* https://github.com/llvm/llvm-project/blob/ab51eccf88f5321e7c60591c5546b254b6afab99/lldb/source/Host/common/NativeProcessProtocol.cpp#L422 */
llvm::Expected<NativeProcessProtocol::SoftwareBreakpoint>
NativeProcessProtocol::EnableSoftwareBreakpoint(lldb::addr_t addr,
uint32_t size_hint) {
auto expected_trap = GetSoftwareBreakpointTrapOpcode(size_hint);
if (!expected_trap)
return expected_trap.takeError();
llvm::SmallVector<uint8_t, 4> saved_opcode_bytes(expected_trap->size(), 0);
// Save the original opcodes by reading them so we can restore later.
size_t bytes_read = 0;
Status error = ReadMemory(addr, saved_opcode_bytes.data(),
saved_opcode_bytes.size(), bytes_read);
if (error.Fail())
return error.ToError();
// Ensure we read as many bytes as we expected.
if (bytes_read != saved_opcode_bytes.size()) {
return llvm::createStringError(
llvm::inconvertibleErrorCode(),
"Failed to read memory while attempting to set breakpoint: attempted "
"to read {0} bytes but only read {1}.",
saved_opcode_bytes.size(), bytes_read);
}
// Write a software breakpoint in place of the original opcode.
size_t bytes_written = 0;
error = WriteMemory(addr, expected_trap->data(), expected_trap->size(),
bytes_written);
if (error.Fail())
return error.ToError();
// Ensure we wrote as many bytes as we expected.
if (bytes_written != expected_trap->size()) {
return llvm::createStringError(
llvm::inconvertibleErrorCode(),
"Failed write memory while attempting to set "
"breakpoint: attempted to write {0} bytes but only wrote {1}",
expected_trap->size(), bytes_written);
}
llvm::SmallVector<uint8_t, 4> verify_bp_opcode_bytes(expected_trap->size(),
0);
size_t verify_bytes_read = 0;
error = ReadMemory(addr, verify_bp_opcode_bytes.data(),
verify_bp_opcode_bytes.size(), verify_bytes_read);
if (error.Fail())
return error.ToError();
// Ensure we read as many verification bytes as we expected.
if (verify_bytes_read != verify_bp_opcode_bytes.size()) {
return llvm::createStringError(
llvm::inconvertibleErrorCode(),
"Failed to read memory while "
"attempting to verify breakpoint: attempted to read {0} bytes "
"but only read {1}",
verify_bp_opcode_bytes.size(), verify_bytes_read);
}
if (llvm::ArrayRef(verify_bp_opcode_bytes.data(), verify_bytes_read) !=
*expected_trap) {
return llvm::createStringError(
llvm::inconvertibleErrorCode(),
"Verification of software breakpoint "
"writing failed - trap opcodes not successfully read back "
"after writing when setting breakpoint at {0:x}",
addr);
}
return SoftwareBreakpoint{1, saved_opcode_bytes, *expected_trap};
}
Можно выделить 3 платформно-зависимых места (функции): GetSoftwareBreakpointTrapOpcode
, ReadMemory
и WriteMemory
. Первый - получение инструкции точки останова. Она зависит больше от CPU, чем от ОС, поэтому опустим и сконцентрируемся на 2 последних.
Реализация чтения/записи для Windows находится в классе ProcessDebugger
методах ReadMemory
/WriteMemory
. Внутри это просто обертка над ReadProcessMemory
. Но дополнительно мы делаем повторную попытку чтения, если произошла ошибка чтения из-за того, что указанный диапазон слишком большой (превышает допустимый), то пытаемся прочитать сколько сможем:
ReadMemory
/* https://github.com/llvm/llvm-project/blob/ab51eccf88f5321e7c60591c5546b254b6afab99/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp#L262 */
Status ProcessDebugger::ReadMemory(lldb::addr_t vm_addr, void *buf, size_t size,
size_t &bytes_read) {
Status error;
bytes_read = 0;
lldb::process_t handle = m_session_data->m_debugger->GetProcess()
.GetNativeProcess()
.GetSystemHandle();
void *addr = reinterpret_cast<void *>(vm_addr);
SIZE_T num_of_bytes_read = 0;
if (::ReadProcessMemory(handle, addr, buf, size, &num_of_bytes_read)) {
bytes_read = num_of_bytes_read;
return Status();
}
error = Status(GetLastError(), eErrorTypeWin32);
MemoryRegionInfo info;
if (GetMemoryRegionInfo(vm_addr, info).Fail() ||
info.GetMapped() != MemoryRegionInfo::OptionalBool::eYes)
return error;
size = info.GetRange().GetRangeEnd() - vm_addr;
if (::ReadProcessMemory(handle, addr, buf, size, &num_of_bytes_read)) {
bytes_read = num_of_bytes_read;
return Status();
}
error = Status(GetLastError(), eErrorTypeWin32);
return error;
}
Запись выглядит так же как и описали ранее. WriteProcessMemory
+ FlushInstructionCache
:
WriteMemory
/* https://github.com/llvm/llvm-project/blob/ab51eccf88f5321e7c60591c5546b254b6afab99/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp#L292 */
Status ProcessDebugger::WriteMemory(lldb::addr_t vm_addr, const void *buf,
size_t size, size_t &bytes_written) {
/* ... */
HostProcess process = m_session_data->m_debugger->GetProcess();
void *addr = reinterpret_cast<void *>(vm_addr);
SIZE_T num_of_bytes_written = 0;
lldb::process_t handle = process.GetNativeProcess().GetSystemHandle();
if (::WriteProcessMemory(handle, addr, buf, size, &num_of_bytes_written)) {
FlushInstructionCache(handle, addr, num_of_bytes_written);
bytes_written = num_of_bytes_written;
} else {
error = Status(GetLastError(), eErrorTypeWin32);
}
return error;
}
hardware vs software
Вы уже могли заметить сочетание SoftwareBreakpoint
. Можно выделить 2 типа точек останова: software и hardware. Если говорить грубо, то:
software
- это инструкции точки остановаhardware
- это CPU исключения
До этого момента мы использовали только software
точки останова - ставили инструкцию 0xCC
. Они доступны практически для любого CPU так как не требуют особых условий.
hardware
с другой стороны, как можно догадаться, используют возможности CPU. С одной стороны, эти точки останова сильно зависят от железа (CPU) и их количество сильно ограничено. С другой, они позволяют то, чего software
нет, в частности, точка останова при изменении данных по определенному адресу.
О hardware/software мы поговорим позже.
Но это точки останова. Иногда необходимо выполнить только 1 инструкцию. В Windows это реализуется не с помощью отдельной функции, а с помощью выставления TF
(Trap Flag) флага в регистре RFLAGS
(для x86). Этот флаг 9-ый, то есть его шестнадцатеричное представление - 0x100
.
Когда этот шаг сделан, то возникает то же исключение EXCEPTION_DEBUG_BREAK
, но код исключения будет уже EXCEPTION_SINGLE_STEP
.
Выставление TF
static void make_single_step() {
/* Выставление режима Single step */
CONTEXT ctx;
GetThreadContext(child_hThread, &ctx);
ctx.EFlags |= 0x100;
SetThreadContext(child_hThread, &ctx);
/* Продолжение работы */
ContinueDebugEvent(child_hProcess, child_hThread, DBG_CONTINUE);
DEBUG_EVENT event;
WaitForDebugEvent(&event, INFINITE);
if (event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT &&
event.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_SINGLE_STEP) {
/* Single step */
}
}
Поле называется EFLAGS
. Это название из 32-битного мира, для 64-битных используется префикс R
.
Если мы посмотрим на реализацию lldb, то увидим, что они также выставляют этот флаг:
TargetThreadWindows::DoResume
/* https://github.com/llvm/llvm-project/blob/ab51eccf88f5321e7c60591c5546b254b6afab99/lldb/source/Plugins/Process/Windows/Common/TargetThreadWindows.cpp#L126 */
Status TargetThreadWindows::DoResume() {
StateType resume_state = GetTemporaryResumeState();
StateType current_state = GetState();
if (resume_state == current_state)
return Status();
if (resume_state == eStateStepping) {
Log *log = GetLog(LLDBLog::Thread);
uint32_t flags_index =
GetRegisterContext()->ConvertRegisterKindToRegisterNumber(
eRegisterKindGeneric, LLDB_REGNUM_GENERIC_FLAGS);
uint64_t flags_value =
GetRegisterContext()->ReadRegisterAsUnsigned(flags_index, 0);
ProcessSP process = GetProcess();
const ArchSpec &arch = process->GetTarget().GetArchitecture();
switch (arch.GetMachine()) {
/*
*
* Выставляем флаг TF
*
*/
case llvm::Triple::x86:
case llvm::Triple::x86_64:
flags_value |= 0x100; // Set the trap flag on the CPU
break;
case llvm::Triple::aarch64:
case llvm::Triple::arm:
case llvm::Triple::thumb:
flags_value |= 0x200000; // The SS bit in PState
break;
default:
LLDB_LOG(log, "single stepping unsupported on this architecture");
break;
}
GetRegisterContext()->WriteRegisterFromUnsigned(flags_index, flags_value);
}
if (resume_state == eStateStepping || resume_state == eStateRunning) {
DWORD previous_suspend_count = 0;
HANDLE thread_handle = m_host_thread.GetNativeThread().GetSystemHandle();
do {
// ResumeThread returns -1 on error, or the thread's *previous* suspend
// count on success. This means that the return value is 1 when the thread
// was restarted. Note that DWORD is an unsigned int, so we need to
// explicitly compare with -1.
previous_suspend_count = ::ResumeThread(thread_handle);
if (previous_suspend_count == (DWORD)-1)
return Status(::GetLastError(), eErrorTypeWin32);
} while (previous_suspend_count > 1);
}
return Status();
}
Отладочная информация
Теперь переходим к части поинтереснее - отладочные символы. Подход Windows - хранить отладочные символы CodeView в отдельном файле .pdb
.
Для работы с отладочными символами необходимо подключить уже другой заголовок - <DbgHelp.h>
. В нем определено множество вспомогательных функций для работы с символами отладки.
Перед началом работы с символами необходимо инициализировать состояние. Это делается с помощью функции SymInitialize
. Эта библиотека часто на вход принимает дескрипторы процесса, потока и даже файла. Но откуда их взять?
Кажется это сложно, но в Windows поняли, что возможно чаще всего библиотеку будут инициализировать в самом начале. Поэтому они сделали такое решение: когда процесс создается, то генерируется событие CREATE_PROCESS_DEBUG_EVENT
и в его структуре данных события уже хранятся нужные нам дескрипторы.
Когда библиотека инициализирована нужно ее настроить. Для этого используется функция SymSetOptions
. На вход ей передается маска флагов состояния. Сейчас я передаю 1 флаг - SYMOPT_LOAD_LINES
, загрузить информацию о строках исходного кода
Последним шагом, скармливаем ей наш модуль, из которого она должна прочитать символы. Это делается с помощью функции SymLoadModule
.
Собирая все вместе получаем нечто подобное.
Инициализация при запуске
int main(int argc, const char **argv) {
/* ... */
while (TRUE) {
DEBUG_EVENT event;
BOOL should_stop = FALSE;
DWORD continue_flag = DBG_EXCEPTION_NOT_HANDLED;
if (!WaitForDebugEvent(&event, INFINITE)) {
break;
}
switch (event.dwDebugEventCode) {
case CREATE_PROCESS_DEBUG_EVENT:
if (SymInitialize(event.u.CreateProcessInfo.hProcess, NULL, FALSE)) {
SymSetOptions(SYMOPT_LOAD_LINES);
DWORD64 base =
SymLoadModule(event.u.CreateProcessInfo.hProcess,
event.u.CreateProcessInfo.hFile,
event.u.CreateProcessInfo.lpImageName,
NULL,
(DWORD64)event.u.CreateProcessInfo.lpStartAddress,
0);
}
break;
/* ... */
}
}
return 0;
}
Как можете заметить, вся необходимая информация уже находится с данных события, что очень удобно. Можете заметить, что SymLoadModule
возвращает какой-то base
. Это базовый адрес модуля, но я его интерпретирую как дескриптор/идентификатор, так как его нужно передавать везде.
Далее можем приступать к работе. Для начала, рассмотрим как получается информация о строках исходного кода. И на удивление просто: нам нужно только 2 функции: SymEnumSourceFiles
и SymEnumLines
. Обе устроены одинаково: передаем идентификатор цели (процесс, дескриптор, файл...), контекст (значение, которое будет передаваться) и колбэк, который будет вызван для каждого элемента множества. Под множеством, как можно догадаться из названий функций, имеется ввиду множество файлов исходного кода и строк внутри них соответственно.
Все это можно описать так:
SymEnumSourceFiles
Выводим файл и номера строк внутри него.
static BOOL CALLBACK process_single_source_file(PSRCCODEINFO line, PVOID context) {
/* Файл:Строка (Адрес инструкции) */
printf("%s: %d (%llx)\n", line->FileName, line->LineNumber, line->Address);
return TRUE;
}
static BOOL CALLBACK process_source_files_info(PSOURCEFILE file, PVOID context) {
DWORD64 base = (DWORD64)context;
if (!SymEnumLines(child_hProcess, base, NULL, NULL, process_single_source_file, context)) {
print_error("SymEnumLines");
}
printf("\n");
return TRUE;
}
static void show_source_file_info(DWORD64 base) {
if (!SymEnumSourceFiles(child_hProcess, base, "*.c", process_source_files_info, (PVOID)base)) {
print_error("SymEnumSourceFiles");
}
}
Можете заметить, что колбэк для SymEnumLines
получает структуру SRCCODEINFO
и она довольно полезная, потому что хранит в себе не только номер строки и файл, но еще и адрес этой инструкции (line->Address
).
Выводиться будут все файлы, участвовавшие в компиляции конкретного файла. Даже заголовки (или как в моем случае, неизвестные
.cpp
файлы), поэтому стоит проверить название файла в колбэке.
Теперь, перейдем к более интересной теме - типам. Для них также есть отдельная функция, которая принимает колбэк - SymEnumTypes
. Передаваемый колбэк получает на вход указатель на структуру SYMBOL_INFO
. В ней хранится информация связанная с символом.
Также, как как Windows сильно связан с PDB, то в этой структуре есть поле Tag
. Она хранит тэг типа из PDB файла. С ее помощью (если PDB доступен) мы можем получить тип символа.
Но было бы все так просто. Я сказал, что в SYMBOL_INFO
хранится информация о символе, но символы бывают разные и, соответственно, данные могут быть разные. В этой структуре хранятся все необходимые данные для начала работы, а все остальное надо получить самим.
Получать дополнительную информацию о символе мы можем с помощью функции SymGetTypeInfo
. Для ее использования мы передаем значение из перечисления IMAGEHLP_SYMBOL_TYPE_INFO
(команду) и адрес, в который будет записываться результат (тип зависит от команды). Всего есть 39 команд, но все описывать не буду.
Просто покажу пример использования. Мы будем получать все структуры (классы) и выводить все их поля вместе с типами. Для простоты, я буду выводить только int
, char
и float
.
Определение типа структур
static BOOL CALLBACK process_type_info(PSYMBOL_INFO symbol, ULONG symbol_size, PVOID context) {
/* 1 */
if (symbol->Tag != SymTagUDT) {
return TRUE;
}
/* 1.5 */
DWORD kind;
if (!SymGetTypeInfo(child_hProcess, symbol->ModBase, symbol->TypeIndex, TI_GET_UDTKIND, &kind)) {
return FALSE;
}
if (kind != UdtStruct && kind != UdtClass) {
return TRUE;
}
/* 2 */
DWORD childrenCount;
if (!SymGetTypeInfo(child_hProcess, symbol->ModBase, symbol->TypeIndex, TI_GET_CHILDRENCOUNT, &childrenCount)) {
return FALSE;
}
int findChildrenSize = sizeof(TI_FINDCHILDREN_PARAMS) + childrenCount * sizeof(ULONG);
TI_FINDCHILDREN_PARAMS *children = malloc(findChildrenSize);
memset(children, 0, findChildrenSize);
children->Count = childrenCount;
if (!SymGetTypeInfo(child_hProcess, symbol->ModBase, symbol->TypeIndex, TI_FINDCHILDREN, children)) {
return FALSE;
}
/* 3 */
printf("%s %s\n", kind == UdtClass ? "class" : "struct", symbol->Name);
for (DWORD i = 0; i < childrenCount; i++) {
/* 4 */
DWORD childTag;
if (!SymGetTypeInfo(child_hProcess, symbol->ModBase, children->ChildId[i], TI_GET_SYMTAG, &childTag)) {
return FALSE;
}
if (childTag != SymTagData) {
continue;
}
WCHAR *memberName;
if (!SymGetTypeInfo(child_hProcess, symbol->ModBase, children->ChildId[i], TI_GET_SYMNAME, &memberName)) {
return FALSE;
}
/* 5 */
DWORD typeId;
if (!SymGetTypeInfo(child_hProcess, symbol->ModBase, children->ChildId[i], TI_GET_TYPEID, &typeId)) {
return FALSE;
}
DWORD memberTypeTag;
if (!SymGetTypeInfo(child_hProcess, symbol->ModBase, typeId, TI_GET_SYMTAG, &memberTypeTag)) {
return FALSE;
}
/* 6 */
char *typeName;
if (memberTypeTag == SymTagBaseType) {
DWORD baseType;
if (!SymGetTypeInfo(child_hProcess, symbol->ModBase, typeId, TI_GET_BASETYPE, &baseType)) {
return FALSE;
}
typeName = "unknown";
switch (baseType) {
case btInt:
typeName = "int";
break;
case btChar:
typeName = "char";
break;
case btFloat:
typeName = "float";
break;
}
} else {
typeName = "*complex*";
}
printf("\t%s %ls\n", typeName, memberName);
}
printf("\n");
return TRUE;
}
static void show_types_info(DWORD64 base, ULONG64 baseOfImage) {
if (!SymEnumTypes(child_hProcess, baseOfImage, process_type_info, (PVOID)base)) {
print_error("SymEnumTypes");
}
}
Теперь, давайте разберем, что здесь написано. Для удобства я добавил комментарии по частям. Вначале (1) мы проверяем, что переданный символ - это пользовательский тип (SymTagUDT
). Тэг хранится в поле Tag
, но также мы можем получить тэг сами (1.5).
После (2) мы получаем информацию о дочерних членах этого символа. В этой части необходимо вначале получить их общее количество, а затем аллоцировать необходимое место для структуры. Делается это с помощью 2 команд.
В результате мы получим массив из TypeIndex
дочерних членов (т.е. просто индексы, не структура SYMBOL_INFO
).
Далее (3) начинаем итерироваться по всем членам и обрабатывать каждый. Сперва (4), проверяем, что этот член - поле (SymTagData
). Дело в том, что дочерними могут быть функции или базовые классы (я тестировал логику на C, поэтому такую проверку я мог и не делать).
Теперь (4), получаем информацию о типе этого поля. Делается это за 3 шага:
получаем индекс типа поля
получаем тэг этого тип и
получаем название этого типа.
Шаг 1 наверное понятен, поэтому рассмотрим остальные. Здесь типы хранятся примерно так же как и в DWARF, то есть для указателя, структуры пользовательской, базового (встроенного) типа, typedef
'ов и т.д. есть свои тэги и обрабатывать их необходимо также в зависимости от тэга. Для этого мы его и получили. Я вывожу название только для базовых типов, поэтому на шаге 3 получаю индекс базового типа (Base Type). Они все определены и значения хранятся в перечислении BasicType
(префикс bt
у его значений).
Вроде просто, но не совсем. Кто хочет использовать DbgHelp
, то вам несколько советов:
Многие перечисления и другая информация о PDB файле (то же самое перечисление
SymTagEnum
) по умолчанию заголовкомDbgHelp.h
не отдается. Для этого необходимо перед его включением объявить макрос_NO_CVCONST_H
. Это тогда, когда у вас нет заголовочного файлаcvconst.h
. Его можно просто скачать с репозитория Microsoft на GitHub (что я и сделал)У каждого тэга есть свои определенные команды (
TI_GET_XXX
), которые он поддерживает. Если нет, то возникнет ошибкаIncorrect function
. Я искал, но не нашел список поддерживаемых команд (скорее всего они в документации PDB). Зато нашел проектTypeInfoDump
, в котором есть примеры использования этих команд для различных тэгов. Во время написания кода для меня он стал документацией.
Стек вызовов
Честно говоря, эту часть я писать изначально не планировал, но в любой статье посвященной отладчику на Windows она есть, поэтому исключением не стану. Напомню, что стек вызовов - это пройтись по всем фреймам, начиная с текущего, и вывести информацию о нем.
Для развертки стека в Windows имеется отдельная функция StackWalk
. Это рабочая лошадка всего - мы просто вызываем ее в цикле, пока не наткнемся на адрес возврата 0
. Это означает, что мы дошли до конца.
Эта функция работает со структурой STACKFRAME
. В самом начале мы заполняем ее поля (обычно текущим фреймом с помощью GetThreadContext
), а далее заходим в цикл и считываем данные очередного фрейма.
Для примера я вывожу файл и номер строки, на которой этот фрейм находится в данный момент. Делаю это с помощью DbgHelp
.
Собирая все вместе мы получаем:
StackWalk
static void dump_callstack() {
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
if (!GetThreadContext(child_hThread, &ctx)) {
print_error("GetThreadContext");
return;
}
STACKFRAME stack = {0};
stack.AddrPC.Offset = ctx.Rip;
stack.AddrPC.Mode = AddrModeFlat;
stack.AddrFrame.Offset = ctx.Rbp;
stack.AddrFrame.Mode = AddrModeFlat;
stack.AddrStack.Offset = ctx.Rsp;
stack.AddrStack.Mode = AddrModeFlat;
do
{
if (!StackWalk(IMAGE_FILE_MACHINE_AMD64, child_hProcess, child_hThread, &stack, &ctx,
NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) {
print_error("StackWalk");
return;
}
DWORD displacement;
IMAGEHLP_LINE64 line;
DWORD64 addr = stack.AddrPC.Offset;
if (SymGetLineFromAddr(child_hProcess, addr, &displacement, &line)) {
/* Файл:Строка */
printf("%s:%d\n", line.FileName, line.LineNumber);
}
if (GetLastError() == ERROR_MOD_NOT_FOUND) {
continue;
}
} while (stack.AddrReturn.Offset != 0);
}
Но и тут не без замечаний. Для доступа к функции SymGetLineFromAddr
необходима библиотека DbgHelp
. И здесь стоит вернуться к моменту ее инициализации. Изначально я передавал еще 1 флаг - SYMOPT_DEFERRED_LOADS
, для ленивой загрузки информации, но когда я начал тестировать код, то всегда возникала ошибка The specified module could not be found
. Она исчезла после удаления этого флага. Причину я не искал.
Но все же эта ошибка говорит о том, что указанный модуль найти не удалось и это вполне нормальная (если так можно сказать) ситуация. Например, если вверх по стеку находится не наш код и доступа к отладочным символам у нас нет. Для тестирования я вызывал функцию DebugBreak
(генерирует исключение для отладки) - она была первой в стеке и для нее не было отладочной информации, поэтому для нее возникала эта ошибка.
KolibriOS
Я думал, какую бы еще ОС рассмотреть. Но рынок делится между UNIX-подобными ОС и Windows. Linux рассмотрели, Window тоже, а остальные похожи на Linux. Поэтому принял решение рассмотреть что-нибудь необычное. Остановился на KolibriOS.
Для начала введение - что это такое. KolibriOS это форк (2004 год) другой ОС MenuetOS. Ее ядро написано полностью на ассемблере FASM, но прикладные программы (браузер, редакторы, плеер и т.д.), которые поставляются вместе, уже на C.
Часто говорят, что она помещается на 1 дискете. Это правда, но частично - это подготовленный образ для дискеты. Загрузочный образ для диска (.iso) весит уже чуть более 100 Мб. Чтобы добиться такого, флоппи образ был сильно урез в некоторых частях, но даже с учетом этого при первой загрузке я разницы в UI не заметил: то же окно и те же программы (даже игры).
Теперь перейдем к основной теме - отладке.
Как могли заметить, чтобы программу, выполняющуюся на платформе X, можно было отладить, необходимо, чтобы эта платформа X предоставляла нужные инструменты. В случае ОС это системные вызовы. KolibriOS их тоже имеет - 80 штук, причем у некоторых системных вызовов есть подкатегории. В KolibriOS они называются функции и подфункции, соответственно. Их вызов выполняется, как и во многих других ОС - выставляем в регистр eax
номер системного вызова (другие регистры заполняем при необходимости, например в ebx
- номер подфункции) и вызываем инструкцию int 0x40
, которая нужна для системного вызова (на Linux используется же int 0x80
).
Для отладки имеется собственный системный вызов под номером 69. Она так и называется - Отладка. Также имеется 10 подфункций (по номерам):
Указание буфера для сохранения отладочных сообщений
Получение регистров
Сохранение регистров
Отсоединение от отлаживаемого процесса
Приостановка потока
Возобновление потока
Чтение из памяти процесса
Запись в память процесса
Завершение отлаживаемого потока
Установить/снять аппаратную точку останова
Эти вызовы можно применить только для отлаживаемого процесса. Чтобы начать отлаживать процесс, необходимо создать его с определенным флагом. Для создания новых процессов используется системный вызов 70, подфункция 7 - Запуск программы (далее, такие пары буду указывать как 70,7
). Ей передается указатель на особую структуру, в которой необходимо выставить бит 0 в поле флагов. Пока это единственный поддерживаемый флаг.
В документации на wiki.kolibrios.org используется именование функция-подфункция. Но чтобы не было конфликтов с обычными функциями, я буду называть их системными вызовами.
В качестве подопытного мы будем рассматривать koldbg
- интерактивный (оконный) отладчик для KolibriOS. Он кстати тоже написан полностью на ассемблере, поэтому примеры кода будут интересными. Кроме него есть еще mtdbg
, но у первого больше функциональности.
Макрос mcall
Далее, буду показывать кода ассемблера. KolibriOS написан на FASM и в нем есть концепция макросов. Один из часто используемых - mcall
. По сути, это просто макрос для системного вызова. Просто посмотрите на его определение и сами все поймете:
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/macros.inc#L297
macro mcall a,b,c,d,e,f,g { ; [mike.dld], [Ghost]
local ..ret_point
__mov eax,a
__mov ebx,b
__mov ecx,c
__mov edx,d
__mov esi,e
__mov edi,f
__mov ebp,g
if __CPU_type eq p5
int 0x40
else
if __CPU_type eq p6
push ebp
mov ebp, esp
push ..ret_point ; it may be 2 or 5 byte
sysenter
..ret_point:
pop edx
pop ecx
else
if __CPU_type eq k6
push ecx
syscall
pop ecx
else
display 'ERROR : unknown CPU type (set to p5)', 10, 13
__CPU_type equ p5
int 0x40
end if
end if
end if
}
Вначале, мы выставляем аргументы системного вызова, а затем, в зависимости от архитектуры, соответствующую инструкцию для вызова ядра.
Архитектура
Для начала немного архитектуры. В koldbg
она событийно-ориентированная.
Перед началом работы выставляются флаги событий, которые мы отслеживаем, системный вызов 40
- установить маску для ожидаемых событий. Отслеживается 4 события: нажатие на клавиши (клавиатуры), нажатие на кнопку (окна), событие отладки и перерисовка окна (да, оконная подсистема внутри ядра).
Далее, вызывается бизнес-логика и по ее окончании вызывается функция WaitEvent
- ожидание другого события и запуск для него обработчика. Это системный вызов 10
- ожидать события.
Событийная часть
SF_WAIT_EVENT
- это константа 10
; Входная точка
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L1663
Start:
; ...
; set event mask - default events and debugging events
mcall 40,EVM_REDRAW or EVM_KEY or EVM_BUTTON or EVM_DEBUG
; ...
; Ожидание следующего события и запуск обработчика для него
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L1720
WaitEvent:
push SF_WAIT_EVENT
pop eax
int 40h
cmp al,9
jz DebugMsg
dec eax
jz Redraw
dec eax
jz Key
sub eax,4
jz Mouse
; Дальше код закрытия окна - обработчик EVM_BUTTON (т.к. кнопка одна - закрытие)
; Событие отладки
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L1894
DebugMsg:
; ...
DbgMsgStart:
; ...
DbgNotify:
; ...
; Процесс завершился
Terminated:
; ...
; Исключение
Exception:
; ...
.Int3:
.UserINT3:
.NotDbg:
.Suspended:
; ...
DbgMsgEnd:
; ...
jmp WaitEvent
; Перерисовка окна
Redraw:
; ...
.ReSize:
; ...
.ReSizeDraw:
; ...
; Нажатие клавиши
Key:
; ...
cmp [DebuggeePID],0
jz DoCommandLine
cmp [CmdLineActive],0
jnz GetCommandLine
jmp WinSwitch
; Step into
F7:
jz .No
call OnStep
.No:jmp WaitEvent
; Step over
F8:
jz F7.No
call OnProceed
jmp F7.No
; Кнопка мыши
Mouse:
; Обработка клика и двойного клика
jmp WaitEvent
В ассемблере могли заметить обработчик нажатия клавиши - Key
. Внутри он вызывает другую функцию DoCommandLine
. Из названия становится ясно, что она обрабатывает командную строку. В общих чертах, отладчик хранит буфер текущей команды и как только обработчик Key
получает Enter
, то происходит процесс выполнения команды. Для поиска команды используется функция FindCmd
. После ее нахождения в регистре esi
будет храниться адрес нужного обработчика. Функции всех обработчиков имеют вид OnXXX
, где XXX
- это название команды.
Запуск процесса под отладкой
Теперь, мы знаем достаточно, чтобы начать исследовать отладчик.
В начале, рассмотрим процесс запуска отлаживаемого процесса. Для запуска программы используется команда load
. На вход она получает путь к программе и аргументы. Здесь ничего необычного.
За load
отвечает функция OnLoad
. Внутри она просто обертка над системным вызовом 70,7
. Этот системный вызов просто создает и запускает новый процесс. Он принимает указатель на структуру с информацией о целевом процессе. В нем имеется поле флагов, в котором необходимо выставить единственный флаг - сигнал о том, что процесс необходимо отлаживать.
Запуск процесса
; Статически аллоцированная структура
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L2698
FN70LoadBlock: dd 7 ; Первое поле - это номер подфункции
dd 1 ; Флаг - запуск под отладкой
LoadParams dd 0 ; Остальные поля заполняются в OnLoadInit
dd 0
dd 0
LoadName: db 0
rb 255
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L221
OnLoad: mov esi,[CurArg]
OnLoadInit:
; ...
DoReLoad:
; ...
; Вызов 70,7
mcall 70,FN70LoadBlock
test eax,eax
jns .Loaded
; ...
.Loaded:
; Подгрузка отладочных символов
; ...
mov dword [ecx],'.dbg'
call OnLoadSymbols.Silent ; Try to load .dbg file
; ...
mov dword [ecx],'.map' ; If failed, try .map file too
call OnLoadSymbols.Silent
ret
Можете заметить инструкции в конце. Они нужны для загрузки map
файлов - символьной информации (адреса функций, переменных и другой символьной информации). Вначале, пробуется .dbg
файл (генерируется FASM), а затем .map
(генерируется GCC).
Точки останова
Теперь, перейдем к более интересной теме - точки останова. За них отвечает команда bp
и соответствующий обработчик OnBp
. Внутри нее происходит тривиальная логика: вычисление расположения, нахождение существующей точки останова, ее выставление и показ сообщения.
Точка останова выставляется так же как и на всех других платформах - запись инструкции 0xCC
. Эта логика находится в функции EnableBreakPoint
. Для ее выставления необходимо взаимодействовать с памятью отлаживаемого процесса - это реализуется с помощью системных вызовов 69,6
(прочитать из памяти отлаживаемого процесса) и 69,7
(записать в память отлаживаемого процесса):
EnableBreakPoint
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L2143
EnableBreakPoint:
; ...
; Сохраняем инструкцию
mcall 69,6,[DebuggeePID],1,[edi-5]
dec eax
jnz .Err
; Та самая точка останова
push 0xCC
mov edi,esp
; 7 = 6 + 1
inc ebx
; Записываем инструкцию
mcall 69
pop eax
; ...
.DR:
; Выставление аппаратной точки останова
; ...
mcall 69,9,[DebuggeePID]
; ...
Можете заметить, что в конце имеется вызов 69,9
- это аппаратная точка останова.
Когда мы достигаем точки останова (программной), то вызывается обработчик события исключения. Эту часть уже могли видеть, но продублирую еще раз:
Обработчик события исключения
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L1894
DebugMsg:
; ...
; Обработчик исключения
Exception:
; int3 command generates exception 0x0D, #GP
push eax
cmp al,0x0D
jnz .NotDbg
; check for 0xCC byte at eip
push 0
mcall 69,6,[DebuggeePID],1,[_EIP],esp
pop eax
cmp al,0xCC
je .Int3
; check for 0xCD03 word at eip
push 0
inc edx
mcall 69;,6,[DebuggeePID],2,[_EIP],esp
pop eax
cmp ax,0x03CD
jne .NotDbg
mov eax,[_EIP]
inc [_EIP]
inc [_EIP]
jmp .UserINT3
.Int3:
; this is either dbg breakpoint or int3 cmd in debuggee
mov eax,[_EIP]
call FindEnabledBreakPoint
jnz .UserINT3
; dbg breakpoint; clear if one-shot
; Обрабатываем точку останова
; ...
.UserINT3:
; Это пользовательская инструкция int3
; ...
.NotDbg:
; ...
jmp WaitEvent
Когда мы получаем исключение, то проверяем, что код исключения, который генерирует int3
. А затем проверяем, что под нами находится либо:
0xCC
- это нашint3
либо0xCD03
- это уже развернутая версияint 3
(напомню, что3
- это аргумент дляint
, а сама инструкцияint
имеет код0xCD
)
Потом мы просто проверим, что это наша точка останова (мы ее выставили).
Немного жаргонизма
Читая документацию к koldbg
узнал, что точки останова называют бряки
Соответствующие условия называются точками останова, breakpoint(s), в просторечии - бряками.
Шаги
Теперь приступим к шагам. Мы рассматриваем koldbg
, это ассемблерный отладчик, то есть здесь нет информации о строках исходного кода, только ассемблер, поэтому всего у нас будет 2 типа шагов:
Step
Proceed
Первое - это всем известное step into. То есть мы просто выполняем текущую инструкцию. Шаг выполняется с помощью команды step
(или s
) и за его обработку отвечает функция OnStep
.
Реализовано это с помощью выставления TF
флага в регистрах. Но дальше уже становится интереснее, потому что происходит исследование текущей инструкции. В частности, мы проверяем сейчас мы выполняем инструкции:
int XXX
syscall
sysenter
Если мы встретили хотя бы один из них, то TF
флаг убирается, и после этих инструкций выставляется временная точки останова (one-shot breakpoint).
OnStep
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L810
OnStep:
; ...
; Выставляем флаг TF
call GetContext
or byte [_EFL+1],1 ; set TF
call SetContext
and byte [_EFL+1],not 1
; if instruction at eip is "int xx", set one-shot breakpoint immediately after
mov eax,[_EIP]
call FindEnabledBreakPoint
jnz @F
cmp byte [edi+5],0xCD
jz .Int
@@:
; Проверяем следующую инструкцию
push 0
mcall 69,6,[DebuggeePID],3,[_EIP],esp
; int XXX
cmp eax,edx
pop eax
jnz .DoIt
cmp al,0xCD
jz .Int
; syscall
cmp ax,0x050F
jz .SysCall
; sysenter
cmp ax,0x340F
jz .SysEnter
; resume process
.DoIt:
ret
; return address is [ebp-4]
; У SysEnter и SysCall логика примерно одинаковая - объединил
.SysEnter:
.SysCall:
and byte [_EFL+1],not 1 ; clear TF - avoid system halt (!)
call SetContext
; Выставляем временную точку останова после
; после текущей инструкции, если не выставлена
.Int:
mov eax,[_EIP]
inc eax
inc eax
@@:
push eax
call FindEnabledBreakPoint
pop eax
jz .DoIt
; there is no enabled breakpoint yet; set temporary breakpoint
mov bl,5
call AddBreakPoint
jmp .DoIt
Теперь подойдем к Proceed. Если не догадались, то это step over. Шаг выполняется с помощью команды proceed
(или p
).
Я выделил 3 категории инструкций, которые необходимо обрабатывать по особенному:
Операции со строками:
movs
(копирование),cmps
(сравнение),stos
(сохранение),lods
(чтение),scas
(поиск)Циклы:
loopnz
,loopz
,loop
Вызов функции:
call
Если мы встречаем одну из таких, то просто вставляем временную точку останова после этой инструкции. В противном случае, выполняем step (буквально вызываем его обработчик).
OnProceed
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L906
OnProceed:
; ...
@@:
; Если мы на точке останова, то выполняем step и заходим в нее
call GetByteNoBreak
jc OnStep
inc esi
; skip prefixes
call IsPrefix
jz @B
cmp al,0xE8 ; call
jnz @f
add esi,4
jmp .DoIt
; Обнаруживаем особенные инструкции
; Строковые
; A4,A5 = movs; A6,A7 = cmps
@@:
cmp al,0xA4
jb @F
cmp al,0xA8
jb .DoIt
; AA,AB = stos; AC,AD = lods; AE,AF = scas
@@:
cmp al,0xAA
jb @F
cmp al,0xB0
jb .DoIt
; Циклы
; E0 = loopnz; E1 = loopz; E2 = loop
@@:
cmp al,0xE0
jb .NoLoop
cmp al,0xE2
ja .NoLoop
inc esi
jmp .DoIt
; Вызов функции
; FF /2 = call
.NoLoop:
cmp al,0xFF
jnz OnStep
; ...
; Ставим временную точку останова и продолжаем выполнение
.DoIt:
; insert one-shot breakpoint at esi and resume
call GetByteNoBreak
jc OnStep
mov eax,esi
call FindEnabledBreakPoint
jz @F
mov eax,esi
mov bl,5
call AddBreakPoint
jmp OnStep.DoIt
@@:
ret
Также, вы могли заметить, что в начале этой функции находится вызов другой функции IsPrefix
. Причина в том, что в инструкциях x86 каждая инструкция может иметь префикс - 1 байт описывающий команду или уточняющий ее семантику. Например, можно выделить такие примеры префиксов:
Указание размера операнда (32/16 битные регистры)
Указание размера адреса (64/32 битный адрес)
Повторить (rep, repnz)
Заблокировать кэш линию для атомарной операции (lock)
Они просто информационные, поэтому их стоит пропустить. Но здесь кроется разгадка того почему для строковых инструкций и цикла мы ставим точку останова вместо того, чтобы просто выполнить ее (step). Все дело в префиксе rep
/repnz
- повторение инструкции пока регистр RCX
не станет равным 0. Сама инструкция (строковая или цикл) выполняет только 1 действие, но из-за rep
она будет постоянно выполнять эту инструкцию раз за разом.
А если посмотрим на функцию IsPrefix
, то поймем, что она не возвращает, что за тип префикса перед нами. Поэтому все что нам остается - думать о худшем и поставить точку останова.
IsPrefix
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L1022
IsPrefix:
cmp al,0x64 ; fs:
jz .Ret
cmp al,0x65 ; gs:
jz .Ret
cmp al,0x66 ; use16/32
jz .Ret
cmp al,0x67 ; addr16/32
jz .Ret
cmp al,0xF0 ; lock
jz .Ret
cmp al,0xF2 ; repnz
jz .Ret
cmp al,0xF3 ; rep(z)
jz .Ret
cmp al,0x2E ; cs:
jz .Ret
cmp al,0x36 ; ss:
jz .Ret
cmp al,0x3E ; ds:
jz .Ret
cmp al,0x26 ; es:
.Ret:
ret
Ну и последний вопрос - как мы понимаем, что остановились из-за шага? Все очень просто - в обработчике исключения мы перепрыгиваем через обработчики точки останова и передаем управление пользователю. А флаг TF
очищается всегда, даже если не выставлен.
Архитектуры ЦП
Теперь перейдем к самому низу - процессорам. К этому моменту уже становится понятно, что от процессора нам необходимо по сути 2 вещи: знать инструкцию точки останова и возможность выполнить только 1 инструкцию. Дополнительно, можно указать возможность поставить аппаратную точку останова.
Начнем с x86, так как все примеры выше относились к нему, поэтому начать будет просто.
x86
Также известен под: i386, 8086, IA-32
Это 32 бит, 64 рассматриваю отдельно
В начале немного теории.
Процессор выполняет инструкции последовательно (есть пайплайны, но они должны быть прозрачными для пользователя). Но что же делать, если нам пришло уведомление от кого-то и надо его проверить?
Здесь в игру вступают прерывания (interrupts) и исключения (exceptions):
interrupt - это прерывание, генерируемое внешним устройством (например, сетевая карта)
exception - это сигнал, который генерируется самим процессором из-за внештатной (или не очень) ситуации (например, деление на 0)
Когда ОС загружается одна из первых вещей, которые она делает - инициализирует IDT, Interrupt Descriptor Table. Можно сказать, что это массив обработчиков таких прерываний/исключений. Все исключения имеют свой номер и когда он случается, то запускается обработчик из IDT. Можно сказать, что это массив, но не указателей, а специальной записи, которая хранит информацию о расположении этого обработчика.
Подобный механизм для обработки асинхронных событий есть практически в каждом процессоре. Теперь перейдем к функциональности.
Точки останова
x86 - это CISC архитектура, то есть размеры ее инструкций переменные. Чтобы мы могли безопасно вставлять инструкции точки останова, ее размер должен быть равен размеру наименьшей адресуемой ячейки. В данном случае это 1 байт - 0xCC
. Вообще, это сокращенная форма общей 2 байтной инструкции int 3
(0xCD03
). Когда процессор выполняет эту инструкцию, то генерируется исключение Breakpoint Exception (#BP).
Но эти 2 формы - не одно и то же. Согласно документации, разница проявляется при использовании VM8086 режима (virtual-8086 mode). Цитирую из документации:
An interrupt generated by the
INTO
,INT3
, orINT1
instruction differs from one generated byINT n
in the following ways:
The normal IOPL checks do not occur in virtual-8086 mode. The interrupt is taken (without fault) with any IOPL value.
The interrupt redirection enabled by the virtual-8086 mode extensions (VME) does not occur. The interrupt is always handled by a protected-mode handler.
(These features do not pertain to
CD03
, the “normal” 2-byte opcode forINT 3
. Intel and Microsoft assemblers will not generate theCD03
opcode from any mnemonic, but this opcode can be created by direct numeric code definition or by self-modifying code.)
Подытоживая, INT3
, INT1
и INTO
обходят различные проверки IOPL (I/O Priviledge Level) и сразу же отправляются обработчику прерывания. Но к CD03
(полный INT3
) это не относится.
Посмотрим как это исключение обрабатывается в ядре Linux:
Обработка BP
В обработке исключения участвует множество функций. Я расположил их сверху в них, в порядке вызова.
/*
* Номер исключения
* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/x86/include/asm/trapnr.h#L22
*/
#define X86_TRAP_BP 3 /* Breakpoint */
/*
* Регистрация IDT
* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/x86/kernel/idt.c#L63
*/
static const __initconst struct idt_data early_idts[] = {
/* ... */
SYSG(X86_TRAP_BP, asm_exc_int3),
/* ... */
};
/*
* Определение обработчика
* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/x86/kernel/traps.c#L784
*/
DEFINE_IDTENTRY_RAW(exc_int3)
{
/* ... */
if (user_mode(regs)) {
do_int3_user(regs);
} else {
if (!do_int3(regs))
die("int3", regs, 0);
}
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/x86/kernel/traps.c#L774 */
static void do_int3_user(struct pt_regs *regs)
{
/* ... */
do_trap(X86_TRAP_BP, SIGTRAP, "int3", regs, 0, 0, NULL);
/* ... */
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/x86/kernel/traps.c#L191 */
static void
do_trap(int trapnr, int signr, char *str, struct pt_regs *regs,
long error_code, int sicode, void __user *addr)
{
/* ... */
if (!sicode)
force_sig(signr);
else
/* ... */
}
Можете заметить, что мы зарегистрировали обработчик для int 3
(по коду исключения 3), а затем превратили его в сигнал SIGTRAP
, который отправили процессу.
Если вы внимательно читали код, то могли заметить, что в KolibriOS обрабатывается не #BP, а #GP - General Protection Exception (код исключения 0x0D
). Еще раз приведу этот участок проверки:
Обработка GP вместо BP
DebugMsg:
; ...
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/programs/develop/koldbg/koldbg.asm#L1963
Exception:
; ...
; int3 command generates exception 0x0D, #GP
push eax
cmp al,0x0D
jnz .NotDbg
; ...
Но точка останова генерирует #BP (0x03
). Почему так? Ответ мы найдем, если почитаем документацию немного дальше:
Each of the
INT n
,INTO
, andINT3
instructions generates a general-protection exception (#GP) if the CPL is greater than the DPL value in the selected gate descriptor in the IDT.
Здесь речь идет о каких-то CPL и DPL - Current Protection Level и Descriptor Protection Level, соответственно. Если говорить коротко, то когда выполняем int XXX
, то за его обработку отвечает функция, которая хранится в IDT (Interrupt Description Table). Грубо говоря, это массив из 8 байтных (для 32-битной системы) дескрипторов. За обработку прерывания XXX
(аргумент для int
) отвечает обработчик под индексом XXX
в IDT.
Но безопасность превыше всего, поэтому, чтобы ограничить доступ непривилегированным процессам, каждый такой дескриптор имеет "уровень допуска" - этот самый DPL. Он задается как 2 битное число и соответствует своему кольцу защиты. Их может быть 4 (от 0 до 3), но на практике используется 0 (kernel-space) и 3 (user-space).
А теперь, следите за руками. Согласно документации, все int XXX
должны проверять свой IOPL (I/O Priviledge Level) - CPL должен быть не больше DPL (то есть у вызывающего процесса привилегий для вызова не меньше, что логично), но если это не так, то будет вызвано исключение #GP и int3
тому не исключение.
Где задается этот уровень привилегий для обработчика прерывания? В том самом дескрипторе - элементе IDT.
Offset |
P |
DPL |
0 |
Gate Type |
Reserved |
Segment Selector |
Offset |
---|---|---|---|---|---|---|---|
63-48 |
47 |
46-45 |
44 |
43-40 |
39-32 |
31-16 |
15-0 |
Жирным я выделил поле, которое за этот DPL отвечает. Оно как раз занимает 2 бита.
Чтобы все положить на свои места посмотрим как регистрируются обработчики IDT в ядре KolibriOS:
Регистрация IDT
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/kernel/trunk/core/sys32.inc#L18
build_interrupt_table:
; Создание таблицы IDT
mov edi, idts
mov esi, sys_int
mov ecx, 0x40
mov eax, (10001110b shl 24) + os_code
@@:
movsw ; low word of code-entry
stosd ; interrupt gate type : os_code selector
movsw ; high word of code-entry
loop @b
movsd ; copy low dword of trap gate for int 0x40
movsd ; copy high dword of trap gate for int 0x40
mov ecx, 23
mov eax, (10001110b shl 24) + os_code
@@:
movsw ; low word of code-entry
stosd ; interrupt gate type : os_code selector
movsw ; high word of code-entry
loop @b
; Регистрация таблицы
lidt [esi]
ret
; Статически выделенная область адресов обработчиков исключений
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/kernel/trunk/core/sys32.inc#L42
sys_int:
; exception handlers addresses (for interrupt gate construction)
dd e0,e1,e2,e3,e4,e5,e6,except_7 ; SEE: core/fpu.inc
dd e8,e9,e10,e11,e12,e13,page_fault_exc,e15
dd e16, e17,e18, e19
times 12 dd unknown_interrupt ;int_20..int_31
; Шаблонный код функции обработчика исключений
; https://github.com/KolibriOS/kolibrios/blob/975284f5f3deece4fe4ac0ac9227e6728a2f1d1f/kernel/trunk/core/sys32.inc#L94
macro exc_wo_code [num] {
e#num :
save_ring3_context
mov bl, num
; Обобщенный обработчик исключения
jmp exc_c
} exc_wo_code 0,1,2,3,4,5,6,15,16,19
Можно заметить, что для создания этой таблицы используется шаблонный код - в каждом элементе IDT различаются только адреса функции обработчика. Но все остальное у них одинаково. Давайте рассмотрим эту одинаковую часть, а именно вот эту инструкцию - mov eax, (10001110b shl 24) + os_code
.
Адрес обработчика занимает 32 бита, но разбит на 2 16-битные части. В начале мы копируем первую часть, т.е. 16 бит, и уже после нее выполняем интересующую нас инструкцию. Эта инструкция копирует 32 бита в текущий дескриптор (который заполняем на этой итерации). Заполнение происходит с 16-ого бита, поэтому мы заполняем биты с 16 по 48. Вычислим чему будет равно (10001110b shl 24) + os_code
- это 32 битное число. Если мы наложим ее на маску этого дескриптора, то получим, что битовое число 10001110
это биты с 40 по 47, то есть отвечают за поля P
, DPL
и Gate Type
. А что же в DPL
у нас записано? Правильно - 00
, то есть все обработчики в IDT имеют право выполняться только в кольце ядра. А согласно документации, если CPL
> DPL
(3 > 0, в случае отладчика), то будет генерироваться #GP. Вот и все!
Шаги
Теперь, посмотрим на шаги. Реализовать их можно 2 способами.
Про первый мы уже слышали: выставление флага TF
(Trap Flag) в EFLAGS
регистре. Если он выставлен, то после каждой выполненной инструкции процессор будет генерировать исключение #DB (0x01
).
Второй способ - это инструкция int1
(у нее также есть своя 1-байтовая короткая версия - 0xF1
). Она также посылает исключение #DB. Но разница между ними есть - работа с регистрами. Согласитесь, если мы получили #DB, то в общем случае не ясно из-за чего - выставленный флаг или инструкция. Поэтому, было решено добавить специальный флаг BS
в регистр DR6
(Debug-Status): этот флаг выставляется, если #DB посылается из-за выставленного флага TF
.
Я не разработчик железа, но про int1
(osdev или документация intel) пишут, что он подходит для отладки железа по причине того, что он обходит проверку на DPL и может сразу отправить #DB (т.е. не получим #GP):
In contrast, the INT1 instruction can deliver a #DB even if the CPL is greater than the DPL of descriptor 1 in the IDT. (This behavior supports the use of INT1 by hardware vendors performing hardware debug)
Таким образом, в обычной программе нам достаточно просто выставить флаг TF
, чтобы запустить single-stepping режим (что в Windows руками и делали).
Давайте, посмотрим как ptrace
реализует этот режим:
ptrace(PTRACE_SINGLESTEP)
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/kernel/ptrace.c#L809 */
#define is_singlestep(request) ((request) == PTRACE_SINGLESTEP)
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/kernel/ptrace.c#L823 */
static int ptrace_resume(struct task_struct *child, long request,
unsigned long data)
{
if (/* ... */) {
/* ... */
} else if (is_singlestep(request) || /* ... */) {
if (unlikely(!arch_has_single_step()))
return -EIO;
user_enable_single_step(child);
} else {
/* ... */
}
/* ... */
return 0;
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/x86/kernel/step.c#L219 */
void user_enable_single_step(struct task_struct *child)
{
enable_step(child, 0);
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/x86/kernel/step.c#L204 */
static void enable_step(struct task_struct *child, bool block)
{
if (enable_single_step(child) && /* ... */)
/* ... */
else if (/* ... */)
/* ... */
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/x86/kernel/step.c#L113 */
static int enable_single_step(struct task_struct *child)
{
struct pt_regs *regs = task_pt_regs(child);
/* ... */
/* Set TF on the kernel stack.. */
regs->flags |= X86_EFLAGS_TF;
/* ... */
return 1;
}
Как и ожидалось, ptrace
просто выставляет флаг TF
в EFLAGS
.
Аппаратные точки останова
Последнее - точки останова. Как вы могли догадаться, речь пойдет об аппаратных точках останова.
В x86 поддерживается 4 аппаратных точки останова. Напомню, что эти точки останова срабатывают при попытке работы с какой-либо ячейкой памяти. Регистры DR0
, DR1
, DR2
и DR3
хранят эти адреса.
Также, в регистре DR7
(Debug Control) хранится описание этих точек останова. Для каждой свой набор характеристик (далее n
- это номер точки останова, 0-3):
Локальная или глобальная (первая, очищается при переключении задачи, вторая - сохраняется) -
Ln
илиGn
Условия запуска (чтение, запись, выполнение) -
Bn
Размер адреса (накладывается маска, чтобы определить подходит ли адрес) -
LENn
Все эти точки останова вызывают #DB, но это исключение генерируется в различное время: чтение/запись после, а выполнение - перед выполнением целевой инструкции. В документации делают разделение на классы исключений: первое - это Trap
(генерируется после выполнения инструкции), второе - Fault
(перед выполнением).
На самом деле,
Fault
возникает после выполнения, просто состояние восстанавливается каким было до выполнения.
Чтобы понять, что #DB возник из-за аппаратной точки останова используются флаги B0
- B3
регистра DR6
. Они выставляются перед выполнением обработчика. Это позволяет нам определить, какая из них сработала.
Давайте посмотрим как они (аппаратные точки останова) используются. Но делать это будет на примере gdb, так как это функциональность предоставляемая процессам.
В gdb аппаратные точки останова называются watchpoint
(или data breakpoint
). Для ее установки используется команда watch
.
Сама логика ее выставления находится в функции x86_dr_insert_watchpoint
:
x86_dr_insert_watchpoint
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/nat/x86-dregs.c;h=7ea9f49eb680955ffffc9132d67b2aebd773f941;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l504 */
int
x86_dr_insert_watchpoint (struct x86_debug_reg_state *state,
enum target_hw_bp_type type,
CORE_ADDR addr, int len)
{
int retval;
/* Work on a local copy of the debug registers, and on success,
commit the change back to the inferior. */
struct x86_debug_reg_state local_state = *state;
if (type == hw_read)
return 1; /* unsupported */
if (((len != 1 && len != 2 && len != 4)
&& !(TARGET_HAS_DR_LEN_8 && len == 8))
|| addr % len != 0)
{
retval = x86_handle_nonaligned_watchpoint (&local_state,
WP_INSERT,
addr, len, type);
}
else
{
unsigned len_rw = x86_length_and_rw_bits (len, type);
retval = x86_insert_aligned_watchpoint (&local_state,
addr, len_rw);
}
if (retval == 0)
x86_update_inferior_debug_regs (state, &local_state);
if (show_debug_regs)
x86_show_dr (state, "insert_watchpoint", addr, len, type);
return retval;
}
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/nat/x86-dregs.c;h=7ea9f49eb680955ffffc9132d67b2aebd773f941;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l304 */
static int
x86_insert_aligned_watchpoint (struct x86_debug_reg_state *state,
CORE_ADDR addr, unsigned len_rw_bits)
{
int i;
if (!x86_dr_low_can_set_addr () || !x86_dr_low_can_set_control ())
return -1;
/* First, look for an occupied debug register with the same address
and the same RW and LEN definitions. If we find one, we can
reuse it for this watchpoint as well (and save a register). */
ALL_DEBUG_ADDRESS_REGISTERS (i)
{
if (!X86_DR_VACANT (state, i)
&& state->dr_mirror[i] == addr
&& X86_DR_GET_RW_LEN (state->dr_control_mirror, i) == len_rw_bits)
{
state->dr_ref_count[i]++;
return 0;
}
}
/* Next, look for a vacant debug register. */
ALL_DEBUG_ADDRESS_REGISTERS (i)
{
if (X86_DR_VACANT (state, i))
break;
}
/* No more debug registers! */
if (i >= DR_NADDR)
return -1;
/* Now set up the register I to watch our region. */
/* Record the info in our local mirrored array. */
state->dr_mirror[i] = addr;
state->dr_ref_count[i] = 1;
X86_DR_SET_RW_LEN (state, i, len_rw_bits);
/* Note: we only enable the watchpoint locally, i.e. in the current
task. Currently, no x86 target allows or supports global
watchpoints; however, if any target would want that in the
future, GDB should probably provide a command to control whether
to enable watchpoints globally or locally, and the code below
should use global or local enable and slow-down flags as
appropriate. */
X86_DR_LOCAL_ENABLE (state, i);
state->dr_control_mirror |= DR_LOCAL_SLOWDOWN;
state->dr_control_mirror &= X86_DR_CONTROL_MASK;
return 0;
}
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/nat/x86-linux-dregs.c;h=a6c0ea63552d6563d3da0f578bc759eae0706828;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l139 */
void
x86_linux_update_debug_registers (struct lwp_info *lwp)
{
ptid_t ptid = ptid_of_lwp (lwp);
int clear_status = 0;
gdb_assert (lwp_is_stopped (lwp));
if (lwp_debug_registers_changed (lwp))
{
struct x86_debug_reg_state *state = x86_debug_reg_state (ptid.pid ());
int i;
/* Prior to Linux kernel 2.6.33 commit
72f674d203cd230426437cdcf7dd6f681dad8b0d, setting DR0-3 to
a value that did not match what was enabled in DR_CONTROL
resulted in EINVAL. To avoid this we zero DR_CONTROL before
writing address registers, only writing DR_CONTROL's actual
value once all the addresses are in place. */
x86_linux_dr_set (ptid, DR_CONTROL, 0);
ALL_DEBUG_ADDRESS_REGISTERS (i)
if (state->dr_ref_count[i] > 0)
{
x86_linux_dr_set (ptid, i, state->dr_mirror[i]);
/* If we're setting a watchpoint, any change the inferior
has made to its debug registers needs to be discarded
to avoid x86_stopped_data_address getting confused. */
clear_status = 1;
}
/* If DR_CONTROL is supposed to be zero then it's already set. */
if (state->dr_control_mirror != 0)
x86_linux_dr_set (ptid, DR_CONTROL, state->dr_control_mirror);
lwp_set_debug_registers_changed (lwp, 0);
}
if (clear_status
|| lwp_stop_reason (lwp) == TARGET_STOPPED_BY_WATCHPOINT)
x86_linux_dr_set (ptid, DR_STATUS, 0);
}
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/nat/x86-linux-dregs.c;h=a6c0ea63552d6563d3da0f578bc759eae0706828;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l58 */
static void
x86_linux_dr_set (ptid_t ptid, int regnum, unsigned long value)
{
int tid;
gdb_assert (ptid.lwp_p ());
tid = ptid.lwp ();
errno = 0;
ptrace (PTRACE_POKEUSER, tid, u_debugreg_offset (regnum), value);
if (errno != 0)
perror_with_name (_("Couldn't write debug register"));
}
Можем заметить 4 вещи:
В начале, мы проверяем маску длины адреса. Проверяются 3 маски, а также еще 4-ая, но с флагом (в следующем разделе объяснение)
gdb хранит копию (mirror) настоящих регистров отладки, работает с ними, а после обновляет настоящие
Для обновления DR регистров используется
ptrace(POKEUSER)
Все аппаратные точки останова выставляются локальными (
X86_DR_LOCAL_ENABLE (state, i);
), хотя поддержка для глобальных имеетсяgdb не поддерживает
watchpoint
с доступом чтения (hw_read
)
По последнему пункту отдельно. В документации gdb нашел следующее:
x86 processors support setting watchpoints on I/O reads or writes. However, since no target supports this (as of March 2001), and since enum target_hw_bp_type doesn't even have an enumeration for I/O watchpoints, this feature is not yet available to GDB running on x86.
То есть, физически мы можем выставить эти флаги, но работать они не будут. Поэтому в gdb просто не стали это делать.
AMD64
Только что мы рассмотрели x86. Но у него есть 64-битный брат - amd64 (x64, x86_64, x86-64).
По факту, это расширение x86, поэтому сильных различий в ней нет, но некоторые есть, их и перечислю. Изменения я брал из документации.
Во-первых, регистры. В amd64 они были расширены до 64 бит и регистры отладки тоже - DR0 - DR3
теперь занимают 64 бита. Если вспомнить, то длина адреса определяется полем LENn
в DR7. Но поддержка 64-бит оказалась проста - в таблице соответствия значения к длине оставалось одно неиспользуемое - 10
. Вот его и стали использовать для определения 64-битного адреса.
В x86 я обмолвился, что в при выставлении
DR
регистров проверялась длина для 3 значений и еще 4 при специальном флаге. По сути это флаг 64-битного режима.
Также, добавлено еще 3 отладочных регистра:
DebugCtlMSR
LastBranchx
LastExceptionx
MSR
- Model-specific register
DebugCtlMSR
по сути добавляет 2 фичи:
Отслеживание производительности
Останов на ветвлении
Первое, отслеживание производительности, нам не особо интересно. Поэтому сфокусируемся на 2 - останов на ветвлении.
Раньше, мы могли остановиться либо после текущей инструкции, либо на определенной точке останова. Но благодаря флагу BTF
(Branch Single Step) этого регистра мы можем выполнить инструкцию до следующей инструкции ветвления: jmp
, loop
, call
, int
, syscall
и т.д.
Этот флаг вспомогательный к TF
(если BTF
= 0, то поведение старое, если 1, то останавливаемся на ветвлении).
Кроме того, есть другой флаг - LBR
(Last Branch Record). Если и он выставлен, то после срабатывания BTF
нам в отдельном регистре становится доступна информация ветвления (старый/новый RIP).
ARM
Также известен как: aarch64/aarch32.
Когда обсуждают отладку на уровне процессора, то чаще всего я слышал про x86. Поэтому сейчас настало время поговорить и про другие архитектуры. Начнем с ARM.
ARM - это не 1 архитектура, а их семейство. Существует несколько наборов инструкций (разделение по длине команд):
A32
- команды 32 битаT32
- команды 16 и 32 битаA64
- команды 32 бита
A64 появилась позже остальных (с выходом ARMV8) и ее отличие в поддержке 64-битной разрядности. В контексте ARM имеется такое понятие как Execution State и их может быть 2: AArch64
и AArch32
. Говоря грубо, это разрядность: первое - 64 бита (A64), вторая - 32 бита (A32, T32). Причем, в процессе работы возможен переход между ними.
Я буду рассматривать AArch64
и, соответственно, A64
. В качестве документации используют этот документ.
Точки останова
Точка останова реализуется с помощью инструкции BRK
. При ее выполнении генерируется исключение Breakpoint Instruction
. Но в отличие от x86 это не просто константная инструкция - она принимает аргумент, 2 байтное число (константа). Эта константа затем сохраняется в регистре ESR_ELn
(n - уровень исключения).
Уровни исключений
В ARMV8 определяется 4 уровня исключений и каждый соответствует какому-либо уровню привилегий (как кольца защиты в x86):
EL0
- приложенияEL1
- ядро ОСEL2
- гипервизорEL3
- монитор безопасности
При обработке исключения его уровень может быть повышен, а при возвращении - понижен. То же самое касается и AArchXXX
- при обработке возможен переход AArch32
-> AArch64
, и наоборот при возвращении.
Также, в ARM имеется несколько Link регистров. Они хранят адреса возврата при вызове функции. А для исключений имеются собственные Exception Link регистры. Всего их 3, для каждого уровня, начиная с 1: ELR_EL1
, ELR_EL2
, ELR_EL3
. В них хранятся адреса для возврата на предыдущий уровень.
Про уровни исключений можно почитать в этой статье.
Таким образом, чтобы поставить точку останова, нам необходимо подставить на место этой константы какое-нибудь число. Но в общем-то это не обязательно.
В указанном документе байты точки останова не указаны, зато они есть здесь. Хотя зачем напрягаться, лучше посмотреть что там в gdb.
Выставление точки останова в gdb
Напомню, что за платформно-зависимые части вынесены в отдельные классы/функции. Поэтому реализация выставления точки останова реализована так: обобщенная функция получает инструкцию точки останова (массив байт), а потом записывает по указанному адресу.
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/aarch64-tdep.c;h=e4bca6c66323b1da1df6a73bb3d49992b2822f2b;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l2520 */
/* AArch64 BRK software debug mode instruction.
Note that AArch64 code is always little-endian.
1101.0100.0010.0000.0000.0000.0000.0000 = 0xd4200000. */
constexpr gdb_byte aarch64_default_breakpoint[] = {0x00, 0x00, 0x20, 0xd4};
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/arch-utils.h;h=40c62f30a65fbf86a1644ae24ef3f6ec329fd87f;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l31 */
template <size_t bp_size,
const gdb_byte *break_insn_little,
const gdb_byte *break_insn_big>
struct bp_manipulation_endian
{
static int
kind_from_pc (struct gdbarch *gdbarch, CORE_ADDR *pcptr)
{
return bp_size;
}
static const gdb_byte *
bp_from_kind (struct gdbarch *gdbarch, int kind, int *size)
{
*size = kind;
if (gdbarch_byte_order (gdbarch) == BFD_ENDIAN_BIG)
return break_insn_big;
else
return break_insn_little;
}
};
#define BP_MANIPULATION(BREAK_INSN) \
bp_manipulation<sizeof (BREAK_INSN), BREAK_INSN>
typedef BP_MANIPULATION (aarch64_default_breakpoint) aarch64_breakpoint;
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/mem-break.c;h=712ad000daf167cd7a7154463da600b5b07cc300;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l37 */
int
default_memory_insert_breakpoint (struct gdbarch *gdbarch,
struct bp_target_info *bp_tgt)
{
CORE_ADDR addr = bp_tgt->placed_address;
const unsigned char *bp;
gdb_byte *readbuf;
int bplen;
int val;
/* Determine appropriate breakpoint contents and size for this address. */
bp = gdbarch_sw_breakpoint_from_kind (gdbarch, bp_tgt->kind, &bplen);
/* Save the memory contents in the shadow_contents buffer and then
write the breakpoint instruction. */
readbuf = (gdb_byte *) alloca (bplen);
val = target_read_memory (addr, readbuf, bplen);
if (val == 0)
{
/* ... */
val = target_write_raw_memory (addr, bp, bplen);
}
return val;
}
Можно заметить, что gdb использует 0 для этой константы.
В
AArch32
используется другая ассемблерная инструкция -BKPT
. Но при этом в A32 используется 32 битная инструкция с 2 байтной константой, а в T32 уже инструкция короче - 16 бит, а константа - 1 байт.
Чтобы определить, что за тип исключения был, в регистр ESR_ELn
записывается код исключения. Для BRK
он будет равен 0x3C
. И если посмотрим в код Linux, то увидим его использование:
Обработка BKPT в Linux
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/arm64/include/asm/esr.h#L67 */
#define ESR_ELx_EC_BRK64 UL(0x3C)
#define ESR_ELx_EC(esr) (((esr) & ESR_ELx_EC_MASK) >> ESR_ELx_EC_SHIFT)
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/arm64/kernel/entry-common.c#L485 */
asmlinkage void noinstr el1h_64_sync_handler(struct pt_regs *regs)
{
unsigned long esr = read_sysreg(esr_el1);
switch (ESR_ELx_EC(esr)) {
/* ... */
case ESR_ELx_EC_BREAKPT_CUR:
case ESR_ELx_EC_SOFTSTP_CUR:
case ESR_ELx_EC_WATCHPT_CUR:
case ESR_ELx_EC_BRK64:
el1_dbg(regs, esr);
break;
/* ... */
}
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/arm64/kernel/entry-common.c#L466 */
static void noinstr el1_dbg(struct pt_regs *regs, unsigned long esr)
{
unsigned long far = read_sysreg(far_el1);
if (/* ... */)
do_debug_exception(far, esr, regs);
}
Шаги
Реализация шагов аналогична x86. В регистре PSTATE
имеется специальный флаг SS
(Single Step). Если он выставлен, то после выполнения инструкции будет послан специальный сигнал.
Ранее, я уже показывал код из lldb для продолжения работы на Windows. Там, мы рассматривали TF
флаг процессора. Но также внизу был код для ARM. Продублирую этот участок кода:
TragetThreadWindows::DoResume
/* https://github.com/llvm/llvm-project/blob/6d34cfac53b993a6cdf3d6669e017eac3a2296c8/lldb/source/Plugins/Process/Windows/Common/NativeThreadWindows.cpp#L46 */
Status TargetThreadWindows::DoResume() {
/* ... */
if (resume_state == eStateStepping) {
const ArchSpec &arch = process->GetTarget().GetArchitecture();
switch (arch.GetMachine()) {
case llvm::Triple::x86:
case llvm::Triple::x86_64:
flags_value |= 0x100; // Set the trap flag on the CPU
break;
case llvm::Triple::aarch64:
case llvm::Triple::arm:
case llvm::Triple::thumb:
flags_value |= 0x200000; // The SS bit in PState
break;
}
}
/* ... */
}
Комментарий говорит сам за себя - выставляем флаг SS
(бит) в регистре PSTATE
. Ну и чтобы наверняка - посмотрим как в Linux реализуется ptrace(PTRACE_SINGLESTEP)
:
ptrace(PTRACE_SINGLESTEP)
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/kernel/ptrace.c#L823 */
static int ptrace_resume(struct task_struct *child, long request,
unsigned long data)
{
if (/* ... */) {
/* ... */
} else if (is_singlestep(request) || is_sysemu_singlestep(request)) {
user_enable_single_step(child);
} else {
/* ... */
}
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/arm64/kernel/debug-monitors.c#L445 */
void user_enable_single_step(struct task_struct *task)
{
struct thread_info *ti = task_thread_info(task);
if (!test_and_set_ti_thread_flag(ti, TIF_SINGLESTEP))
set_user_regs_spsr_ss(user_regs);
}
#define DBG_SPSR_SS (1 << 21)
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/arm64/kernel/debug-monitors.c#L144 */
static void set_user_regs_spsr_ss(struct user_pt_regs *regs)
{
regs->pstate |= DBG_SPSR_SS;
}
На этом можно было и остановиться, если бы не gdb. В gdb имеется функциональность выполнять software шаги, то есть тогда когда не доступны шаги через hardware (те самые флаги регистра). В таких случаях, шаги реализуются через точки останова.
И вот тут следует знать об одной особенности этой архитектуры - расширение LSE
, Large System Extensions. Грубо говоря, эта фича добавляет поддержку транзакций - набор инструкций, которые должны быть выполнены атомарно.
В gdb решено, что если мы решили сделать шаг, когда находимся перед такой последовательностью, то шаг - это вся транзакция. А для реализации этого, ищется конец этой последовательности. Реализовано это в функции aarch64_software_single_step
:
aarch64_software_single_step
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/aarch64-tdep.c;h=e4bca6c66323b1da1df6a73bb3d49992b2822f2b;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l3450 */
static std::vector<CORE_ADDR>
aarch64_software_single_step (struct regcache *regcache)
{
struct gdbarch *gdbarch = regcache->arch ();
enum bfd_endian byte_order_for_code = gdbarch_byte_order_for_code (gdbarch);
const int insn_size = 4;
const int atomic_sequence_length = 16; /* Instruction sequence length. */
CORE_ADDR pc = regcache_read_pc (regcache);
CORE_ADDR breaks[2] = { CORE_ADDR_MAX, CORE_ADDR_MAX };
CORE_ADDR loc = pc;
CORE_ADDR closing_insn = 0;
uint32_t insn = read_memory_unsigned_integer (loc, insn_size,
byte_order_for_code);
int index;
int insn_count;
int bc_insn_count = 0; /* Conditional branch instruction count. */
int last_breakpoint = 0; /* Defaults to 0 (no breakpoints placed). */
aarch64_inst inst;
if (aarch64_decode_insn (insn, &inst, 1, NULL) != 0)
return {};
/* Look for a Load Exclusive instruction which begins the sequence. */
if (inst.opcode->iclass != ldstexcl || bit (insn, 22) == 0)
return {};
for (insn_count = 0; insn_count < atomic_sequence_length; ++insn_count)
{
loc += insn_size;
insn = read_memory_unsigned_integer (loc, insn_size,
byte_order_for_code);
if (aarch64_decode_insn (insn, &inst, 1, NULL) != 0)
return {};
/* Check if the instruction is a conditional branch. */
if (inst.opcode->iclass == condbranch)
{
if (bc_insn_count >= 1)
return {};
/* It is, so we'll try to set a breakpoint at the destination. */
breaks[1] = loc + inst.operands[0].imm.value;
bc_insn_count++;
last_breakpoint++;
}
/* Look for the Store Exclusive which closes the atomic sequence. */
if (inst.opcode->iclass == ldstexcl && bit (insn, 22) == 0)
{
closing_insn = loc;
break;
}
}
/* We didn't find a closing Store Exclusive instruction, fall back. */
if (!closing_insn)
return {};
/* Insert breakpoint after the end of the atomic sequence. */
breaks[0] = loc + insn_size;
/* Check for duplicated breakpoints, and also check that the second
breakpoint is not within the atomic sequence. */
if (last_breakpoint
&& (breaks[1] == breaks[0]
|| (breaks[1] >= pc && breaks[1] <= closing_insn)))
last_breakpoint = 0;
std::vector<CORE_ADDR> next_pcs;
/* Insert the breakpoint at the end of the sequence, and one at the
destination of the conditional branch, if it exists. */
for (index = 0; index <= last_breakpoint; index++)
next_pcs.push_back (breaks[index]);
return next_pcs;
}
Аппаратные точки останова
ARM часто используется как микропроцессор на всяких платах, поэтому запустить на нем Линукс, накатить gdb и начать отладку как обсуждали ранее получится не всегда. Но разработчики не глупые, поэтому этот недостаток компенсировали множеством дополнительных регистров.
Для отладки существует несколько вспомогательных отладочных регистров:
DBGAUTHSTATUS_EL1
(Debug Authentication Status) - информация об интерфейсе аутентификации (зависит от реализации)DBGCLAIMSET_EL1
(Debug Claim Tag Set) - выставление специальных битов тэга CLAIM (используется для взаимодействия отладчика и машины)DBGCLAIMCLR_EL1
(Debug Claim Tag Clear) - очистка тэга CLAIMDBGDTR_EL0
(Debug Data Transfer) - передача 64-битных слов между отладчиком и машиной (в обе стороны)DBGDTRRX_EL0
(Debug Data Transfer) - передача 32-битных слов от отладчика к машинеDBGDTRTX_EL0
(Debug Data Transfer) - передача 32-битных слов от машины к отладчикуDBGPRCR_EL1
(Debug Power Control) - запрос на выключение (его эмуляция)DBGVCR32_EL2
(Debug Vector Catch) - возможность доступа к региструDBGVCR
(он есть в AArch32, но не в AArch64)DSPSSR_EL0
(Debug Saved Program Status) - хранит состояние при входе в режим отладкиDLR_EL0
(Debug Link) - адрес возврата для продолжения работы из режима отладки
Эти регистры необходимы при взаимодействии с внешним отладчиком, то есть когда мы отлаживаем железо. Вообще, чтобы отлаживать железо на ARM, необходимо войти в состояние отладки (Debug State). Это можно сделать с помощью инструкции HTL
. Но про отладку железа я говорить не буду, а эти регистры просто упомянул. Сконцентрируемся мы на 4 других.
Эти регистры отвечают за точки останова. Можно сказать, что это 2 группы с 2 шаблонными регистрами: первый хранит какие-то данные, а второй эти данные интерпретирует (регистр данных и регистр контроля назовем их). И сразу спойлер: каждого регистра 16 штук и в названии имеется число <n>
- это его номер (от 0 до 15), хотя в документации говорится, что их может быть от 2 до 16.
Первая группа - точки останова. Когда мы отлаживаем железо, то поставить программную точку останова не представляется возможным, поэтому единственный вариант в данном случае - поддержка со стороны самого железа. И это реализуется с помощью 2 регистров:
DBGBVR<n>_EL1
(Debug Breakpoint Value) - данныеDBGBCR<n>_EL1
(Debug Breakponit Control) - контроль
В регистре контроля хранятся разные поля, но главное - это BT
(Breakpoint Type). Оно определяет тип точки останова и оно же определяет то, как интерпретировать регистр данных. Этот поле занимает 4 бита и точек останова 16 типов соответственно (строго говоря, их 8, так как 1 бит используется как флаг).
Далее идет группа уже для точек наблюдения (watchpoint). Это те самые аппаратные точки останова, о которых говорили в x86. Они нужны для отслеживания изменений в адресах данных:
DBGWVR<n>_EL1
(Debug Watchpoint Value) - данныеDBGWCR<n>_EL1
(Debug Watchpoint Control) - контроль
Здесь уже регистр контроля не определяет интерпретацию регистра данных. Но зато в нем также есть поле условий активации - LSC
(Load Store Control). В отличие от x86 доступны только 3 события: запись (store), чтение (load) или запись/чтение (load/store).
Адрес в регистре данных тоже должен быть выровнен по 8 байтам. Но как же быть, если я хочу, например, отслеживать изменения на адресе 9 (8 + 1)? Для этого имеется поле BAS
(Byte Address Select). Это битовая маска, в которой можно указать смещение отслеживаемого байта. Например, маска 0b00000010
говорит о том, что необходимо отслеживать адрес DBGWVR<n>_EL1
+ 1, поэтому записав в этот регистр адрес 4 и указанную маску. Так как это битовая маска, то, например, выставив все биты в 1 мы будем отслеживать все изменения в слове.
Посмотрим, как это реализуется в gdb:
Выставление аппаратной точки останова
Небольшое замечание: в коде gdb вы не найдете прямого упоминания регистров DBGWCR
/DBGWVR
, вместо этого используется специальная структура, отображаемая на эти регистры. Работа ведется с ней, а после этот кэш сбрасывается.
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/nat/aarch64-hw-point.h;h=0d50eaab0dec8baf111fc2f22e3163be9f5e3de9;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l33 */
#define AARCH64_HBP_MAX_NUM 16
#define AARCH64_HWP_MAX_NUM 16
#define AARCH64_HBP_ALIGNMENT 4
#define AARCH64_HWP_ALIGNMENT 8
struct aarch64_debug_reg_state
{
/* hardware breakpoint */
CORE_ADDR dr_addr_bp[AARCH64_HBP_MAX_NUM];
unsigned int dr_ctrl_bp[AARCH64_HBP_MAX_NUM];
unsigned int dr_ref_count_bp[AARCH64_HBP_MAX_NUM];
/* hardware watchpoint */
/* Address aligned down to AARCH64_HWP_ALIGNMENT. */
CORE_ADDR dr_addr_wp[AARCH64_HWP_MAX_NUM];
/* Address as entered by user without any forced alignment. */
CORE_ADDR dr_addr_orig_wp[AARCH64_HWP_MAX_NUM];
unsigned int dr_ctrl_wp[AARCH64_HWP_MAX_NUM];
unsigned int dr_ref_count_wp[AARCH64_HWP_MAX_NUM];
};
/* https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=gdb/nat/aarch64-hw-point.c;h=6acee0fb814c48ac43e127254f811397802f0682;hb=23c84db5b3cb4e8a0d555c76e1a0ab56dc8355f3#l299 */
static int
aarch64_dr_state_insert_one_point (struct aarch64_debug_reg_state *state,
enum target_hw_bp_type type,
CORE_ADDR addr, int offset, int len,
CORE_ADDR addr_orig)
{
int i, idx, num_regs, is_watchpoint;
unsigned int ctrl, *dr_ctrl_p, *dr_ref_count;
CORE_ADDR *dr_addr_p, *dr_addr_orig_p;
/* Set up state pointers. */
is_watchpoint = (type != hw_execute);
if (is_watchpoint)
{
num_regs = aarch64_num_wp_regs;
dr_addr_p = state->dr_addr_wp;
dr_addr_orig_p = state->dr_addr_orig_wp;
dr_ctrl_p = state->dr_ctrl_wp;
dr_ref_count = state->dr_ref_count_wp;
}
else
{
num_regs = aarch64_num_bp_regs;
dr_addr_p = state->dr_addr_bp;
dr_addr_orig_p = nullptr;
dr_ctrl_p = state->dr_ctrl_bp;
dr_ref_count = state->dr_ref_count_bp;
}
ctrl = aarch64_point_encode_ctrl_reg (type, offset, len);
/* Find an existing or free register in our cache. */
idx = -1;
for (i = 0; i < num_regs; ++i)
{
if ((dr_ctrl_p[i] & 1) == 0)
{
idx = i;
/* no break; continue hunting for an exising one. */
}
else if (dr_addr_p[i] == addr
&& (dr_addr_orig_p == nullptr || dr_addr_orig_p[i] == addr_orig)
&& dr_ctrl_p[i] == ctrl)
{
idx = i;
break;
}
}
/* No space. */
if (idx == -1)
return -1;
/* Update our cache. */
if ((dr_ctrl_p[idx] & 1) == 0)
{
/* new entry */
dr_addr_p[idx] = addr;
if (dr_addr_orig_p != nullptr)
dr_addr_orig_p[idx] = addr_orig;
dr_ctrl_p[idx] = ctrl;
dr_ref_count[idx] = 1;
/* Notify the change. */
aarch64_notify_debug_reg_change (state, is_watchpoint, idx);
}
else
{
/* existing entry */
dr_ref_count[idx]++;
}
return 0;
}
Если мы попадаем на breakponit, то генерируется Breakpoint exception
, а если на watchpoint - то Watchpoint exception
. То есть в итоге, у нас 3 различных исключения (программная точка останова - Breakpoint instruction exception
).
Отладка железа
Ранее я уже обронил слово о внешней отладке, но что такое внятно не объяснил. ARM часто используется на встраиваемых системах. На них ОС не накатишь и по SSH не подключишься. Для их отладки необходимо использовать внешние средства и отладка с помощью них называется внешней отладкой (external debug). По аналогии, отладка привычными нам средствами называется уже self-hosted debug.
Для того, чтобы начать внешнюю отладку на ARM, необходимо войти в состояние отладки (debug state). Когда мы входим в это состояние, то контроль передается внешнему отладчику и выполнение останавливается. А войти в это состояние можно, когда возникают, так называемые, Halting debug exceptions. Самое простое - это инструкция HLT
, при ее выполнении машина переходит в режим отладки. Также, это может быть Breakpoint или Watchpoint exception (логично, что при их срабатывании необходимо остановиться и передать управление). Но чтобы external не конфликтовал self-hosted отладкой существует флаг HDE
(Halting debug enable) в регистре EDSCR
(external debug status and control).
Также, на сколько я понял, поведение инструкции точки останова (
BRK
) в debug state не определено:The following events never generate entry to Debug state:
Breakpoint Instruction exceptions. This means that these events can do one of the following:
They can generate a debug exception.
They can be ignored.
Пока мы находимся в режиме отладки мы можем выполнять отладочные действия: выполнять какие-то инструкции (в документации явно прописано как различные инструкции влияют на состояние), читать значения регистров или памяти и так далее.
Чтобы выйти из debug state, необходимо отправить Restart request
на специальный триггер.
Но на главный вопрос пока не ответили - так как же происходит общение отладчика и процесса? Для взаимодействия между отладчиком и процессом используется DCC - Debug Communication Channel. Это логический канал взаимодействия, а его физическая реализация состоит из совокупности регистров и флагов:
DBGDTRRX
иDBGDTRTX
- это флаги передачи данных (упоминал ранее).DBG
- debug,DTR
- data transfer register,RX
- receive,TX
- transmit.EDITR
(External Debug Instruction Transfer Register) - регистр для передачи инструкций, которые необходимо выполнить.EDSCR
(External Debug Status and Control Register) - регистр с флагами контроля. Например,RXO
- флаг уведомления об ошибке потока передачи.
Но конечно же это самый низкоуровневый интерфейс. Им пользуются уже более высокоуровневые, предоставляющие интерфейс удобнее, чем сырые регистры. Таковым является, например, JTAG.
PowerPC
Также известен как: ppc, ppc64, power isa, PowerPC64
PowerPC - это RISC архитектура, созданная совместно Motorolla, IBM и Apple (AIM). Она основана на другой, более ранней POWER, использовавшейся на RS/6000, и проектировалась с учетом совместимости. Работать может как в 32, так и в 64 битном режиме. Для 64 битного режима используется название PowerPC64
, его мы и будем рассматривать.
Еще одной особенностью является то, что это гарвардская архитектура, то есть для инструкций и данных используется разная память.
PowerPC определяет 3 уровня ISA:
Хотел бы я сказать, что это аналогия колец защиты x86, но это не так. IUSA определяет набор основных инструкций, которые можно использовать для написать программ, VEA (грубо говоря) дополняет это все кэшами, виртуальной памятью, атомарностью и т.д., а OEA - уже больше для системных программистов, так как там описывается модель исключений и прерываний, привилегированные инструкции.
У PowerPC 3 уровня привилегий, причем для определения уровня необходимо использовать 2 флага из регистра MSR
(Machine State Register):
PR
(Problem State)HV
(Hypervisor State)
PR/HV |
0 |
1 |
---|---|---|
0 |
Privileged |
Hypervisor |
1 |
User |
User |
Уровни привилегий выстроены следующим образом, с увеличением привилегий:
User
Privileged
Hypervisor
Как можно догадаться, User - режим пользователя, а вот остальное - это уже привилегированный режим. Причем Hypervisor можно отнести к уровню самой ОС.
Когда возникает исключение, то оно обрабатывается соответствующим обработчиком. При этом, уровень привилегий повышается: PR
всегда сбрасывается (то есть переход в привилегированный режим), а HV
выставляется в зависимости от окружения.
Теперь, приступим к отладке.
Программные точки останова
Программные точки останова реализуются с помощью инструкции td
- Trap Double Word. Она занимает 4 байта (это RISC) и при ее срабатывании генерируется прерывание Program interrupt.
Ее особенность заключается в том, что она условная, то есть существует возможность добавить условие ее срабатывания. Ее сигнатура td TO, RA, RB
. RA
и RB
- это номера регистров общего назначения (просто их числа), которые сравниваются, а TO
- это битовая маска условия срабатывания (меньше, равно, больше и т.д.). Из примера документации: td 0x2, 3, 4
- если регистр 3 (R3
) больше (0x2
) регистра 4 (R4
), то будет сгенерирован Program interrupt.
Также существует и другая версия этой инструкции - tdi
, Trap Double Word Immediate. Отличие от td
в том, что вместо регистра RB
используется константное число (16 битное).
Для 32 битного режима используются инструкции
tw
иtwi
(Word вместо Double Word).
Но Program interrupt
может срабатывать и из-за других причин, например, при выполнении недопустимой инструкции (Illegal instruction). Для того, чтобы их различать, используется регистр MSR
(Machine Status Register). Диапазон 4-48 записан как зарезервированный, но на самом деле он используется для записи дополнительной информации при возникновении исключений. Для Trap
исключения выставляется бит 46.
Посмотрим как в Linux обрабатывается это исключение:
Program Interrupt
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/include/asm/reg.h#L800 */
#define SRR1_PROGTRAP 0x00020000 /* Trap */
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/traps.c#L1626 */
DEFINE_INTERRUPT_HANDLER(program_check_exception)
{
do_program_check(regs);
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/traps.c#L593 */
#define REASON_TRAP SRR1_PROGTRAP
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/traps.c#L1479 */
static void do_program_check(struct pt_regs *regs)
{
unsigned int reason = get_reason(regs);
if (reason & REASON_TRAP) {
/* ... */
/* User mode considers other cases after enabling IRQs */
if (!user_mode(regs)) {
_exception(SIGTRAP, regs, TRAP_BRKPT, regs->nip);
return;
}
}
/* ... */
if (reason & REASON_TRAP) {
/* ... */
_exception(SIGTRAP, regs, TRAP_BRKPT, regs->nip);
return;
}
/* ... */
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/traps.c#L368 */
void _exception(int signr, struct pt_regs *regs, int code, unsigned long addr)
{
/* ... */
force_sig_fault(signr, code, (void __user *)addr);
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/kernel/signal.c#L1733 */
int force_sig_fault(int sig, int code, void __user *addr)
{
return force_sig_fault_to_task(sig, code, addr, current);
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/kernel/signal.c#L1720 */
int force_sig_fault_to_task(int sig, int code, void __user *addr,
struct task_struct *t)
{
struct kernel_siginfo info;
clear_siginfo(&info);
info.si_signo = sig;
info.si_errno = 0;
info.si_code = code;
info.si_addr = addr;
return force_sig_info_to_task(&info, t, HANDLER_CURRENT);
}
Если посмотрите на код, то можете заметить, что для маски
Trap
используется0x00020000
. Но если выставить в 64 битном числе выставить 46 бит в 1, то будет не то же самое. Причина в том, что в документации все биты записываются с конца. Если мы выставим бит 18 (64 - 46), то получим указанное значение.
Шаги
Реализация шагов аналогична предыдущим архитектурам. В регистре MSR
имеется флаг SE
- Single Step Trace Enable (бит 53). Если он выставлен, то при выполнении следующей инструкции будет сгенерирован Trace interrupt
.
Trace interrupt
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/traps.c#L1150 */
DEFINE_INTERRUPT_HANDLER(single_step_exception)
{
__single_step_exception(regs);
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/traps.c#L583 */
#define clear_single_step(regs) (regs_set_return_msr((regs), (regs)->msr & ~MSR_SE))
#define clear_br_trace(regs) (regs_set_return_msr((regs), (regs)->msr & ~MSR_BE))
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/traps.c#L1133 */
static void __single_step_exception(struct pt_regs *regs)
{
clear_single_step(regs);
clear_br_trace(regs);
/* ... */
_exception(SIGTRAP, regs, TRAP_TRACE, regs->nip);
}
Также существует флаг BE
- Branch Trace Enable. Он занимает бит 54 (рядом с SE
). Если он выставлен, то Trap interrupt будет сгенерирован при выполнении очередной инструкции ветвления.
Аппаратные точки останова
Наконец, аппаратные точки останова.
Во-первых, они не обязательны и в реализации могут отсутствовать. Во-вторых, для инструкций и данных используется 2 разных регистра. Причина этого - гарвардская архитектура, инструкции и данные хранятся отдельно. В-третьих, поддерживается только 1 такая точка останова.
Для данных используется регистр DABR
- Data Address Breakpoint Register. Для PowerPC64 он имеет размер 64 бита. В нем имеется 3 флага и поле адреса. Адрес - 60 бит, то есть записываемые адреса необходимо выравнивать, а остальное - это флаги условий активации (чтение/запись).
Здесь тоже есть отличие от предыдущих архитектур. Если ранее мы самостоятельно (в коде отладчика) выставляли эти регистры, то сейчас это делается с помощью вызова ptrace
. Еще одна причина почему ptrace
- это швейцарский нож, так потому что его функциональность может меняться в зависимости от машины. Если мы работаем на PowerPC и имеется расширение HWDEBUG (которое и добавляет аппаратные точки останова), то нам становятся доступны еще 3 запроса и один из них PPC_PTRACE_SETHWDEBUG
- команда для выставления аппаратной точки останова.
Соответственно, этот код располагается уже в самом ядре.
PPC_PTRACE_SETHWDEBUG
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/include/uapi/asm/ptrace.h#L199 */
#define PPC_PTRACE_GETHWDBGINFO 0x89
#define PPC_PTRACE_SETHWDEBUG 0x88
#define PPC_PTRACE_DELHWDEBUG 0x87
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/ptrace/ptrace.c#L40 */
long arch_ptrace(struct task_struct *child, long request,
unsigned long addr, unsigned long data)
{
void __user *datavp = (void __user *) data;
unsigned long __user *datalp = datavp;
switch (request) {
/* ... */
case PPC_PTRACE_SETHWDEBUG: {
struct ppc_hw_breakpoint bp_info;
if (copy_from_user(&bp_info, datavp,
sizeof(struct ppc_hw_breakpoint)))
return -EFAULT;
return ppc_set_hwdebug(child, &bp_info);
}
}
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/ptrace/ptrace-noadv.c#L193 */
long ppc_set_hwdebug(struct task_struct *child, struct ppc_hw_breakpoint *bp_info)
{
int i;
int len = 0;
struct thread_struct *thread = &child->thread;
struct perf_event *bp;
struct perf_event_attr attr;
struct arch_hw_breakpoint brk;
if (bp_info->version != 1)
return -ENOTSUPP;
/*
* We only support one data breakpoint
*/
if ((bp_info->trigger_type & PPC_BREAKPOINT_TRIGGER_RW) == 0 ||
(bp_info->trigger_type & ~PPC_BREAKPOINT_TRIGGER_RW) != 0 ||
bp_info->condition_mode != PPC_BREAKPOINT_CONDITION_NONE)
return -EINVAL;
if ((unsigned long)bp_info->addr >= TASK_SIZE)
return -EIO;
brk.address = ALIGN_DOWN(bp_info->addr, HW_BREAKPOINT_SIZE);
brk.type = HW_BRK_TYPE_TRANSLATE | HW_BRK_TYPE_PRIV_ALL;
brk.len = DABR_MAX_LEN;
brk.hw_len = DABR_MAX_LEN;
if (bp_info->trigger_type & PPC_BREAKPOINT_TRIGGER_READ)
brk.type |= HW_BRK_TYPE_READ;
if (bp_info->trigger_type & PPC_BREAKPOINT_TRIGGER_WRITE)
brk.type |= HW_BRK_TYPE_WRITE;
if (bp_info->addr_mode == PPC_BREAKPOINT_MODE_RANGE_INCLUSIVE)
len = bp_info->addr2 - bp_info->addr;
else if (bp_info->addr_mode == PPC_BREAKPOINT_MODE_EXACT)
len = 1;
else
return -EINVAL;
i = find_empty_ptrace_bp(thread);
if (i < 0)
return -ENOSPC;
/* Create a new breakpoint request if one doesn't exist already */
hw_breakpoint_init(&attr);
attr.bp_addr = (unsigned long)bp_info->addr;
attr.bp_len = len;
arch_bp_generic_fields(brk.type, &attr.bp_type);
bp = register_user_hw_breakpoint(&attr, ptrace_triggered, NULL, child);
thread->ptrace_bps[i] = bp;
if (IS_ERR(bp)) {
thread->ptrace_bps[i] = NULL;
return PTR_ERR(bp);
}
return i + 1;
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/kernel/process.c#L805 */
static inline int set_dabr(struct arch_hw_breakpoint *brk)
{
unsigned long dabr, dabrx;
dabr = brk->address | (brk->type & HW_BRK_TYPE_DABR);
dabrx = ((brk->type >> 3) & 0x7);
if (ppc_md.set_dabr)
return ppc_md.set_dabr(dabr, dabrx);
if (IS_ENABLED(CONFIG_PPC_ADV_DEBUG_REGS)) {
mtspr(SPRN_DAC1, dabr);
if (IS_ENABLED(CONFIG_PPC_47x))
isync();
return 0;
} else if (IS_ENABLED(CONFIG_PPC_BOOK3S)) {
mtspr(SPRN_DABR, dabr);
if (cpu_has_feature(CPU_FTR_DABRX))
mtspr(SPRN_DABRX, dabrx);
return 0;
} else {
return -EINVAL;
}
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/powerpc/include/asm/reg.h#L1399 */
#define mtspr(rn, v) asm volatile("mtspr " __stringify(rn) ",%0" : \
: "r" ((unsigned long)(v)) \
: "memory")
Замечания:
Существует еще 1 регистр для точек останова -
DABRX
. В ней хранится маска привилегий. Но сейчас это не важно, поэтому опустилВ конце находится сама функция, которая выставляет значения регистров. В ней используется функция
mtspr
. На самом деле это обертка над одноименной инструкцией. Она необходима для выставления значений регистров, зависящих от реализации. А так какDABR
- это регистр добавляемый в расширении, то использовать необходимо его.Существует несколько версий архитектуры и для них используются разные способы выставления точки останова, поэтому имеются различные способы ее обработки.
Но это точка останова для данных. Для инструкций используется другой регистр - IABR
(Instruction Address Breakpoint Register). В ней по аналогии хранится адрес инструкции, такой же выровненный.
Loongson
Также известна как: loongarch, godson, LA32/LA64
Последним в нашем списке рассмотрю архитектуру Loongson. Эта архитектура была создана в начале 2000-х, но уже имеет богатую историю - несколько поколений и семейств.
LoongArch - это RISC архитектура. Она состоит базовой части (Loongson Base) и множества (необязательных) расширений, например, LBT - Loongson Binary Translation (расширение для бинарной трансляции x86/ARM/MIPS).
На поверхности эта архитектура похожа на остальные: поддержка 32 и 64 битного режимов, все инструкции размером 32 бит, 3 уровня привилегий (причем уровень 0 - это уровень ядра, а 3 - пользователя), вектор прерываний и т.д.
Модель исключений тоже понятна - у каждого исключения свой номер и его обрабатывает соответствующая функция, хранящаяся в таблице.
Для отладки используется регистр DBG
. В нем хранится информация, специфичная для режима отладки. В частности, DST
- флаг нахождения в режиме отладки. Он выставляется автоматически при срабатывании Debug exception
. Также, есть несколько флагов говорящих о том, какое исключение сработало: несколько отдельных флагов для возможных исключений отладки, а также поле с кодом исключения.
Программная точка останова
За программную точку останова отвечает инструкция break #imm
. Ей на вход передается число, которое потом можно передать обработчику. А исключение, которое возникает - Breakpoint exception
.
Здесь также уже знакомая ситуация. Но вот что интересно, так это код обработки в ядре. Ранее мы видели, что отладчики передают 0 в качестве заглушки. Но вот в ядре Linux оно используется для передачи некоторой информации, а конкретно используется для механизмов kprobe.
Обработка точки останова
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/loongarch/kernel/traps.c#L703 */
asmlinkage void noinstr do_bp(struct pt_regs *regs)
{
bool user = user_mode(regs);
unsigned int opcode, bcode;
unsigned long era = exception_era(regs);
irqentry_state_t state = irqentry_enter(regs);
if (regs->csr_prmd & CSR_PRMD_PIE)
local_irq_enable();
if (__get_inst(&opcode, (u32 *)era, user))
goto out_sigsegv;
bcode = (opcode & 0x7fff);
/*
* notify the kprobe handlers, if instruction is likely to
* pertain to them.
*/
switch (bcode) {
case BRK_KDB:
if (kgdb_breakpoint_handler(regs))
goto out;
else
break;
case BRK_KPROBE_BP:
if (kprobe_breakpoint_handler(regs))
goto out;
else
break;
case BRK_KPROBE_SSTEPBP:
if (kprobe_singlestep_handler(regs))
goto out;
else
break;
case BRK_UPROBE_BP:
if (uprobe_breakpoint_handler(regs))
goto out;
else
break;
case BRK_UPROBE_XOLBP:
if (uprobe_singlestep_handler(regs))
goto out;
else
break;
default:
current->thread.trap_nr = read_csr_excode();
if (notify_die(DIE_TRAP, "Break", regs, bcode,
current->thread.trap_nr, SIGTRAP) == NOTIFY_STOP)
goto out;
else
break;
}
switch (bcode) {
case BRK_BUG:
bug_handler(regs);
break;
case BRK_DIVZERO:
die_if_kernel("Break instruction in kernel code", regs);
force_sig_fault(SIGFPE, FPE_INTDIV, (void __user *)regs->csr_era);
break;
case BRK_OVERFLOW:
die_if_kernel("Break instruction in kernel code", regs);
force_sig_fault(SIGFPE, FPE_INTOVF, (void __user *)regs->csr_era);
break;
default:
die_if_kernel("Break instruction in kernel code", regs);
force_sig_fault(SIGTRAP, TRAP_BRKPT, (void __user *)regs->csr_era);
break;
}
out:
if (regs->csr_prmd & CSR_PRMD_PIE)
local_irq_disable();
irqentry_exit(regs, state);
return;
out_sigsegv:
force_sig(SIGSEGV);
goto out;
}
Аппаратная точка останова
После программных рассмотрим аппаратные точки останова. Согласно документации их максимальное количество 14. Также для breakpoint'ов и watchpoint'ов используются разные регистры (n - номер регистра):
watchpoint -
MWPNCFG
(M - memory)breakpoint -
FWPnCFG
(F - fetch)
Выставление точек останова
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/loongarch/kernel/hw_breakpoint.c#L174 */
static int hw_breakpoint_control(struct perf_event *bp,
enum hw_breakpoint_ops ops)
{
u32 ctrl, privilege;
int i, max_slots, enable;
struct pt_regs *regs;
struct perf_event **slots;
struct arch_hw_breakpoint *info = counter_arch_bp(bp);
if (arch_check_bp_in_kernelspace(info))
privilege = CTRL_PLV0_ENABLE;
else
privilege = CTRL_PLV3_ENABLE;
/* Whether bp belongs to a task. */
if (bp->hw.target)
regs = task_pt_regs(bp->hw.target);
if (info->ctrl.type == LOONGARCH_BREAKPOINT_EXECUTE) {
/* Breakpoint */
slots = this_cpu_ptr(bp_on_reg);
max_slots = boot_cpu_data.watch_ireg_count;
} else {
/* Watchpoint */
slots = this_cpu_ptr(wp_on_reg);
max_slots = boot_cpu_data.watch_dreg_count;
}
i = hw_breakpoint_slot_setup(slots, max_slots, bp, ops);
if (WARN_ONCE(i < 0, "Can't find any breakpoint slot"))
return i;
switch (ops) {
case HW_BREAKPOINT_INSTALL:
/* Set the FWPnCFG/MWPnCFG 1~4 register. */
if (info->ctrl.type == LOONGARCH_BREAKPOINT_EXECUTE) {
write_wb_reg(CSR_CFG_ADDR, i, 0, info->address);
write_wb_reg(CSR_CFG_MASK, i, 0, info->mask);
write_wb_reg(CSR_CFG_ASID, i, 0, 0);
write_wb_reg(CSR_CFG_CTRL, i, 0, privilege);
} else {
write_wb_reg(CSR_CFG_ADDR, i, 1, info->address);
write_wb_reg(CSR_CFG_MASK, i, 1, info->mask);
write_wb_reg(CSR_CFG_ASID, i, 1, 0);
ctrl = encode_ctrl_reg(info->ctrl);
write_wb_reg(CSR_CFG_CTRL, i, 1, ctrl | privilege);
}
enable = csr_read64(LOONGARCH_CSR_CRMD);
csr_write64(CSR_CRMD_WE | enable, LOONGARCH_CSR_CRMD);
if (bp->hw.target && test_tsk_thread_flag(bp->hw.target, TIF_LOAD_WATCH))
regs->csr_prmd |= CSR_PRMD_PWE;
break;
case HW_BREAKPOINT_UNINSTALL:
/* Reset the FWPnCFG/MWPnCFG 1~4 register. */
if (info->ctrl.type == LOONGARCH_BREAKPOINT_EXECUTE) {
write_wb_reg(CSR_CFG_ADDR, i, 0, 0);
write_wb_reg(CSR_CFG_MASK, i, 0, 0);
write_wb_reg(CSR_CFG_CTRL, i, 0, 0);
write_wb_reg(CSR_CFG_ASID, i, 0, 0);
} else {
write_wb_reg(CSR_CFG_ADDR, i, 1, 0);
write_wb_reg(CSR_CFG_MASK, i, 1, 0);
write_wb_reg(CSR_CFG_CTRL, i, 1, 0);
write_wb_reg(CSR_CFG_ASID, i, 1, 0);
}
if (bp->hw.target)
regs->csr_prmd &= ~CSR_PRMD_PWE;
break;
}
return 0;
}
Шаг
Наконец-то что-то интересное - шаги. А точнее их отсутствие. Да, архитектура Loongson не поддерживает возможность выполнения только 1 инструкции. Но это не значит, что поддержки этого совсем нет.
Для обхода этого ограничения в Linux используются аппаратные точки останова. Она ставится на текущую инструкцию (на которую указывает PC). Таким образом, когда выполнение продолжится, будет мгновенно сгенерировано исключение. Последний вопрос - как обнаружить что мы запросили шаг и это не настоящая точка останова. Это сделано просто - в структуре потока хранится адрес инструкции для которой запросили single step.
Обработка шага
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/loongarch/kernel/ptrace.c#L1077 */
void user_enable_single_step(struct task_struct *task)
{
struct thread_info *ti = task_thread_info(task);
/* Выставляем точку останова */
set_single_step(task, task_pt_regs(task)->csr_era);
/* Запоминаем адрес */
task->thread.single_step = task_pt_regs(task)->csr_era;
set_ti_thread_flag(ti, TIF_SINGLESTEP);
}
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/loongarch/kernel/ptrace.c#L1035 */
static int set_single_step(struct task_struct *tsk, unsigned long addr)
{
struct perf_event *bp;
struct perf_event_attr attr;
struct arch_hw_breakpoint *info;
struct thread_struct *thread = &tsk->thread;
bp = thread->hbp_break[0];
if (!bp) {
ptrace_breakpoint_init(&attr);
attr.bp_addr = addr;
attr.bp_len = HW_BREAKPOINT_LEN_8;
attr.bp_type = HW_BREAKPOINT_X;
bp = register_user_hw_breakpoint(&attr, ptrace_triggered,
NULL, tsk);
if (IS_ERR(bp))
return PTR_ERR(bp);
thread->hbp_break[0] = bp;
} else {
int err;
attr = bp->attr;
attr.bp_addr = addr;
/* Reenable breakpoint */
attr.disabled = false;
err = modify_user_hw_breakpoint(bp, &attr);
if (unlikely(err))
return err;
csr_write64(attr.bp_addr, LOONGARCH_CSR_IB0ADDR);
}
info = counter_arch_bp(bp);
info->mask = TASK_SIZE - 1;
return 0;
}
В предыдущей секции я не показал обработчик исключения, возникающего при срабатывании точки останова. Покажу здесь, так как заметная ее часть отводится этому шагу:
Обработка точки останова
/* https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/arch/loongarch/kernel/traps.c#L787 */
asmlinkage void noinstr do_watch(struct pt_regs *regs)
{
irqentry_state_t state = irqentry_enter(regs);
if (kgdb_breakpoint_handler(regs))
goto out;
if (test_tsk_thread_flag(current, TIF_SINGLESTEP)) {
int llbit = (csr_read32(LOONGARCH_CSR_LLBCTL) & 0x1);
unsigned long pc = instruction_pointer(regs);
union loongarch_instruction *ip = (union loongarch_instruction *)pc;
if (llbit) {
/*
* When the ll-sc combo is encountered, it is regarded as an single
* instruction. So don't clear llbit and reset CSR.FWPS.Skip until
* the llsc execution is completed.
*/
csr_write32(CSR_FWPC_SKIP, LOONGARCH_CSR_FWPS);
csr_write32(CSR_LLBCTL_KLO, LOONGARCH_CSR_LLBCTL);
goto out;
}
if (pc == current->thread.single_step) {
/*
* Certain insns are occasionally not skipped when CSR.FWPS.Skip is
* set, such as fld.d/fst.d. So singlestep needs to compare whether
* the csr_era is equal to the value of singlestep which last time set.
*/
if (!is_self_loop_ins(ip, regs)) {
/*
* Check if the given instruction the target pc is equal to the
* current pc, If yes, then we should not set the CSR.FWPS.SKIP
* bit to break the original instruction stream.
*/
csr_write32(CSR_FWPC_SKIP, LOONGARCH_CSR_FWPS);
goto out;
}
}
} else {
breakpoint_handler(regs);
watchpoint_handler(regs);
}
force_sig(SIGTRAP);
out:
irqentry_exit(regs, state);
}
Заключение
Вот и подошло к концу наше путешествие по отладчикам. Подытоживая, отладчик - это слоеный пирог с протекающей начинкой абстракцией. Нельзя так Просто создать отладчик для всех: разные ЦП, особенности ОС, детали реализации рантаймов, всякие UB, различные форматы отладочных символов и т.д. Мы обязаны знать для какой системы пишем этот отладчик, чтобы знать не только как правильно изменять его состояние, но и какие баги в нем содержатся.
Есть также множество вопросов, которые я не покрыл. Например: загружаемые библиотеки, многопоточные процессы, отладка железа и ОС реального времени, функциональность отладки для разных ЯП (например, точки останова при исключении в C++).
Здесь без особого вывода - каждый сделает его сам.
Ссылки:
How debuggers work - серия статей, с которой начал изучение
Writing a Linux Debugger - статьи по написанию отладчика в Linux. Отсюда взяли идею реализации step over добавлением точек останова на все строки
Learning about debuggers - сборник статей по отладчикам
Dumbugger - моя реализация отладчика
dokwork
Честно сказать, я уже и не надеялся встреить на хабре подобные основательные статьи. Спасибо огромное за проделанную работу!