TL;DR: мы реализовали GDB сервер для ESP8266, который позволяет просматривать стек вызовов и осуществлять ограниченную отладку.
Друзья часто спрашивают меня: какого это, разрабатывать ПО для встраиваемых платформ. Обычно при это они имеют ввиду ограниченность ресурсов (размер хранилища выполняемого кода, количество доступной памяти и т.д.) и иногда то, что для такой разработки необходимы специфические навыки.
Однако в наши дни, когда восприятие встраиваемых платформ изменилось под влиянием носимых устройств, которые могут без проблем эмулировать мой первый персональный компьютер и при этом у них остаются ресурсы, что бы воспроизводить в фоне музыку, легко забыть о проблемах, с которыми каждыми день сталкивается разработчик встраиваемого ПО.
Cesanta разрабатывает платформу, которая снижает порог вхождения в “магический” мир встраиваемого программирования, но при разработке этой платформы, мы еще сильнее сталкиваемся с проблемами, которые мешают остальным.
Сегодня я бы хотел поговорить об инструментах разработки, а именно, о той части, которая предоставляется почти любым окружением для разработки; то, что так легко принять за должное: трассировка стека вызовов.
Я не большой поклонник отладчиков. Более того, я предпочитаю использовать для отладки printf. По моему мнению, во многих случаях подключение отладчика приносит больше хлопот, чем практической пользы.
Последнее десятилетие я писал и поддерживал системы на C/C++/Python/Java/Scala/Go и я использовал отладчик только в очень редких случаях. И даже тогда отладчик использовался лишь как средство понять цепочку вызовов, которая привёла к данному сбою.
Так как для большинства языков стек вызовов это встроенная функциональность, такие ситуации, в основном, случались при отладке С/C++ программ.
Возвращаясь в сегодняшний день: затейте встроенную разработку, выберите ESP8266 в качестве платформы, оставьте меня без отладчика и я окажусь в затруднительном положении.
Внезапно это и случилось:
- Отладочный вывод — слишком дорогое удовольствие. Единственный интерфейс с устройством это подключению по последовательному порту, который помимо этого используется что бы делать что-то ещё.
- Медленный цикл «редактироване/компиляция/перепрошивка/запуск» заставляет молиться, что бы на этот раз карты легли как надо.
- Память просто заканчивается, если написать слишком много логирующего кода.
И я осознал, что если я хочу что-то закончить, мне нужен этот чёртов стек.
Для начала: почему у нас вообще возникла эта проблема? Что за история с ESP8266?
ESP8266
ESP8266 это чертовски крутой кусок кремния; и его крутейшая часть это цена: можно получить полнофункциональную плату за $3.60. И не нужно дорогих плат для разработки, можно использовать прямо так, с минимальным набором инструментов.
Этот продукт начал свой путь как WiFi модуль, который понимает AT команды через последовательный порт. Просто подключи к Arduino — и можно начинать.
Но использование в качестве дочерней платы, это лишь начало истории. Плата сама по себе имеет 32 битный процессор, 32Kb IRAM, 80Kb DRAM и 512Кб (и более) флеша. Достаточно быстро Espressif начала распространять SDK, который позволяет писать программы непосредственно для устройства. Возможности устройства превосходят таковые у хорошо известных AVR при сопоставимом потреблении энергии.
Каким же образом, малоизвестная китайская компания всего этого добилась? Я не могу судить о качестве ASIC и радио модуля, но они кажутся достаточно неплохими для моего неопытного взгляда. А вот качество SDK и документации заставляет удивиться.
Ядром ESP8266EX является очень интересный процессор Xtensa, разработанный Tensilica. Tensilica была куплена Cadence в 2013 году. Эта компания наиболее известна своими конфигурируемыми процессорными ядрами.
И они предоставляют отличные инструменты, такие как компилятор, отладчик, эмулятор…
Проблема в том, что Espressif не может поставлять эти инструменты вместе со своим SDK. Более того, даже если купить Xtensa SDK непосредственно у Cadence, точные параметры, использованные при производстве ESP8266EX можно определить только по куче сгенерированных файлов. Трудно даже понять, возможно ли всё это отревёрсить, пока не закончилась триальная лицензия Xtensa SDK. И даже если это возможно, то игра стоит свеч только если ESP8266 — это всё что вас интересует.
И пока Espressif использует инструменты Xtensa для сборки своих бинарных библиотек, наиболее подходящий выбор для пользователя это использование порта GCC. Конкретная архитектура используемая в ESP8266 называется lx106.
Конфигурируемый характер платформы Xtensa подразумевает, что фактический набор фич (включая набор инструкций) различных устройств построенных на базе Xtensa CPU весьма различен. Это сильно усложняет переиспользование инструментов и понимание того, как вообще это всё должно работать.
Первой особенностью lx106 является то, что эта конфигурация не использует одну из важнейших функций Xtensa: окно регистров. Это оказывает серьезное влияние на соглашение о вызовах используемое ESP8266 и как следствие на всех инструментах.
Существует активно развивающийся порт GCC поддерживаемый Max Filippov, доступный на гитхабе. Он пока далёк от совершенства, но он развивается семимильными шагами. Снимаю шляпу перед Максом за его преданность сообществу.
Существующие же варианты отладки ESP8266 достаточно скромны.
Вы можете попробовать on-chip-debugger (либо посредством xt-ocd от Xtensa или посредством openocd, но это требует JTAG подключения и ESP с достаточным количеством разведённых пинов (т. е. такой подход не будет работать на ESP-01).
Qemu порт пока находится в зачаточном состоянии.
Просмотр стека вызовов
Если всё, что нам нужно, это иметь возможность трассировки стека, то самым простым способом могла бы быть реализация данной функциональности прямо в кода, т.е. что-то вроде libunwind. К сожалению, не существует порта подобной библиотеки для lx106. Более того, учитывая, что большая часть кода компилируется без frame pointer, реализация такой библиотеки разумного размера будет…. скажем так, сложной.
GDB решает проблему анализируя код, инструкцию за инструкцией, находя пролог функции, откатывая изменения стека и т.д. Что если попробовать скормить GDB содержимое памяти и позволить ему выполнить тяжёлую работу?
Протокол GDB сервера
GDB поддерживает удалённую отладку посредством простого текстового протокола. Он может работать по сети или последовательному порту. Простое описание протокола есть тут.
Две основные команды, которые мы должны поддерживать это:
- 'g' — выгрузить содержимое регистров
- 'm' — прочитать пачку байтов из памяти
Таким образом, всё что нам нужно сделать это написать код, который выполняется если происходит исключение, выяснить состояние регистров и реализовать протокол GDB по последовательному порту.
Получаем управление
Сначала я попробовал напрямую изменить низкоуровневый вектор исключений в Xtensa CPU, но безуспешно.
Потом я обратил внимание, на то что в скрипте линковщика упоминается функция _xtos_set_exception_handler. XTOS это очень тонкая прослойка, предоставляемая Xtensa SDK.
Выяснилось, что _xtos_set_exception_handler позволяет зарегистрировать С-функцию, которая будет вызвана, если произойдет заданное исключение.
ICACHE_FLASH_ATTR void gdb_init() {
char causes[] = {EXCCAUSE_ILLEGAL, EXCCAUSE_INSTR_ERROR,
EXCCAUSE_LOAD_STORE_ERROR, EXCCAUSE_DIVIDE_BY_ZERO,
EXCCAUSE_UNALIGNED, EXCCAUSE_INSTR_PROHIBITED,
EXCCAUSE_LOAD_PROHIBITED, EXCCAUSE_STORE_PROHIBITED};
int i;
for (i = 0; i < (int) sizeof(causes); i++) {
_xtos_set_exception_handler(causes[i], gdb_exception_handler);
}
}
Низкоуровневый обработчик исключений сохраняет состояние регистров в структуре на стеке и вызывает С-обработчик, передавая ему адрес этой структуры в качестве параметра.
Поскольку Xtensa параметризируемый, в документации не так просто понять, что относится к чему. Документация Xtensa очень общая и хотя многое можно понять из кода, доступного для других конфигураций Xtensa, нельзя быть уверенным, что это применимо к lx106.
В итоге я запутался, и решил просто записывать определённые значения в регистры, что бы увидеть где они в конечном итоге появятся. Мне удалось найти регистры а2-а16, но а1 (указатель стека) кажется затирался содержимым а0 (адрес возврата).
Позднее я нашёл пару ссылок, которые подтвердили мои догадки и объяснили потерю регистра a1.
Ну и собирая всё вместе:
struct xtos_saved_regs {
uint32_t pc; /* instruction causing the trap */
uint32_t ps;
uint32_t sar;
uint32_t vpri; /* current xtos virtual priority */
uint32_t a0; /* when __XTENSA_CALL0_ABI__ is true */
uint32_t a[16]; /* a2 - a15 */
};
Регистр LITBASE отсутствует, но кажется, низкоуровневый обработчик исключений не изменяет его и поэтому можно просто отдать GDB его текущее значение.
Ключевая фишка здесь в том, что несмотря на отсутствие указателя стека, его можно вычислить по адресу структуры xtos_saved_regs, переданную в обработчик С. Это на 256 байт ниже указателя стека.
Теперь, мы можем просто отключить прерывания и ждать запросов от GDB
/* The user should detach and let gdb do the talkin' */
ICACHE_FLASH_ATTR void gdb_server() {
printf("waiting for gdbn");
/*
* polling since we cannot wait for interrupts inside
* an interrupt handler of unknown level.
*
* Interrupts disabled so that the user (or v7 prompt)
* uart interrupt handler doesn't interfere.
*/
xthal_set_intenable(0);
for (;;) {
int ch = gdb_read_uart();
if (ch != -1) gdb_handle_char(ch);
}
}
Общение с GDB
Теперь нужно понять какой формат ответа на команду 'g' ожидает GDB.
Это зависит от конкретного GDB. Нам необходимо использовать порт под lx106.
Описание регистров можно найти в файле gdb/regformats/reg-xtensa.dat вот тут.
Из него мы получаем:
struct regfile {
uint32_t a[16];
uint32_t pc;
uint32_t sar;
uint32_t litbase;
uint32_t sr176;
uint32_t sr208;
uint32_t ps;
};
Есть пара менее интересных технических тонкостей относительно протокола GDB и безопасного доступа к памяти (часть памяти не доступна для побайтовой адресации), но в целом это всё.
Посмотрим что получилось:
#0 0x40242557 in crash (v7=<optimized out>, this_obj=18445899648779419648, args=18446462599806581592) at user/v7_esp.c:371
#1 0x4023c321 in i_eval_call (v7=v7@entry=0x3fff5c28, a=a@entry=0x3fff96f0, pos=pos@entry=0x3ffffe94, scope=<optimized out>,
this_object=<error reading variable: can't compute CFA for this frame>, is_constructor=<optimized out>, is_constructor@entry=0) at user/v7.c:9977
#2 0x40239962 in i_eval_expr (v7=0x3fff5c28, v7@entry=<error reading variable: can't compute CFA for this frame>, a=0x3fff96f0,
a@entry=<error reading variable: can't compute CFA for this frame>, pos=0x3ffffe94, pos@entry=<error reading variable: can't compute CFA for this frame>,scope=<optimized out>) at user/v7.c:9595
#3 0x4023bcf0 in i_eval_stmt (v7=<error reading variable: can't compute CFA for this frame>, a=<error reading variable: can't compute CFA for this frame>,
pos=<error reading variable: can't compute CFA for this frame>, pos@entry=0x3ffffe94, scope=<optimized out>, brk=<optimized out>,brk@entry=0x3ffffe90) at user/v7.c:10487
#4 0x4023bd4a in i_eval_stmts (v7=<error reading variable: can't compute CFA for this frame>, a=<error reading variable: can't compute CFA for this frame>, pos=0x3ffffe94, pos@entry=<error reading variable: can't compute CFA for this frame>, end=15, scope=<optimized out>, brk=<error reading variable: can't compute CFA for this frame>) at user/v7.c:10053
#5 0x4023b104 in i_eval_stmt (v7=<optimized out>, a=a@entry=0x3fff96f0, pos=pos@entry=0x3ffffe94, scope=<optimized out>, brk=<optimized out>, brk@entry=0x3ffffe90) at user/v7.c:10088
#6 0x4024140a in v7_exec_with (v7=<optimized out>, res=res@entry=0x3fffff30, src=<optimized out>, w=<optimized out>) at user/v7.c:10607
#7 0x4024148a in v7_exec (v7=<optimized out>, res=res@entry=0x3fffff30, src=<optimized out>) at user/v7.c:10631
#8 0x402421c4 in process_js (cmd=<optimized out>) at user/v7_cmd.c:66
#9 0x4024234a in process_command (cmd=cmd@entry=0x3ffebc14 <recv_buf$3591> "crash()") at user/v7_cmd.c:128
#10 0x402423f7 in process_prompt_char (symb=<optimized out>) at user/v7_cmd.c:163
#11 0x40244a59 in rx_task (events=<optimized out>) at user/v7_uart.c:151
#12 0x40000f49 in ?? ()
#13 0x40000f49 in ??
Это трейс стека кода собранного с -Og -g3
Работа с -Os пока не возможна. Также обратите внимание на “error reading variable: can't compute CFA for this frame”. Кажется lx106 GDB требует некоторых доработок (ну или я что-то упустил).
Примечание: Проблема с CFA была исправлена в gdb 7.9.1, доступного в ветке «lx106-g++-1.21.0» этого репозитория. Спасибо Ангусу за указание на то, что стоит попробовать новый gdb. Он, однако, не исправляет проблемы с -Os.
Если я найду время, я продолжу работу и добавлю возможность устанавливать точки останова и возобновлять выполнение после исключения, но текущая реализация решает насущную проблему: отображение стека вызовов. Я надеюсь вам это будет полезно.
Исходные тексты (GPLv2) лежат здесь.
А инструкции по использованию тут.
Наслаждайтесь.
Автор: athree