OpenOCD, ThreadX и ваш процессор

Данная заметка может оказаться полезной для людей, который пишут 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.

Со всеми параметрами, кроме последнего, проблем нет: ширина указателя обычно равна разрядности процессора, смещения считаются ручками (и то, почти всегда они неизменны).

Интересный вопрос: откуда брать информацию по стекингу? А ведь информации там немало:

направление роста стека (ну это просто) число регистров в системе (это тоже просто, запускаем на имеющейся версии 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; 0×80 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 0×4004b754; _tx_thread_execute_ptr 40004ce0: 4004b750 .word 0×4004b750; _tx_thread_current_ptr 40004ce4: 4004b778 .word 0×4004b778; _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.В общем, счастливой отладки!

Ресурсы:

PS не хватает хаба «Обратная разработка» и подсветки для разных ассемблеров ;-)

© Habrahabr.ru