- Viewing the Call Stack in WinDbg
- Debugger Command Window
- Calls Window
- Additional Information
- View the call stack and use the Call Stack window in the debugger
- View the call stack while in the debugger
- Display non-user code in the Call Stack window
- Switch to another stack frame (change the debugger context)
- View the source code for a function on the call stack
- Run to a specific function from the Call Stack window
- Set a breakpoint on the exit point of a function call
- Display calls to or from another thread
- Visually trace the call stack
- View the disassembly code for a function on the call stack (C#, C++, Visual Basic, F#)
- Change the optional information displayed
- Load symbols for a module (C#, C++, Visual Basic, F#)
- To load symbols
- To set the symbol path
- Последовательность вызова функций
- Пример
Viewing the Call Stack in WinDbg
The call stack is the chain of function calls that have led to the current location of the program counter. The top function on the call stack is the current function, the next function is the function that called the current function, and so on. The call stack that is displayed is based on the current program counter, unless you change the register context. For more information about how to change the register context, see Changing Contexts.
In WinDbg, you can view the call stack by entering commands or by using the Calls window.
Debugger Command Window
You can view the call stack by entering one of the k (Display Stack Backtrace) commands in the Debugger Command window.
Calls Window
As an alternative to the k command, you can view the call stack in the Calls window. To open the Calls window, choose Call Stack from the View menu.
The following screen shot shows an example of a Calls window.
Buttons in the Calls window enable you to customize the view of the call stack. To move to the corresponding call location in the Source window or Disassembly window, double-click a line of the call stack, or select a line and press ENTER. This action also changes the local context to the selected stack frame. For more information about running to or from this point, see Controlling the Target.
In user mode, the stack trace is based on the stack of the current thread. For more information about the stack of the current thread, see Controlling Processes and Threads.
In kernel mode, the stack trace is based on the current register context. You can set the register context to match a specific thread, context record, or trap frame. For more information about setting the register context, see Register Context.
The Calls window has a toolbar that contains several buttons and has a shortcut menu with additional commands. To access this menu, select and hold (or right-click) the title bar or select the icon near the upper-right corner of the window (). The toolbar and menu contain the following buttons and commands:
Raw args displays the first three parameters that are passed to the function. On an x86-based processor, this display includes the first three parameters that are passed to the function («Args to Child»).
Func info displays Frame Pointer Omission (FPO) data and other internal information about the function. This command is available only on an x86-based processor.
Source displays source module names and line numbers after the function names (if the debugger has this information).
Addrs displays various frame-related addresses. On an x86-based processor, this display includes the base pointer for the stack frame («ChildEBP») and the return address («RetAddr»).
Frame nums displays frame numbers. Frames are always numbered consecutively, beginning with zero.
Arg types displays detailed information about the arguments that are expected and received by the functions in the stack.
Always floating causes the window to remain undocked even if it is dragged to a docking location.
Move with frame causes the window to move when the WinDbg frame is moved, even if the window is undocked. For more information about docked, tabbed, and floating windows, see Positioning the Windows.
Additional Information
For more information about the register context and the local context, see Changing Contexts.
View the call stack and use the Call Stack window in the debugger
By using the Call Stack window, you can view the function or procedure calls that are currently on the stack. The Call Stack window shows the order in which methods and functions are getting called. The call stack is a good way to examine and understand the execution flow of an app.
When debugging symbols are not available for part of a call stack, the Call Stack window might not be able to display correct information for that part of the call stack, displaying instead:
[Frames below may be incorrect and/or missing, no symbols loaded for name.dll]
The Call Stack window is similar to the Debug perspective in some IDEs like Eclipse.
The dialog boxes and menu commands you see might differ from those described here, depending on your active settings or edition. To change your settings, select Import and Export Settings on the Tools menu. See Reset settings.
View the call stack while in the debugger
While debugging, in the Debug menu, select Windows > Call Stack.
A yellow arrow identifies the stack frame where the execution pointer is currently located. By default, this stack frame’s information appears in the source, Locals, Autos, Watch, and Disassembly windows. To change the debugger context to another frame on the stack, switch to another stack frame.
Display non-user code in the Call Stack window
- Right-click the Call Stack window and select Show External Code.
Non-user code is any code that is not shown when Just My Code is enabled. In managed code, non-user code frames are hidden by default. The following notation appears in place of the non-user code frames:
Switch to another stack frame (change the debugger context)
In the Call Stack window, right-click the stack frame whose code and data that you want to view.
Or, you can double-click a frame in the Call Stack window to switch to that frame.
Select Switch to Frame.
A green arrow with a curly tail appears next to the stack frame you selected. The execution pointer remains in the original frame, which is still marked with the yellow arrow. If you select Step or Continue from the Debug menu, execution will continue in the original frame, not the frame you selected.
View the source code for a function on the call stack
- In the Call Stack window, right-click the function whose source code you want to see and select Go To Source Code.
Run to a specific function from the Call Stack window
- In the Call Stack window, select the function, right-click, and then choose Run to Cursor.
Set a breakpoint on the exit point of a function call
Display calls to or from another thread
- Right-click the Call Stack window and select Include Calls To/From Other Threads.
Visually trace the call stack
In Visual Studio Enterprise (only), you can view code maps for the call stack while debugging.
In the Call Stack window, open the shortcut menu. Choose Show Call Stack on Code Map (Ctrl + Shift + `).
View the disassembly code for a function on the call stack (C#, C++, Visual Basic, F#)
- In the Call Stack window, right-click the function whose disassembly code you want to see and select Go To Disassembly.
Change the optional information displayed
- Right-click in the Call Stack window and set or clear Show .
Load symbols for a module (C#, C++, Visual Basic, F#)
In the Call Stack window, you can load debugging symbols for code that does not currently have symbols loaded. These symbols can be .NET or system symbols downloaded from the Microsoft public symbol servers, or symbols in a symbol path on the computer that you are debugging.
To load symbols
In the Call Stack window, right-click the stack frame for which symbols are not loaded. The frame will be dimmed.
Point to Load Symbols and then select Microsoft Symbol Servers (if available), or browse to the symbol path.
To set the symbol path
In the Call Stack window, choose Symbol Settings from the shortcut menu.
The Options dialog box opens and the Symbols page is displayed.
Select Symbol Settings.
In the Options dialog box, click the Folder icon.
In the Symbol file (.pdb) locations box, a cursor appears.
Enter a directory pathname to the symbol location on the computer that you are debugging. For local and remote debugging, this is a path on your local computer.
Select OK to close the Options dialog box.
Последовательность вызова функций
Многие специалисты, по роду своей деятельности сталкивающиеся с исследованием различных модулей, анализом причин тех или иных сбоев и проводящие иную аналогичную работу, в своей практике часто сталкиваются с таким понятием как стек вызовов. Очевидно, что он фактически представляет собой очередность вызова функций в контексте выполняющегося потока. С первого взгляда может показаться, что стек вызовов достаточно тривиален, тем не менее, в нем далеко не все так очевидно, как хотелось бы, и поэтому я решил набросать (прежде всего для себя самого) небольшую шпаргалку относительно некоторых важных деталей. Помимо последовательности вызова функций, так же неплохо было бы обсудить формат описания функций в стеке, да и некоторые другие нюансы. Тем, кому доводилось работать с различного рода задачами в отладчиках, к примеру, над изучением различного рода дампов памяти, или отладкой приложения, понимали, что одним из центральных аспектов является так называемый стек вызовов потока, который представляет собой список, состоящий из записей вида:
Очевидно, что приведен всего-лишь общий вид записи, но нам интересна сегодня, в первую очередь, сама структура данной записи. В приведенном выше формате отладчик выводит данные о вызываемых в ходе выполнения кода функциях. Формат записи в стеке вызовов следующий:
Как можно увидеть по рисунку, формат записи таков: имя_модуля ! имя_функции + смещение , где:
- имя_модуля — имя библиотеки (исполняемого файла), содержащей функцию;
- имя_функции — наименование (имя) функции, в составе которой размещается инструкция, вызывающая вышестоящую (следующую по стеку вызовов) функцию;
- смещение — количество байт (в шестнадцатеричной системе счисления) от адреса точки входа функции до начала инструкции, следующей за инструкцией, вызвавшей вышестоящую (по стеку вызовов) функцию. Смещение высчитывается от стартового адреса функции (от первого её байта).
В ряде ситуаций (при отсутствии символов) имя функции может отсутствовать, тогда адрес отображается не вполне корректно: имя_модуля + смещение либо имя_модуля ! имя_ближайшей_определенной_функции + смещение .
По большому счету, как раз совокупность подобных записей и составляет собой стек вызовов.
По идее, из названия следует, что стек вызовов должен представлять собой только часть (отдельные ячейки) традиционного стека потока, хранящую исключительно адреса возврата (информацию для возврата) управления в вышестоящую (вызывающую, вызвавшую) подпрограмму. Тем не менее, поскольку в современном программировании на языках высокого уровня подпрограммы (функции) оперируют стековыми фреймами, то подразумевается, что стек вызовов хранит всю информацию о подпрограммах (локальные данные, параметры, значения регистров и прч.).
То есть стек вызовов это не весь стек (потока), поскольку в последнем могут храниться «сторонние» данные (локальные переменные функций, значения регистров и прч.), а исключительно информация, относящаяся к вызываемым подпрограммам (функциям). Эти значения формируют своего рода последовательность (цепочку) вложенных вызовов подпрограмм, которая является достаточно важной частью при исследовании процесса выполнения кода потока (при различного рода сбоях), поскольку по цепочке вызовов можно определить как именно код вел себя на протяжении всего времени выполнения (до момента сбоя/контролируемого останова) и даже определить области кода, где предположительно произошла ошибка. Поэтому, для реверсивного инженера чрезвычайно важно как можно подробнее изучить последовательность вызова функций при выполнении кода.
Последовательность вызова функций, как уже говорилось, можно наблюдать в стеке вызовов. Давайте посмотрим, как именно стек вызовов выглядит в отладчике Windbg, для этого в программе присутствует класс команд k* (например: knL ). Вот типичный пример:
Чем вам не последовательность? Правда читается она достаточно своеобразно: снизу вверх. Объясняется это принципами работы самого стека: последним пришел, первым вышел (Last In First Out), или иначе первым пришел, последним вышел. То есть, самый первый вызов процедуры помещает в стек адрес возврата первым (на самую вершину стека), затем следующий вторым и так далее. В итоге первый адрес оказывается как бы «на дне» стека. Стек растет в сторону уменьшения адресов. Таким образом, со временем в стеке получается эдакая своеобразная «башня» из находящихся друг над другом адресов возврата (можно отследить по столбцу Child-SP ).
В блоке можно наблюдать столбец с именем Call site . Это обозначение можно перевести как Область вызовов , и оно отражает символическое, осмысленное имя, соответствующее адресу возврата (указанного в колонке RetAddr ), сформированное с использованием (если доступны) символов для представления более осмысленного вывода. Каждая запись области вызовов содержит в себе наименование модуля, функции и смещение, для упрощенной идентификации как самой вызывающей функции, так и входящей в ее состав инструкции. Грубо говоря она представляет собой точный указатель (адрес) на инструкцию внутри функции. Для лучшего понимания уместно провести провести аналогию с почтовым адресом: имя модуля это город, имя функции — улица, и смещение — это адрес дома. Эта абстракция заметно упрощает понимание принципов адресации той или иной инструкции в адресном пространстве исследуемого процесса.
Теперь давайте вернемся к базовым принципам. В операционной системе присутствует такое понятие как поток выполнения, в котором выполняются разнообразные операции, или, говоря простыми словами, код.
В каждом потоке имеется начальная (главная, стартовая) функция, код которой размещается с точки входа, и начинает выполняться при старте потока (приложения). Вы думаете, что этой главной функцией все в программе и ограничивается, то есть приложение состоит из единственной функции? Нет же, не существует такой одной единой, «волшебной» функции, которая выполнила бы всю необходимую любому приложению работу (исключение составляют лишь крайне простые приложения). Очевидно, что для выполнения даже самых простейших задач этой функции необходима помощь множества других внутренних или внешних (системных) функций. Из этого следует, что:
Для того, что бы не порождать хаос, наборы упомянутых функций классифицированы в разнообразные модули (файлы), называемые библиотеками и систематизированы по роду деятельности. И вот эти то самые функции постоянно вызываются выполняемым в данный момент на процессоре кодом.
Например, ваша программа хочет вывести на рабочий стол окно. Уже представили насколько сложна эта задача на уровне системы и на сколько подзадач она «подразделяется»? Вы думали, что вызовется одна единственная функция, которая спокойно нарисует окно и вернет управление? Не тут то было!! Подобная задача разбивается на множество составляющих (подзадач). Соответственно, наша начальная функция вызывает другую функцию что бы сделать какую-то часть общей работы, например указать библиотеке DirectX отрисовать окно. Функция DirectX получает управление и в ходе своей работы обращается к какой-либо другой функции, потом та, в свою очередь, обращается к следующей, что бы сделать уже требуемую именно ей на произвольном этапе выполнения, работу и так далее и так далее. Получается такая своеобразная «матрешка» из вложенных (или вызывающих) друг в друга функций.
Однако, давайте опять посмотрим на стек вызовов, который мы привели выше. Нужно помнить, что:
Но тут не все так просто, как кажется, надо понимать как именно она это делает. Дьявол, как всегда, кроется в деталях. Реализует она это не со смещения, которое указано в столбце Call site (вызовы), и не с адреса, который указан в столбце RetAddr (адрес возврата). На самом деле, нижестоящая функция просто вызывает «вышестоящую», а это означает, что управление передается в самое начало , то есть на самый первый байт вызываемой функции. Это регламентирует базовый принцип вызова функций (или подпрограмм), который испокон веков применяется в программировании. Теперь понятно, почему смещения, указанные сразу за именем модуля/функции в списке вызовов (столбец Call site), могут ввести в заблуждение?
Поэтому:
- смещение от начала модуля/функции в списке вызовов (столбец Call site) показывает исключительно смещение (в байтах, от начала) инструкции, с которой продолжится выполнение , если вышестоящая функция вернет это самое управление обратно.
- адрес возврата (столбец RetAddr) показывает адрес, куда вернется управление в нижестоящую функцию, когда текущая функция отработает и вернет управление.
Повторение — мать учения, закрепим:
Эдакие своеобразные перекрестные ссылки на соседа в цепочке вызовов. Да, в этом и заключается некоторая неинтуитивность списка, и не все так просто, как хотелось бы, но со временем вы осознаете, что все выстроено выполнено логично. И вот еще, что значит выражение «функция вернула управление»? Это означает ровно то, что функция выполнила ожидаемую от неё работу, и затем управление вернулось (чаще при помощью подкласса команд ret ) в функцию ниже по списку в стеке вызовов, которая и продолжила выполнение, и так далее.
Пример
Давайте рассмотрим конкретный пример. Я возьму произвольный файл дампа памяти, созданный при возникновении критической системной ошибки (BSOD). Открою его в отладчике Windbg, введу команду knL и получу такой вот вывод: