Одна маленькая загадка про Cortex-M

в 9:59, , рубрики: микроконтроллеры, Программинг микроконтроллеров
Нам представилась возможность провести небольшое, но крайне поучительное тактическое занятие

На днях, в прцессе портирования FreeRTOS на микроконтроллер с ядром Cortex-M1, о котором я уже писал, возник маленький вопрос, который совершенно неожиденно яростно сопротивлялся всем попыткам найти на него ответ при помощи ГУГЛА всемогущего. Причем в процессе поиска выяснилось, что этот вопрос интересовал не меня одного, а, значит, не может быть следствием врожденной (либо приобретенной) тупости вопрошающего, ну или, в крайнем случае, свидетельствует, что таковая встречается не столь уж редко. Слегка озадаченный невозможностью применить обычный способ поиска ответов, решил прибегнуть к более экзотическому и слегка забытому — подумать и найти ответ самостоятельно. К сожалению, тоже не получилось, равно как не помогла и попытка проконсультироваться с другими неглупыми людьми (сам себя не похвалишь — весь день ходишь как оплеванный). Поскольку на Хабре таковых должно быть в избытке, попробуем экстенсивный путь решения путем вовлечения в этот процесс еще большего количества специалистов. Поэтому вместо победного поста пишу пост жалобный — помогите, люди добрые, кто чем может. Итак, переходим к сути проблемы.

В процессе переключения задачи возникает необходимость сохранения и последующего восстановления контекста процесса. Очевидно, что этот процесс является аппаратно-зависимым, и в процессе портирования к нему должно быть особое внимание. Поскольку за основу бралось решение для M0, на архитектуру M1, которая является подмножеством вышеуказанной, все встало без проблем. Тем не менее решил посмотреть коды данного участка, чтобы получить немного экспы. И вот тут меня ждала некоторая неожиданность, а именно: код мне показался замысловатым, поскольку вместо ожидаемых команд PUSH имелась следующая картина:

xPortPendSVHandler:
; сохраняем контекст текущей задачи - комментарий мой
	mrs r0, psp
	ldr	r3, =pxCurrentTCB	/* Get the location of the current TCB. */
	ldr	r2, [r3]
	subs r0, r0, #32		/* Make space for the remaining low registers. */
	str r0, [r2]			/* Save the new top of stack. */

	stmia r0!, {r4-r7}		/* Store the low registers that are not saved automatically. */
	mov r4, r8				/* Store the high registers. */
	mov r5, r9							
	mov r6, r10							
	mov r7, r11							
	stmia r0!, {r4-r7}
; определяем номер задачи, на которую переключимся
	push {r3, r14}						
	cpsid i								
	bl vTaskSwitchContext				
	cpsie i
	pop {r2, r3}			/* lr goes in r3. r2 now holds tcb pointer. */
; восстанавливаем ее контекст и запускаем
	ldr r1, [r2]						
	ldr r0, [r1]			/* The first item in pxCurrentTCB is the task top of stack. */
	adds r0, r0, #16		/* Move to the high registers. */
	ldmia r0!, {r4-r7}		/* Pop the high registers. */
	mov r8, r4							
	mov r9, r5							
	mov r10, r6							
	mov r11, r7																		
	msr psp, r0				/* Remember the new top of stack for the task. */											
	subs r0, r0, #32		/* Go back for the low registers that are not automatically restored. */
	ldmia r0!, {r4-r7}		/* Pop low registers.  */
	bx r3		
vPortSVCHandler;	
...		
vPortStartFirstTask		
...

