Программирование под ARM TrustZone. Часть 1: Secure Monitor
Статья Андрея Волкова, руководитель направления доверенной платформы компании "Аладдин Р.Д."
Продолжаем наш цикл статей, посвящённый столетию Великой Октябрьской… ARM TrustZone.
- Secure/Non-Secure — это режим процессора. Он задаётся битом NS (Non-Secure) в регистре SCR (Secure Configuration Register). Если NS=1, мы в режиме Non-Secure, если NS=0, мы в доверенном, то есть Secure-режиме.
С точки зрения программной реализации это все, что нам нужно. Аппаратное обеспечение находится по ту сторону абстракции, наблюдая только за NS, а программное обеспечение не только исполняется по-разному при NS=0 и 1, но и может этот бит менять.
В обоих режимах (NS=0 и NS=1) процессор может полноценно работать, настолько, что в каждом режиме может существовать своя ОС:- NS=0: доверенная ОС или Trusted OS, или Trusted Execution Environment (TEE);
- NS=1: гостевая ОС, или Rich OS, или Normal World OS.
У каждой ОС будут своя карта виртуальной памяти, свои приложения, прерывания, драйверы и так далее.
Конечно, не всегда у нас на ARM крутятся две операционки. Доверенный код может быть и не полноценной ОС, а каким-то маленьким монитором безопасности. Или может полностью отсутствовать. Но в смартфонах и планшетах отсутствие доверенной ОС – редкость, там в основном есть и TEE (доверенная ОС) и обычная ОС (например, Android). Только не принимайте за чистую монету название TEE, Trusted Execution Environment. Если TEE называется доверенной – значит, кто-то её коду доверяет, потому что код ведёт к достижению его целей. Может быть, его цель – уничтожить Вселенную, как знать? Вы же исходников не видите.Процесс загрузки
Процессоры всегда стартуют в режиме Secure. Есть много процессоров ARMv7A, где Security Extensions отключены. И тогда они всегда работают как Secure. Например, всеми любимая Sitara.
Но в любом случае – процессоры всегда стартуют в режиме Secure. Первым в процессе загрузки участвует загрузчик, и в случае с TrustZone используется одна из реализаций идеи Trusted Boot – механизма, проверяющего подпись образа перед его запуском. Общий алгоритм тут таков:- считать в память образ загрузчика с внешнего носителя, например, SD, eMMC, NAND, QSPI;
- проверить его подпись открытым ключом, прошитым в процессор на этапе производства изделия;
- если подпись верна, передать управление загрузчику.
Открытый ключ для проверки подписи в процессор прошивается однократно, и после этого только первичный загрузчик, подписанный закрытой частью этого ключа, может быть запущен. Здесь есть и поле для злоупотреблений со стороны крупных производителей.
Подробнее о загрузке ARM – в этой статье. Далее загрузчик проверит подпись доверенной ОС (TEE) и запустит TEE. Та инициализирует все, что нужно в TrustZone, покидает режим Secure и передаст управление гостевой ОС (например, Linux).
Что мы хотим понять в этой статье
На картинке изображены две ОС, которые мы только что загрузили. Гостевая ОС может вызывать функции TEE, для этого она использует Secure Monitor.

Режимы процессора
В ARMv7A есть довольно много режимов работы. На картинке они разделены на уровни PL0, PL1, PL2 и некоторые из уровней Secure, а некоторые – Non-secure.

