Данная заметка может оказаться полезной для людей, который пишут bare-metal код и используют ThreadX в своих задачах (по собственному выбору или по навязыванию SDK). Проблема в том, что что бы эффективно отлаживать код под ThreadX или другую многопоточную операционную систему нужно иметь возможность видеть эти самые потоки, иметь возможность посмотреть стек-трейс, состояние регистров для каждого потока.
OpenOCD (Open On Chip Debugger) заявляет поддержку ThreadX, но не сильно явно оговаривает её широту. А штатно, на момент написания статьи, в версии 0.8.0, это всего два ядра: Cortex M3 и Cortex R4. Мне же, волею судеб, пришлось работать с чипом Cypress FX3 который построен на базе ядра ARM926E-JS.
Под катом рассмотрим что нужно сделать, что бы добавить поддержку вашей версии ThreadX для вашего CPU. Акцент делается на ARM, но, чисто теоретически, вполне может подойти и для других процессоров. Кроме того, рассматривается случай, когда доступа к исходникам ThreadX нет и не предвидится.
С первых строк сразу же огорчу: без ассемблера никуда. Нет, писать на нём нам не придётся, но читать код — да.
Начнём со знакомства с реализацией поддержки ThreadX в OpenOCD. Это всего один файл: src/rtos/ThreadX.c.
Поддерживаемая система описывается структурой ThreadX_params, которая содержит информацию о имени таргета, «ширины» указателя в байтах, набор смещений в структуре TX_THREAD до необходимых служебных полей, а так же информацию о том, как сохраняется контекст потока при переключении (т.н. stacking info). Сами поддерживаемые системы регистрируются при помощи массива ThreadX_params_list.
Со всеми параметрами, кроме последнего, проблем нет: ширина указателя обычно равна разрядности процессора, смещения считаются ручками (и то, почти всегда они неизменны).
Интересный вопрос: откуда брать информацию по стекингу? А ведь информации там немало:
Вот последнее и является самым сложным и непонятным. Могу сразу убедить — стандартного подхода тут нет. Методом тыка эти значения подобрать крайне сложно, а то и невозможно.
Более того, забегая вперёд, как оказалось для ядер Cortex M3/R4 используется одна схема стекинга, а для ARM926E-JS — две! Всё ради экономии.
Кратко (а так же очень грубо и неточно), как работает шедулер в ThreadX: он одновременно обеспечивает кооперативный и вытесняющий подход к организации многозадачности.
Кооперативный подход работает для потоков одинакового приоритета которым не задан слайс времени (0). Т.е. если поток А и Б имеют одинаковый приоритет, поток А начал работу, то поток Б не получит управление пока А:
Если слайс времени задан, то по его завершению поток будет прерван и управление передастся другому следующему в состоянии Ready (для случая, когда поток засыпает, но не выработал свой слайс, так же сработает кооперативный подход). Здесь уже работает вытесняющий подход. Для его работы нужен таймер и прерывания от него с определённой периодичностью. Так же поток А из примера выше, может быть вытеснен потоком В, если его приоритет выше.
Понятно, что контекст потока сохраняется когда он передаёт управление кому-то и восстанавливается, когда он получает управление. Поймём как это происходит — поймём что нужно описывать в массиве смещений регистров.
Не буду вдаваться в подробности, как я выяснял где и как спрятались основные части планировщика, много тут понамешалось: и смекалка, и удача, и гугл, и дизассемблер. Но приведу основные компоненты оного:
Я изучал листинги всех этих функций, но если снова потребуется прикручивать поддержку для неподдерживаемого процессора, я буду акцентировать на последних трёх. Но начну с последней, и только после этого (если не хватит информации) буду изучать другие.
Посмотрим на её листинг (некоторую косвенную адресацию я заменил на реальные символы, сами символы
смотрятся в elf-файле при помощи arm-none-eabi-nm):
Функция до безумства простая:
А вот начиная с 40004cb8 идёт код который, собственно и восстанавливает контекст нового потока.
Сначала вычитываются два значения в регистры r0, r1:
Далее идёт сравнение r0 с нулём:
Очевидно, что эти значения, по крайней мере r0, часть контекста (ведь стековый регистр уже настроен на стек восстанавливаемого треда), но не совсем похоже, что это регистры. А сравнение с нулём подразумевает какое-то ветвление. Продолжая анализ, видим, что если r0 != 0, то выполняется код:
Собственно говоря это и похоже на восстановление контекста. Причём значение регистра r1 — это сохранённое значение статусного регистра CPSR. Если строчка 40004ccc выполнится, то управление дальше не пойдёт: восстановится регистр pc (r15) и программа после этой точки вернётся в то место, откуда она была прервана.
Отлично, теперь мы можем написать такую табличку:
Каждый регистр и каждый флаг — 32 бит или 4 байта, соответственно на этот контекст нужно 17*4 = 68 байт. Логично, что дальше идёт стек, каким он был на момент прерывания.
Но, как видим, это часть работы. У нас есть этот самый флаг. И если его значение 0, то выполняется код:
Судя по всему, это тоже контекст, только несколько сокращённый. Более того, возврат из него происходит как из обычной функции, а не восстановлением регистра pc. Переписав табличку выше, получаем:
Для этого контекста нужно всего 11*4 = 44 байта.
Пользуясь гуглом, просмотром листингов дизассемблера, а так же изучением соглашений по вызову процедур приходим к пониманию, что этот тип контекста используется когда работает кооперативная многозадачность: т.е. когда мы вызвали tx_thread_sleep() или иже с ними. А т.к. такое переключение, по сути, просто вызов функции, то и контекст можно сохранять согласно соглашениям о вызовах, по которому, мы имеем право между вызовами не сохранять значения регистров r0-r3, r12. Более того, нам не нужно сохранять pc — вся необходимая информация уже содержится в rl — адресе возврата из tx_thread_sleep(). Выгода на лицо. Кортексы обычно используются на системах с большим количеством памяти, нежели ARM9E, там к подобных ухищрениям не прибегают и используют один тип стекинга.
По информации из интернета накопал, что первый тип контекста называется interrupt, и используется когда поток прерывается прерыванием, сиречь может быть прерван в любом месте, поэтому необходимо сохранять все возможные регистры. Второй тип контекста называется solicited и используется когда тред прерывается по системному вызову, который приводит к решедулингу.
Вот собственно всё и готово, что бы понять какие переделки нужны в OpenOCD:
Код для первого пункта я приводит не буду, посмотрите патч. Для пункта два немного поясню как составлять табличку смещений понятных OpenOCD.
Первым делом смотрим вывод команды 'info registers', смотрим сколько регистров и в каком порядке выводится, составляем такую рыбу:
Здесь 32 — битность регистра. Для ARM всегда 32. Первая колонка заполняется при помощи табличек, которые мы записали выше, когда анализировали восстановление контекста. Учитываем специальные значения: -1 — данный регистр не сохраняется, -2 — стековый регистр, восстанавливается из структуры потока.
Заполненная рыба для solicited контекста получается такой:
Для interrupt контекста попробуйте написать сами или посмотрите в исходники.
Что это даст:
команды даны для gdb.
В общем, счастливой отладки!
Ресурсы:
PS не хватает хаба «Обратная разработка» и подсветки для разных ассемблеров ;-)
UPD /2015-08-15/: Изменения попали в основную ветку OpenOCD: openocd.zylin.com/#/c/2848
OpenOCD (Open On Chip Debugger) заявляет поддержку ThreadX, но не сильно явно оговаривает её широту. А штатно, на момент написания статьи, в версии 0.8.0, это всего два ядра: Cortex M3 и Cortex R4. Мне же, волею судеб, пришлось работать с чипом Cypress FX3 который построен на базе ядра ARM926E-JS.
Под катом рассмотрим что нужно сделать, что бы добавить поддержку вашей версии ThreadX для вашего CPU. Акцент делается на ARM, но, чисто теоретически, вполне может подойти и для других процессоров. Кроме того, рассматривается случай, когда доступа к исходникам ThreadX нет и не предвидится.
С первых строк сразу же огорчу: без ассемблера никуда. Нет, писать на нём нам не придётся, но читать код — да.
Начнём со знакомства с реализацией поддержки ThreadX в OpenOCD. Это всего один файл: src/rtos/ThreadX.c.
Поддерживаемая система описывается структурой ThreadX_params, которая содержит информацию о имени таргета, «ширины» указателя в байтах, набор смещений в структуре TX_THREAD до необходимых служебных полей, а так же информацию о том, как сохраняется контекст потока при переключении (т.н. stacking info). Сами поддерживаемые системы регистрируются при помощи массива ThreadX_params_list.
Со всеми параметрами, кроме последнего, проблем нет: ширина указателя обычно равна разрядности процессора, смещения считаются ручками (и то, почти всегда они неизменны).
Интересный вопрос: откуда брать информацию по стекингу? А ведь информации там немало:
- направление роста стека (ну это просто)
- число регистров в системе (это тоже просто, запускаем на имеющейся версии OpenOCD «info registers» и считаем число строк).
- выравнивание фрейма на стеке, это значение я получил случайно, для Cortex M3/R4 оно указано 8 байт, для ARM926E-JS — 0 (т.е. без выравнивания). На самом деле выравнивание по 4, но память выделенная при помощи tx_byte_alloc() уже выровнена, а использование стека всегда кратно 4. В общем, попробуйте значения 0, 4 и так далее.
- массив смещений в стеке (относительно текущей вершины) по которым лежат значения конкретных регистров (размер массива равен числу регистров в выводе «info registers»).
Вот последнее и является самым сложным и непонятным. Могу сразу убедить — стандартного подхода тут нет. Методом тыка эти значения подобрать крайне сложно, а то и невозможно.
Более того, забегая вперёд, как оказалось для ядер Cortex M3/R4 используется одна схема стекинга, а для ARM926E-JS — две! Всё ради экономии.
Кратко (а так же очень грубо и неточно), как работает шедулер в ThreadX: он одновременно обеспечивает кооперативный и вытесняющий подход к организации многозадачности.
Кооперативный подход работает для потоков одинакового приоритета которым не задан слайс времени (0). Т.е. если поток А и Б имеют одинаковый приоритет, поток А начал работу, то поток Б не получит управление пока А:
- не завершится
- не вызовет функции, которая приводит к решедулингу (sleep, ожидание на очереди, мутексе, семафоре и т.д.)
Если слайс времени задан, то по его завершению поток будет прерван и управление передастся другому следующему в состоянии Ready (для случая, когда поток засыпает, но не выработал свой слайс, так же сработает кооперативный подход). Здесь уже работает вытесняющий подход. Для его работы нужен таймер и прерывания от него с определённой периодичностью. Так же поток А из примера выше, может быть вытеснен потоком В, если его приоритет выше.
Понятно, что контекст потока сохраняется когда он передаёт управление кому-то и восстанавливается, когда он получает управление. Поймём как это происходит — поймём что нужно описывать в массиве смещений регистров.
Не буду вдаваться в подробности, как я выяснял где и как спрятались основные части планировщика, много тут понамешалось: и смекалка, и удача, и гугл, и дизассемблер. Но приведу основные компоненты оного:
- _tx_timer_interrupt() — функция вызывается из контекста прерывания таймера, по сути отвечает за вытесняющую часть планировщика.
- _tx_thread_context_save() (или _tx_thread_vectored_context_save()) и _tx_thread_context_restore() — пара функций предназначенных для вызова из прерываний для сохранения и восстановления контекста. При восстановлении контекста производится попытка решедулинга.
- _tx_thread_system_return() — часть кооперативного подхода. Вызывается в конце любой цепочки вызовов, которая приводит к решедулингу.
- и, наконец, _tx_thread_schedule() — самая важная функция для анализа и, пожалуй самая простая из всех вышеперечисленных.
Я изучал листинги всех этих функций, но если снова потребуется прикручивать поддержку для неподдерживаемого процессора, я буду акцентировать на последних трёх. Но начну с последней, и только после этого (если не хватит информации) буду изучать другие.
Посмотрим на её листинг (некоторую косвенную адресацию я заменил на реальные символы, сами символы
смотрятся в elf-файле при помощи arm-none-eabi-nm):
40004c7c <_tx_thread_schedule>:
40004c7c: e10f2000 mrs r2, CPSR
40004c80: e3c20080 bic r0, r2, #128 ; 0x80
40004c84: e12ff000 msr CPSR_fsxc, r0
40004c88: e59f104c ldr r1, [pc, #76] ; 40004cdc <_tx_thread_schedule+0x60>
40004c8c: e5910000 ldr r0, [r1]
40004c90: e3500000 cmp r0, #0
40004c94: 0afffffc beq 40004c8c <_tx_thread_schedule+0x10>
40004c98: e12ff002 msr CPSR_fsxc, r2
40004c9c: e59f103c ldr r1, [pc, #60] ; 40004ce0 <_tx_thread_schedule+0x64>
40004ca0: e5810000 str r0, [r1]
40004ca4: e5902004 ldr r2, [r0, #4]
40004ca8: e5903018 ldr r3, [r0, #24]
40004cac: e2822001 add r2, r2, #1
40004cb0: e5802004 str r2, [r0, #4]
40004cb4: e59f2028 ldr r2, [pc, #40] ; 40004ce4 <_tx_thread_schedule+0x68>
40004cb8: e590d008 ldr sp, [r0, #8]
40004cbc: e5823000 str r3, [r2]
40004cc0: e8bd0003 pop {r0, r1}
40004cc4: e3500000 cmp r0, #0
40004cc8: 116ff001 msrne SPSR_fsxc, r1
40004ccc: 18fddfff ldmne sp!, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip, lr, pc}^
40004cd0: e8bd4ff0 pop {r4, r5, r6, r7, r8, r9, sl, fp, lr}
40004cd4: e12ff001 msr CPSR_fsxc, r1
40004cd8: e12fff1e bx lr
40004cdc: 4004b754 .word 0x4004b754 ; _tx_thread_execute_ptr
40004ce0: 4004b750 .word 0x4004b750 ; _tx_thread_current_ptr
40004ce4: 4004b778 .word 0x4004b778 ; _tx_timer_time_slice
Функция до безумства простая:
- разрешить прерывания (строки 40004c7c-40004c84)
- дождаться, что кто-то взведёт _tx_thread_execute_ptr (40004c88-40004c94) — следующий для исполнения тред
- запретить прерывания, а точнее — восстановить статусный регистр (40004c98)
- сохранить указатель _tx_thread_current_ptr в r0 (40004c9c-40004ca0)
- увеличить значение tx_thread_run_count текущего треда на 1 (40004ca4, 40004cac-40004cb0)
- получить значение tx_thread_time_slice текущего треда и присвоить его _tx_timer_time_slice (40004ca8, 40004cb4, 40004cbc)
- установить новый указатель на стек, сохранённый в структуре потока (прочитать tx_thread_stack_ptr) (40004cb8)
А вот начиная с 40004cb8 идёт код который, собственно и восстанавливает контекст нового потока.
Сначала вычитываются два значения в регистры r0, r1:
40004cc0: e8bd0003 pop {r0, r1}
Далее идёт сравнение r0 с нулём:
40004cc4: e3500000 cmp r0, #0
Очевидно, что эти значения, по крайней мере r0, часть контекста (ведь стековый регистр уже настроен на стек восстанавливаемого треда), но не совсем похоже, что это регистры. А сравнение с нулём подразумевает какое-то ветвление. Продолжая анализ, видим, что если r0 != 0, то выполняется код:
40004cc8: 116ff001 msrne SPSR_fsxc, r1
40004ccc: 18fddfff ldmne sp!, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip, lr, pc}^
Собственно говоря это и похоже на восстановление контекста. Причём значение регистра r1 — это сохранённое значение статусного регистра CPSR. Если строчка 40004ccc выполнится, то управление дальше не пойдёт: восстановится регистр pc (r15) и программа после этой точки вернётся в то место, откуда она была прервана.
Отлично, теперь мы можем написать такую табличку:
Смещение Регистр -------- ------- 0 флаг 4 CPSR 8 r0 12 r1 16 r2 20 r3 24 r4 28 r5 32 r6 36 r7 40 r8 44 r9 48 sl (r10) 52 fp (r11) 56 ip (r12) 60 lr (r14) 64 pc (r15)
Каждый регистр и каждый флаг — 32 бит или 4 байта, соответственно на этот контекст нужно 17*4 = 68 байт. Логично, что дальше идёт стек, каким он был на момент прерывания.
Но, как видим, это часть работы. У нас есть этот самый флаг. И если его значение 0, то выполняется код:
40004cd0: e8bd4ff0 pop {r4, r5, r6, r7, r8, r9, sl, fp, lr}
40004cd4: e12ff001 msr CPSR_fsxc, r1
40004cd8: e12fff1e bx lr
Судя по всему, это тоже контекст, только несколько сокращённый. Более того, возврат из него происходит как из обычной функции, а не восстановлением регистра pc. Переписав табличку выше, получаем:
Смещение Регистр -------- ------- 0 флаг 4 CPSR 8 r4 12 r5 16 r6 20 r7 24 r8 28 r9 32 sl (r10) 36 fp (r11) 40 lr (r14)
Для этого контекста нужно всего 11*4 = 44 байта.
Пользуясь гуглом, просмотром листингов дизассемблера, а так же изучением соглашений по вызову процедур приходим к пониманию, что этот тип контекста используется когда работает кооперативная многозадачность: т.е. когда мы вызвали tx_thread_sleep() или иже с ними. А т.к. такое переключение, по сути, просто вызов функции, то и контекст можно сохранять согласно соглашениям о вызовах, по которому, мы имеем право между вызовами не сохранять значения регистров r0-r3, r12. Более того, нам не нужно сохранять pc — вся необходимая информация уже содержится в rl — адресе возврата из tx_thread_sleep(). Выгода на лицо. Кортексы обычно используются на системах с большим количеством памяти, нежели ARM9E, там к подобных ухищрениям не прибегают и используют один тип стекинга.
По информации из интернета накопал, что первый тип контекста называется interrupt, и используется когда поток прерывается прерыванием, сиречь может быть прерван в любом месте, поэтому необходимо сохранять все возможные регистры. Второй тип контекста называется solicited и используется когда тред прерывается по системному вызову, который приводит к решедулингу.
Вот собственно всё и готово, что бы понять какие переделки нужны в OpenOCD:
- нужно доработать механизм регистрации таргетов, что бы была возможность использовать несколько вариантов стекинга для одного таргета;
- собственно составить описание таргета.
Код для первого пункта я приводит не буду, посмотрите патч. Для пункта два немного поясню как составлять табличку смещений понятных OpenOCD.
Первым делом смотрим вывод команды 'info registers', смотрим сколько регистров и в каком порядке выводится, составляем такую рыбу:
static const struct stack_register_offset rtos_threadx_arm926ejs_stack_offsets_solicited[] = {
{ , 32 }, /* r0 */
{ , 32 }, /* r1 */
{ , 32 }, /* r2 */q
{ , 32 }, /* r3 */
{ , 32 }, /* r4 */
{ , 32 }, /* r5 */
{ , 32 }, /* r6 */
{ , 32 }, /* r7 */
{ , 32 }, /* r8 */
{ , 32 }, /* r9 */
{ , 32 }, /* r10 */
{ , 32 }, /* r11 */
{ , 32 }, /* r12 */
{ , 32 }, /* sp (r13) */
{ , 32 }, /* lr (r14) */
{ , 32 }, /* pc (r15) */
{ , 32 }, /* xPSR */
};
Здесь 32 — битность регистра. Для ARM всегда 32. Первая колонка заполняется при помощи табличек, которые мы записали выше, когда анализировали восстановление контекста. Учитываем специальные значения: -1 — данный регистр не сохраняется, -2 — стековый регистр, восстанавливается из структуры потока.
Заполненная рыба для solicited контекста получается такой:
static const struct stack_register_offset rtos_threadx_arm926ejs_stack_offsets_solicited[] = {
{ -1, 32 }, /* r0 */
{ -1, 32 }, /* r1 */
{ -1, 32 }, /* r2 */
{ -1, 32 }, /* r3 */
{ 8, 32 }, /* r4 */
{ 12, 32 }, /* r5 */
{ 16, 32 }, /* r6 */
{ 20, 32 }, /* r7 */
{ 24, 32 }, /* r8 */
{ 28, 32 }, /* r9 */
{ 32, 32 }, /* r10 */
{ 36, 32 }, /* r11 */
{ -1, 32 }, /* r12 */
{ -2, 32 }, /* sp (r13) */
{ 40, 32 }, /* lr (r14) */
{ -1, 32 }, /* pc (r15) */
{ 4, 32 }, /* xPSR */
};
Для interrupt контекста попробуйте написать сами или посмотрите в исходники.
Что это даст:
- вывод списка потоков по «info threads»
- стектрейс индивидуально для потока: «thread apply all bt»
- переключением между потоками: «thread 3»
- переключением между фреймами: «frame 5»
- индивидуальный просмотр состояния регистров каждого потока
команды даны для gdb.
В общем, счастливой отладки!
Ресурсы:
- Патч: ThreadX-arm926ejs.diff
- Сборка под Win32/64, пропатченные исходники, патч, вспомогательные скрипты: openocd-0.8.0-20150206-win.tar.xz
- Дискуссия на форуме Cypress: www.cypress.com/?app=forum&id=167&rID=106353
- Список рассылки OpenOCD: sourceforge.net/p/openocd/mailman/message/33287429
PS не хватает хаба «Обратная разработка» и подсветки для разных ассемблеров ;-)
UPD /2015-08-15/: Изменения попали в основную ветку OpenOCD: openocd.zylin.com/#/c/2848