Кстати, пользуясь случаем, еще до разбора собственно вопроса, хотел бы проклясть авторов этого кода. Обратите внимание, что три метки записаны в разном формате — с двоеточием в конце, без двоеточия в конце (что допускается описанием языка) и без двоеточия, но с точкой с запятой, открывающей отсутствующий комментарий. Если учесть, что в последнем случае метка еще и переопределялась директивой препроцессора, это мне стоило некоторого времени в попытке понять, почему сделано именно так. Ответ «потому что» был найден довольно-таки быстро и удовольствия не принес. Далее, в первой и четвертой строке кода вычисляют значение, которое в пятой строке отправляют по адресу, вычисляемому во второй и третьей строке. Ну зачем разрывать вычисление значения вычислением адреса? С одной стороны, отрадно, что пренебрежение стилем имеет международный характер, а не является нашей национальной особенностью, с другой стороны, оптимизма не добавляет. Вспоминается классическое «Не стоит искать злой умысел в том, что можно объяснить обычной глупостью». Но это так, лирическое отступление на тему яркости солнца и зелености травы. Вернемся собственно к задаче.
Как нетрудно видеть, сохранение части контекста процесса, а именно регистров r4-r11, происходит в строках с 7 по 12, причем с использованием индексной множественной пересылки (остальная часть контекста, регистры r0-r3 и r12-r15, была сохранена в процессе обработки исключения. Почему же используются не команда PUSH, а команда длинной пересылки, причем с пересылками регистр-регистр (команда длинной пересылки работает не дальше регистра r7). Ну во-первых, к сожалению, команда PUSH в архитектуре M работает тоже недалеко, так что пересылок не избежать, но все равно было бы намного понятнее происходящее. Вот тут то и порылась собака.
Дело в том, что в М архитектуре существуют два режима работы — Threat (назовем его пользовательским) и Handler (назовем его системным). Такие названия вполне соответствуют духу, поскольку режим Handler включается для обработки прерывания, которая свойственна именно системному уровню. Есть еще привилегированный и непривилегированный режимы, но в M1 их все равно нет (они неразличимы). Далее, в архитектуре М существуют два указателя стека, MAIN (назовем его системным) и Process (назовем его пользовательским). Данное именование тоже вполне оправданно, поскольку после сброса используется MAIN указатель, а это явно уровень системы. При этом оба указателя имеют уникальные имена в пространстве специальных регистров, MSP и PSP соответственно, что использовано в первой строке кода. Помимо уникальных имен, для доступа к указателю стека есть и регистр (внезапно) указателя стека, который показывает нам один из вышеперечисленных двух под управлением бита в специальном регистре (за подробностями обращайтесь к документации ARM). Пока все выглядит логично, смотрим далее.
В пользовательском режиме МК возможно переключение этого бита и, соответственно, доступ к обоим указателям стека. Ну лично я бы такого права этому режиму не дал во избежание, но кто я такой, чтобы спорить с фирмой ARM, проехали. А вот в системном режиме МК имеет доступ ТОЛЬКО к системному указателю стека и не может переключить значение этого бита. Поэтому он не может напрямую писать в пользовательский стек через команды обращения к стеку. При этом, конечно, остается возможность обращения к соответствующей области памяти через регистровое индексирование, что и делается в подпрограмме, но у меня возник вопрос «Почему так сделано»?.. Почему пользовательскому режиму разрешают переключать указатели и, возможно, выстрелить себе в ногу путем краха системного стека, а системному режиму, который должен быть спроектирован более тщательно специально обученными людьми, в такой возможности отказано? Если бы такое разрешение было бы дано обоим режимам, не было бы вопроса — разработчики не посчитали нужным делать защиту, это их право. НО для системного режима эта возможность сознательна запрещена, значит есть часть аппаратуры, за этот запрет отвечающая. Конечно, эта часть не слишком сложна и я сам могу предлоюить пару простеньких вариантов, но она не могла появиться сама собой. Значит, есть основания так делать, только я их не понимаю. Покрутил в голове варианты, связанные со вложенными прерываниями, ничего не придумал. К сожалению, на сайте ARM ответа не нашел, там пишут о том, КАК работает эта часть МК, а ПОЧЕМУ не сказано (может это сакральное знание и, получив его, можно научиться создавать архитектуры не хуже ARMовских). С тайной надеждой, что все именно так и выношу данный вопрос на суд Хабра-сообщества, жду Ваших вариантов ответа.

Автор: GarryC

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js