Особые режимы процессора
На рисунке выше была ещё пара режимов:
- Non-Secure Hypervisor (HYP), PL2;
- Secure Monitor (SMC), PL1.
Режим HYP используется для аппаратной виртуализации, как в VMWare. Он находится на уровне PL2, – он даже главнее ядра гостевой ОС и может там все разрешить и запретить, прямо как TEE. Но мы совсем не будем в этой статье говорить про виртуализацию по двум причинам: во-первых, мало процессоров ARMv7 и софта с её поддержкой, во-вторых, от Virtualization Extensions в ARM все становится ещё запутанней. Так что лучше оставить виртуализацию пока в стороне.
А вот режим Secure Monitor нам очень нужен, он сделан для переключения между Secure и Non-Secure OC. Давайте его рассмотрим со всех сторон.Secure Monitor
У нас есть две полноценные ОС, и глобально отличаются они только битом NS:
- Secure OS (TEE), NS=0;
- Non-Secure OS (гостевая, например, Linux), NS=1.
Ведь логично, что гостевая ОС не может поменять себе бит NS и получить привилегии Secure? Абсолютно логично. Менее ожидаемо, что и Secure OS не может вот так взять и переключиться в режим Non-Secure, поменяв NS на 1. Но это тоже так.
Дело в том, что переключение между режимами оказалось несколько сложнее, чем один бит поменять:- Для переключения между режимами нужно ещё и сохранить/восстановить контекст. Почти все регистры у Secure и Non-secure общие, и их нужно сохранять и восстанавливать.
- Кроме того, обращение из Normal World в Secure World нужно, чтобы сделать какую-то операцию, а у операции обычно есть параметры и возвращаемое значение. Это тоже нужно учитывать.
Вот для этого и придумали режим Secure Monitor. Попадают туда с помощью вызова “SMC #0“, что расшифровывается как Secure Monitor call. Причём и Secure код должен вызывать “SMC #0“, чтобы переключиться в Non-Secure. И Non-Secure в Secure перепрыгивает также.
#0 – просто атавизм: сначала в ARM хотели передавать код вызова через этот параметр, но потом отказались от этой идеи и используют R0 как номер вызова. В целом, вызов SMC подобен системному вызову операционной системы (SVC):- системный вызов SVC позволяет приложению ОС из непривилегированного режима (PL0) вызвать функцию ОС (PL1);
- вызов монитора SMC позволяет коду гостевой ОС (Non-Secure PL1) вызвать функцию TEE (Secure PL1).
Различие заключается в том, что возврат из системного вызова осуществляется не так, как сам вызов, а вот переход между Secure и Non-Secure – симметрично, через SMC #0.
Три особенности режима Secure Monitor позволяют ему выполнять переключение контекста Secure/Non-Secure.- У него есть свой собственный стек, относящийся к области памяти Secure. Стек доступен сразу по входу в режим Secure Monitor, и в него можно сразу сохранить все регистры (контекст) вызывающей стороны, причём неважно, какой.
- В режиме Secure Monitor мы можем изменять бит NS, как нам заблагорассудится.
- Меняя бит NS в режиме Secure Monitor, мы можем видеть регистры и периферию то из режима Secure, то из режима Non-Secure. NS при этом будет реально меняться, и это отразится на работе всей аппаратной обвязки. Однако все это будет в рамках выполнения одной последовательной подпрограммы. Благодаря этому Secure Monitor может подготовить все необходимое для переключения контекста.
Пример вызова TEE
Например, мы хотим, чтобы TEE нам подписала какой-то документ. Данные о документе мы положим в регистры процессора, например, так:
- R0 – код операции: подписать документ в памяти;
- R1 – начальный адрес документа в памяти Normal World (помним, что представление памяти в Secure и Non-Secure отличается);
- R2 – длина документа;
- R3 – начальный адрес буфера, куда упадёт подпись. Считаем, что, если буфера не хватит, это не проблема TEE.
Мы вызываем SMC #0 для вызова TEE. В ответ мы ожидаем от TEE подпись в указанном буфере и код результата в регистре R0, чтобы понять – успешно прошла операция или нет.
То есть, налицо некий протокол обмена между гостевой ОС и TEE. В ARM в общем случае можно вести себя как угодно и придумывать любые концепции обмена, но есть принятый всеми механизм обмена, описанный в ARM SMC calling convention. Там описано, какие регистры используются для передачи кода команды, данных, для возвращаемых значений и так далее.Что делает Secure Monitor
Начнём с того, что код инициализации TEE записывает адрес точки входа в Secure Monitor (адрес подпрограммы) в таблицу векторов исключений режима Monitor, на которую указывает регистр MVBAR.
Регистр MVBAR доступен только в режиме Secure и указывает на особую таблицу векторов исключений, используемую только при переходе в режим Secure Monitor. У ARM есть и обычная таблица векторов, в которой указаны точки входа в SVC, IRQ, FIQ и так далее. Эта таблица расположена по умолчанию по адресу 0x00000000, но адрес может быть настроен регистром VBAR. Разумеется, для работы двух ОС там предусмотрено два регистра: Secure VBAR и Non-secure VBAR. Какой из них доступен, зависит от бита NS. Так вот, MVBAR используется не для SVC, IRQ и так далее, а только для SMC и пары исключений, которые можно настроить на попадание в Monitor Mode. Например, мы можем настроить Abort и FIQ на попадание в Secure Monitor, и благодаря этому перехватывать эти исключения. При инициализации TEE также устанавливает адрес головы стека для Secure Monitor, и you’re all set, как говорят за океаном. Пример реализации Secure Monitor можете посмотреть в исходниках OP-TEE, код действительно несложный: https://github.com/OP-TEE/optee_os/blob/master/core/arch/arm/sm/sm_a32.S. Посмотрим теперь, что произойдёт при вызове команды SMC #0 из гостевой ОС.- Управление перейдёт на адрес, указанный в таблице MVBAR – на подпрограмму Secure Monitor. При этом
- режим выполнения будет уже SMC;
- в регистр SPSR (Saved Program Status Register) запишется CPSR (Current Program Status Register) вызывающего кода, включая режим, в котором тот был: SVC, IRQ или что-то другое;
- в регистр LR (Link Register) запишется адрес, откуда произошел вызов.
- SPSR и LR пригодятся, чтобы потом осуществить возврат из вызова, поэтому они записываются в контекст вызывающей стороны. Пока это можно сделать только в стек Secure Monitor.
srsdb sp!, #CPSR_MODE_MON // Записать на стек LR и SPSR
- Потом нужно разобраться, с какой стороны растёт на пнях мох вызвали SMC – Secure или Non-Secure. Для этого читаем SCR и проверяем бит NS. Если NS=1, значит нас вызвали из Non-Secure. В нашем примере это так, и мы переключаемся в Secure. Ставим NS=0.
- Сохраняем контекст:
- сохраняем все оставшиеся регистры в стек, а r0-r7 там уже лежат;
- туда же сохраняются регистры CPSR других режимов процессора (IRQ, FIQ и т.п.);
- копируем содержимое стека в контекст Non-Secure.
- Восстанавливаем контекст Secure:
- восстанавливаем регистры CPSR всех режимов;
- загружаем содержимое регистров из контекста Secure;
- загружаем точку входа (будущие PC и CPSR) на стек Secure Monitor.
- Выпрыгиваем из режима Secure Monitor, считав PC и CPSR со стека:
rfefd sp! // rfe=return from exception
Вот очень упрощённое описание того, что вы увидите в приведённом выше по ссылке коде. Там операции даже не все выполняются в таком же порядке. Цель была передать общий смысл, не более того.
На самом деле, это почти все, что делает Secure Monitor – передаёт управление Secure OS. Когда Secure OS закончит с вызовом, она так же вызовет SMC #0. Secure Monitor поймет по NS=0, что сейчас он Secure, и нужно возвращаться в Non-Secure, и сделает те же команды, но немного наоборот. Если вы полезли разбираться с кодом, то вот ещё хинт: Secure Monitor определяет по регистру R0 вызывавшей стороны, что за вызов. Тут могут быть два варианта, описанные в ARM SMC calling convention.- Standard Call – вызов, требующий создания в TEE потока для его обработки. Например, обращение к функции доверенного приложения, или запуск вообще любой функции TEE, которая потребует ожидания, блокировок, семафоров и т.д.
- Fast Call – быстрый вызов TEE, который всего перечисленного выше не требует. Например, мы просим TEE включить для нас ещё пару ядер процессора.
Fast Call – это как прерывание, он обязательно возвратит управление достаточно быстро. Standard Call – как RPC, после его вызова TEE начинает работать на полную катушку, выполнять разные операции, переключать контексты, может быть и ждать результатов операции.
В принципе, Secure Monitor мог бы и оставить эту проверку на TEE и сразу переключить туда, но тут такая реализация. Важно не запутаться в этом коде и увидеть, что оба вызова исполняются в режиме Secure Supervisor, а не в Secure Monitor. Если вы рассматриваете код OP-TEE, все вызовы из Non-Secure World попадают на обработку в Secure, а Secure Monitor сам ничего не обрабатывает. В OP-TEE он работает как привратник. UPDATE: Уважаемый @lorc уточняет, что в ARM Trusted Firmware реализация Secure Monitor не только переключает режимы, он ещё и исполняет ряд системных функций, например, управление питанием. Смотрите его комментарий.
Обработка прерываний и исключений
Гостевая ОС, как правило, не очень-то и знает, что она гостевая. Настраивает себе память, прерывания, выполняет задачи. Все работает как надо, пока не налетит на какое-то ограничение, наложенное на неё TEE. Если налетит – произойдёт Abort, как мы и писали в прошлой статье.
При этом гостевая ОС загрузит кучу драйверов, будет назначать устройствам прерывания и прерывания эти будут приходить в гостевую ОС. А почему, спрашивается, в неё? Вполне может быть, что TEE хочет управлять какими-то устройствами в монопольном режиме и получать от них свои прерывания. Сейчас мы разберёмся, как две операционки делят между собой прерывания. В процессоре ARM основной контроллер прерываний – один (пусть это GICv2), нет отдельных контроллеров для Secure и Non-Secure. Если происходит прерывание, то GICv2 по умолчанию доставит его в режим Secure. Тогда, если произошло прерывание – будет загружен вектор из Secure VBAR. Но если мы запускаем TEE и Linux параллельно, то нужно как-то поделить прерывания. Не дело, если все прерывания будут приходить только в TEE (Secure) или в Linux (Non-secure). Поэтому в GICv2 в рамках поддержки Security Extensions придумали сделать группировку прерываний (регистр GICD_IGROUP):- Group 0 – это Secure прерывание, генерирует IRQ или FIQ, это можно настроить;
- Group 1 – Non-Secure прерывание, генерирует только IRQ.
При такой реализации можно запустить Linux без всяких TEE – и тогда он будет запущен в режиме Secure по умолчанию, настроит себе Secure VBAR, все прерывания будут идти к нему (про VBAR мы писали выше). А если Linux запущен в гостевом режиме, то TEE заранее настроит все ненужные ему прерывания на Group 1, а Linux запустит в Non-Secure режиме. Linux настроит себе Non-Secure VBAR, и все его прерывания будут идти к нему. Идиллия и программная совместимость, драйвер GIC в Linux и знать не должен, работает он в Secure или гостевом режиме.
Ну, казалось бы, все хорошо и понятно. Если произошло Secure-прерывание – будет загружен вектор из Secure VBAR, иначе Non-Secure VBAR. Так нет же! Мы помним, что просто так перейти из Secure-режима в Non-Secure нельзя, для этого у нас есть Secure Monitor. Поэтому:- если прерывание произошло в режиме Non-Secure, и оно Non-Secure, исполняется как обычно, через Non-Secure VBAR;
- если прерывание произошло в режиме Secure, и оно Secure, тоже исполняется как обычно, через Secure VBAR;
- а вот если Secure прерывание произошло в режиме Non-Secure, то происходит вызов Secure Monitor, для того, чтобы сначала перейти в режим Secure;
- про то, что происходит в паре Non-Secure->Secure, можете догадаться.
Сухой остаток – Secure-прерывание может произойти в режиме Non-Secure, и тогда оно пойдёт через Secure Monitor. Механизм его работы, описанный выше, теперь должен разобраться, не в прерывание ли он послан, и соответственно все обработать. И там все в коде у OP-TEE есть, посмотрите.
Очень полезная таблица на этот счёт есть здесь: http://infocenter.arm.com/help/topic/com.arm.doc.faqs/ka16352.html Но и это ещё не все! На самом деле, чтобы это работало, нужно ещё кое-что настроить. В регистре SCR, уже знакомом нам, есть биты, настраивающие, какие прерывания и исключения направлять в Secure Monitor, а какие – обрабатывать через VBAR.
- в GIC для всех Secure прерываний настраивается Group 0;
- для Group 0 настраивается генерация FIQ, а не IRQ;
- в регистре SCR выбирается маршрутизация FIQ в Secure Monitor, а IRQ – по VBAR.
Все эти настройки гостевая ОС уже не сможет изменить. В результате Secure-прерывание всегда генерирует FIQ, а FIQ всегда попадает в Secure Monitor из режима Non-Secure.
Вот так, ARMv7 штука сложная и иногда запутанная. Таким же образом (через регистр SCR) можно настроить и ловлю External Abort из Non-Secure-режима в Secure Monitor. Это может быть полезно, т.к. External Abort может произойти, например, при попытке доступа из режима Non-Secure к Secure-периферии.Заключение
<Описать все программирование TrustZone в одной обзорной статье не получилось, и будет продолжение.
В этот раз мы рассмотрели разделение на Secure и Normal World, поразбирались в работе Secure Monitor и узнали, как ловить прерывания в доверенной среде. В следующей статье будет про TEE: что она делает, насколько она на самом деле самостоятельная ОС, для чего нужны трастлеты, и какой у них жизненный цикл.