- PVSM.RU - https://www.pvsm.ru -
Данная заметка может оказаться полезной для людей, который пишут bare-metal код и используют ThreadX в своих задачах (по собственному выбору или по навязыванию SDK). Проблема в том, что что бы эффективно отлаживать код под ThreadX или другую многопоточную операционную систему нужно иметь возможность видеть эти самые потоки, иметь возможность посмотреть стек-трейс, состояние регистров для каждого потока.
OpenOCD (Open On Chip Debugger [1]) заявляет поддержку ThreadX [2], но не сильно явно оговаривает её широту. А штатно, на момент написания статьи, в версии 0.8.0, это всего два ядра: Cortex M3 и Cortex R4. Мне же, волею судеб, пришлось работать с чипом Cypress FX3 который построен на базе ядра ARM926E-JS.
Под катом рассмотрим что нужно сделать, что бы добавить поддержку вашей версии ThreadX для вашего CPU. Акцент делается на ARM, но, чисто теоретически, вполне может подойти и для других процессоров. Кроме того, рассматривается случай, когда доступа к исходникам ThreadX нет и не предвидится.
С первых строк сразу же огорчу: без ассемблера никуда. Нет, писать на нём нам не придётся, но читать код — да.
Начнём со знакомства с реализацией поддержки ThreadX в OpenOCD. Это всего один файл: src/rtos/ThreadX.c [3].
Поддерживаемая система описывается структурой ThreadX_params, которая содержит информацию о имени таргета, «ширины» указателя в байтах, набор смещений в структуре TX_THREAD до необходимых служебных полей, а так же информацию о том, как сохраняется контекст потока при переключении (т.н. stacking info). Сами поддерживаемые системы регистрируются при помощи массива ThreadX_params_list.
Со всеми параметрами, кроме последнего, проблем нет: ширина указателя обычно равна разрядности процессора, смещения считаются ручками (и то, почти всегда они неизменны).
Интересный вопрос: откуда брать информацию по стекингу? А ведь информации там немало:
Вот последнее и является самым сложным и непонятным. Могу сразу убедить — стандартного подхода тут нет. Методом тыка эти значения подобрать крайне сложно, а то и невозможно.
Более того, забегая вперёд, как оказалось для ядер Cortex M3/R4 используется одна схема стекинга, а для ARM926E-JS — две! Всё ради экономии.
Кратко (а так же очень грубо и неточно), как работает шедулер в ThreadX: он одновременно обеспечивает кооперативный и вытесняющий подход к организации многозадачности.
Кооперативный подход работает для потоков одинакового приоритета которым не задан слайс времени (0). Т.е. если поток А и Б имеют одинаковый приоритет, поток А начал работу, то поток Б не получит управление пока А:
Если слайс времени задан, то по его завершению поток будет прерван и управление передастся другому следующему в состоянии Ready (для случая, когда поток засыпает, но не выработал свой слайс, так же сработает кооперативный подход). Здесь уже работает вытесняющий подход. Для его работы нужен таймер и прерывания от него с определённой периодичностью. Так же поток А из примера выше, может быть вытеснен потоком В, если его приоритет выше.
Понятно, что контекст потока сохраняется когда он передаёт управление кому-то и восстанавливается, когда он получает управление. Поймём как это происходит — поймём что нужно описывать в массиве смещений регистров.
Не буду вдаваться в подробности, как я выяснял где и как спрятались основные части планировщика, много тут понамешалось: и смекалка, и удача, и гугл, и дизассемблер. Но приведу основные компоненты оного:
Я изучал листинги всех этих функций, но если снова потребуется прикручивать поддержку для неподдерживаемого процессора, я буду акцентировать на последних трёх. Но начну с последней, и только после этого (если не хватит информации) буду изучать другие.
Посмотрим на её листинг (некоторую косвенную адресацию я заменил на реальные символы, сами символы
смотрятся в 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
Функция до безумства простая:
А вот начиная с 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 контекста попробуйте написать сами или посмотрите в исходники.
Что это даст:
команды даны для gdb.
В общем, счастливой отладки!
Ресурсы:
PS не хватает хаба «Обратная разработка» и подсветки для разных ассемблеров ;-)
Автор: monah_tuk
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/82006
Ссылки в тексте:
[1] Open On Chip Debugger: http://openocd.sourceforge.net/
[2] заявляет поддержку ThreadX: http://openocd.sourceforge.net/doc/html/GDB-and-OpenOCD.html#gdbrtossupport
[3] src/rtos/ThreadX.c: https://github.com/olerem/openocd/blob/master/src/rtos/ThreadX.c
[4] ThreadX-arm926ejs.diff: https://gist.github.com/h4tr3d/1b973f79d21313365847
[5] openocd-0.8.0-20150206-win.tar.xz: https://mega.co.nz/#!igNmwTzR!5FAhh7_TcEsWlGKvW82Cz9Ggl4OhmBZBeV7O8sE9n_g
[6] www.cypress.com/?app=forum&id=167&rID=106353: http://www.cypress.com/?app=forum&id=167&rID=106353
[7] sourceforge.net/p/openocd/mailman/message/33287429/: http://sourceforge.net/p/openocd/mailman/message/33287429/
[8] Источник: http://habrahabr.ru/post/249991/
Нажмите здесь для печати.