- PVSM.RU - https://www.pvsm.ru -
В этой статье я расскажу о некоторых идеях, на которых построены высокоуровневые части Android, о нескольких его предшественниках и о базовых механизмах обеспечения безопасности.
Статьи серии:
Говоря про Unix- и Linux-корни Android, нужно вспомнить и о других проектах операционных систем, влияние которых можно проследить в Android, хотя они и не являются его прямыми предками.
Я уже упомянул про BeOS, в наследство от которой Android достался Binder.
Plan 9 — потомок Unix, логическое продолжение, развитие его идей и доведение их до совершенства. Plan 9 был разработан в Bell Labs той же командой, которая создала Unix и C — над ним работали такие люди, как Ken Thompson, Rob Pike, Dennis Ritchie, Brian Kernighan, Tom Duff, Doug McIlroy, Bjarne Stroustrup, Bruce Ellis и другие.
В Plan 9 взаимодействие процессов между собой и с ядром системы реализовано не через многочисленные системные вызовы и механизмы IPC, а через виртуальные текстовые файлы и файловые системы (развитие принципа Unix «всё — это файл»). При этом каждая группа процессов «видит» файловую систему по-своему (пространства имён, namespaces), что позволяет запускать разные части системы в разном окружении.
Например, чтобы получить позицию курсора мыши, приложения читают текстовый файл /dev/mouse
. Оконная система rio предоставляет каждому приложению свою версию этого файла, в которой видны только события, относящиеся к окну этого приложения, и используются локальные по отношению к окну координаты. Сама rio читает события «настоящей» мыши через такой же файл /dev/mouse
— в том виде, в котором его видит она. Если она запущена напрямую, этот файл предоставляется ядром и действительно описывает движения настоящей мыши, но она может быть совершенно прозрачно запущена в качестве приложения под другой копией rio, без какой-то специальной поддержки с её стороны.
Plan 9 полностью поддерживает доступ к удалённым файловым системам (используется собственный протокол 9P, кроме того, поддерживаются FTP и SFTP), что позволяет программам совершенно прозрачно получать доступ к удалённым файлам, интерфейсам и ресурсам. Такая «родная» сетевая прозрачность превращает Plan 9 в распределённую операционную систему — пользователь может физически находиться за одним компьютером, на котором запущена rio, запускать приложения на нескольких других, использовать в них файлы, хранящиеся на файловом сервере и выполнять вычисления на CPU-сервере — всё это полностью прозрачно и без специальной поддержки со стороны каждой из частей системы.
За счёт красиво спроектированной архитектуры Plan 9 значительно проще и меньше, чем Unix — на самом деле ядро Plan 9 даже в несколько раз меньше известного микроядра Mach.
Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.
Несмотря на техническое превосходство и наличие слоя совместимости с Unix, Plan 9 не получил широкого распространения. Тем не менее, многие идеи и технологии из Plan 9 получили распространение и были реализованы в других системах. Самая известная из них — кодировка UTF-8, которая была разработана в Plan 9 для обеспечения полной поддержки Unicode при сохранении обратной совместимости с ASCII — стала общепринятым стандартом.
Больше всего идей и технологий из Plan 9 реализовано в Linux:
/proc
(procfs)clone
(аналог rfork
из Plan 9)Многое из этого используется, в том числе, и в Android. Кроме того, в Android реализован механизм intent’ов, похожий на plumber из Plan 9; о нём я расскажу в следующей статье.
Про Plan 9 можно узнать подробнее на сайте plan9.bell-labs.com [3] (сохранённая копия в Wayback Machine [4]), или его зеркале 9p.io [5]
Plan 9 получил продолжение в виде проекта Inferno, тоже разработанного в Bell Labs. К таким свойствам Plan 9, как простота и распределённость, Inferno добавляет переносимость. Программы для Inferno пишутся на высокоуровневом языке Limbo и выполняются — с использованием just-in-time компиляции — встроенной в ядро Inferno виртуальной машиной.
Inferno настолько переносим, что может запускаться
При этом приложениям, запущенным внутри Inferno, предоставляется совершенно одинаковое окружение.
Inferno получил ещё меньше распространения и известности, чем Plan 9. С другой стороны, Inferno во многом предвосхитил Android, самую популярную операционную систему на свете.
Компания Danger Research Inc. была сооснована Энди Рубином (Andy Rubin) в 1999 году, за 4 года до сооснования им же Android Inc. в 2004 году.
В 2002 году Danger выпустили свой смартфон Danger Hiptop. Многие из разработчиков Danger впоследствии работали над Android, поэтому неудивительно, что его операционная система была во многом похожа на Android. Например, в ней были реализованы:
Подробнее о Danger можно прочитать в статье Chris DeSalvo, одного из разработчиков, под названием The future that everyone forgot [7].
Хотя использование высокоуровневых языков для серьёзной разработки сейчас уже никого не удивляет, из популярных операционных систем только у Android «родной» язык — высокоуровневая Java (с другой стороны, здесь можно вспомнить веб с его JavaScript, .NET для Windows и относительно высокоуровневый — но полностью компилируемый в нативный код и не использующий сборку мусора — Swift).
Несмотря на кажущиеся недостатки («Java сочетает в себе красоту синтаксиса C++ со скоростью выполнения питона»), Java обладает множеством преимуществ.
Во-первых, Java — самый популярный [8] (с большим отрывом) язык программирования. У Java огромная экосистема библиотек и инструментов разработки (в том числе систем сборки и IDE). Про Java написано множество статей, книг и документации. Наконец, существует множество квалифицированных Java-разработчиков.
Программы на Java, как и на многих других высокоуровневых языках, переносимы между операционными системами и архитектурами процессора («Write once, run anywhere»). Практически это проявляется, например, в том, что приложения для Android работают без перекомпиляции на устройствах любой архитектуры (Android поддерживает ARM, ARM64, x86, x86–64 и MIPS).
В отличие от низкоуровневых языков вроде C и C++, использующих ручное управление памятью, в Java память автоматически управляется средой времени выполнения (runtime environment). Программа на Java даже не имеет прямого доступа к памяти, что автоматически предотвращает несколько классов ошибок, часто приводящих к падениям и уязвимостям в программах, написанных на низкоуровневых языках — невозможны «висячие ссылки» (из-за которых происходит use-after-free), разыменование нулевого указателя (при попытке это сделать выбрасывается NullPointerException
), чтение неинициализированной памяти и выход за границы массива.
Использование полноценной сборки мусора (по сравнению с automatic reference counting) избавляет программиста от всех проблем и сложностей с циклическими ссылками и позволяет реализовывать ещё более продвинутые (advanced) зависимости между объектами.
Это делает разработку под Android более приятной, чем разработку с использованием низкоуровневых языков, а приложения под Android гораздо более надёжными, в том числе и точки зрения безопасности.
В отличие от большинства других высокоуровневых языков, программы на Java не распространяются в виде исходного кода, а компилируются в промежуточный формат (байткод, bytecode), который представляет собой исполняемый бинарный код для специального процессора.
Хотя делаются попытки [9] создать физический процессор, который исполнял бы Java-байткод напрямую, в подавляющем большинстве случаев в качестве такого процессора используется эмулятор — Java virtual machine (JVM). Обычно используется реализация от Oracle/OpenJDK под названием HotSpot [10].
В Android используется собственная реализация под названием Android Runtime (ART), специально оптимизированная для работы на мобильных устройствах. В старых версиях Android (до 5.0 Lollipop) вместо ART использовалась другая реализация под названием Dalvik.
И в Dalvik, и в ART используется собственный формат байткода [11] и собственный формат файлов, в которых хранится байткод — DEX (Dalvik executable [12]). В отличие от class-файлов в «обычной джаве», весь Java-код приложения обычно [13] компилируется в один DEX-файл classes.dex
. При сборке Android-приложения Java-код сначала компилируется обычным компилятором Java в class-файлы, а потом конвертируется в DEX-файл специальной утилитой [14] (возможно и обратное преобразование [15]).
И HotSpot, и Dalvik, и ART дополнительно оптимизируют выполняемый код. Все три используют just-in-time compilation [16] (JIT), то есть во время выполнения компилируют байткод в куски полностью нативного кода, который выполняется напрямую. Кроме очевидного выигрыша в скорости, это позволяет оптимизировать код для выполнения на конкретном процессоре, не отказываясь от полной переносимости байткода.
Кроме того, ART может компилировать байткод в нативный код заранее, а не во время выполнения (ahead-of-time compilation [17]) — причём система автоматически планирует эту компиляцию на то время, когда устройство не используется и подключено к зарядке (например, ночью). При этом ART учитывает данные, собранные профилировщиком во время предыдущих запусков этого кода (profile-guided optimization [18]). Такой подход позволяет дополнительно оптимизировать код под специфику работы конкретного приложения и даже под особенности использования этого приложения именно этим пользователем.
В результате всех этих оптимизаций производительность Java-кода на Android не сильно уступает производительности низкоуровневого кода (на C/C++), а в некоторых случаях и превосходит её [19].
Java-байткод, в отличие от обычного исполняемого кода, использует объектную модель Java — то есть в байткоде явно записываются такие вещи, как классы, методы и сигнатуры. Это делает возможным компиляцию других языков в Java-байткод, что позволяет написанным на них программам исполняться на виртуальной машине Java и быть в той или иной степени совместимыми (interoperable) с Java.
Существуют как JVM-реализации независимых языков — например, Jython [20] для Python, JRuby [21] для Ruby, Rhino [22] для JavaScript и диалект Lisp Clojure [23] — так и языки, исходно разработанные для компиляции в Java-байткод и выполнения на JVM, самые известные из которых — Groovy [24], Scala [25] и Kotlin [26].
Самый новый из них, Kotlin, специально разработанный для идеальной совместимости с Java и обладающий гораздо более приятным синтаксисом (похожим на Swift), поддерживается [27] Google как официальный язык разработки под Android наравне с Java.
Несмотря на все преимущества Java, в некоторых случаях всё-таки желательно использовать низкоуровневый язык — например, для реализации критичного по производительности компонента, такого как браузерный движок, или чтобы использовать существующую нативную библиотеку. Java позволяет вызывать нативный код через Java Native Interface [28] (JNI), и Android предоставляет специальные средства для нативной разработки — Native Development Kit [29] (NDK), в который входят в том числе заголовочные файлы, компилятор (Clang), отладчик (LLDB) и система сборки.
Хотя NDK в основном ориентирован на использование C/C++, с его помощью можно писать под Android и на других языках — в том числе Rust [30], Swift [31], Python [32], JavaScript [33] и даже Haskell [34]. Больше того, есть даже возможность [35] портировать iOS-приложения (написанные на Objective-C или Swift) на Android практически без изменений.
Модель безопасности в классическом Unix основана на системе UID/GID — специальных номеров, которые ядро хранит для каждого процесса. Процессам с одинаковым UID разрешён доступ друг к другу, процессы с разным UID защищены друг от друга. Аналогично ограничивается доступ к файлам.
По смыслу каждый UID (user ID) соответствует своему пользователю — во времена создания Unix была нормальной ситуация, когда один компьютер одновременно использовался множеством людей. Таким образом, в Unix процессы и файлы разных людей были защищены друг от друга. Чтобы разрешить общий доступ к некоторым файлам, пользователи объединялись в группы, которым и соответствовал GID (group ID).
При этом всем программам, запускаемым пользователем, даётся полный доступ ко всему, к чему есть доступ у этого пользователя. Собственно, поскольку пользователь не может общаться с ядром напрямую, а взаимодействует с компьютером через shell и другие процессы — права пользователя и есть права программ, запущенных от его имени.
Такая модель подразумевает, что пользователь полностью доверяет всем программам, которые использует. В то время это было логично, потому что программы чаще всего либо были частью системы, либо создавались (писались и компилировались) самим пользователем.
В Unix есть и исключение из ограничений доступа — UID 0, который принято называть root. У него есть доступ ко всему в системе, и никакие ограничения на него не распространяются. Этот аккаунт использовался системным администратором; кроме того, под UID 0 запускаются многие системные сервисы.
В современном Linux эта модель была значительно расширена и обобщена, в том числе появились capabilities, позволяющие «получить часть root-прав», и реализующая мандатное управление доступом (mandatory access control, MAC) подсистема SELinux, которая позволяет дополнительно ограничить права (в том числе права root-процессов).
За несколько десятков лет, прошедших с создания Unix до создания Android, практика использования компьютеров («вычислителей») значительно изменилась.
Вместо машин, рассчитанных на параллельное использование многими пользователями (через терминалы — то, что сейчас эмулируют эмуляторы терминалов), появились персональные компьютеры, рассчитанные на использование одним человеком. Компьютеры перестали быть лишь рабочим инструментом и стали центром нашей цифровой жизни. С появлением мобильных устройств — сначала КПК, потом смартфонов, планшетов, умных часов и т.п. — эта тенденция только усилилась (потому что заниматься рабочими вопросами на мобильных устройствах относительно неудобно).
На таких устройствах хранятся гигабайты персональной информации, доступ к которой должен быть защищён и ограничен. В то же время расцвёл рынок сторонних приложений, которым у пользователя нет никаких оснований доверять.
Таким образом, в современных условиях вместо защиты разных пользователей друг от друга необходимо защищать от приложений другие приложения, пользовательские данные и саму систему. Кроме того, широкое распространение получили вирусы, которые обычно используют уязвимости в системе — для защиты от них нужно дополнительно защищать части системы друг от друга, чтобы использование одной уязвимости не давало злоумышленнику доступ ко всей системе.
Хотя часть Android-приложений поставляется с системой — например, такие стандартные приложения, как Калькулятор, Часы и Камера — большинство приложений пользователи устанавливают из сторонних источников. Самый известный из них — Google Play Store [36], но есть и другие, например, F-Droid [37], Amazon Appstore [38], Яндекс.Store [39], китайские Baidu App Store [40], Xiaomi App Store [41], Huawei App Store [42] и т.д. Кроме того, Android позволяет вручную устанавливать произвольные приложения из APK-файлов (это называют sideloading).
Как и другие Unix-подобные системы, Android использует для ограничения доступа существующий механизм UID/GID. При этом — в отличие от традиционного использования, когда UID соответствуют пользователям — в Android разные UID соответствуют разным приложениям. Поскольку процессы разных приложений запускаются с разными UID, уже на уровне ядра приложения защищены и изолированы друг от друга и не имеют доступа к системе и данным пользователя. Это образует песочницу (Application Sandbox) и позволяет пользователю устанавливать любые приложения без необходимости доверять им.
Чтобы всё-таки получить доступ к пользовательским данным, камере, совершению звонков и т.п., приложение должно получить от пользователя разрешение (permission). Некоторые из разрешений [43] существуют в виде GID, в которые приложение добавляется, когда получает это разрешение — например, получение разрешения ACCESS_FM_RADIO
помещает приложение в группу media
, что позволяет ему получить доступ к файлу /dev/fm
. Остальные существуют только на более высоком уровне (в виде записей в файле packages.xml
) и проверяются другими компонентами системы при обращении к высокоуровневому API через Binder.
Небольшая часть системных сервисов в Android запускается под UID 0, то есть root, но большинство используют специально выделенные номера UID, повышая при необходимости свои права с помощью Linux capabilities. Кроме того, Android использует SELinux — использование SELinux в Android называют SEAndroid — для ещё большего ограничения того, какие действия разрешено выполнять приложениям и системным сервисам.
Обычно Android не предоставляет пользователю прямой доступ к root-аккаунту, но в некоторых случаях у него есть возможность этот доступ получить. Как это происходит, зачем это нужно и какими опасностями это грозит я расскажу позднее.
В следующей статье (которая выходит уже через неделю) я расскажу о компонентах, из которых состоят приложения под Android, и об идеях, которые стоят за этой архитектурой.
Автор: Сергей Бугаев
Источник [44]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/264120
Ссылки в тексте:
[1] Image: https://habrastorage.org/web/f93/913/fda/f93913fda77445249c0d6e8f3445d314.png
[2] Как работает Android, часть 1: https://habrahabr.ru/company/solarsecurity/blog/334796/
[3] plan9.bell-labs.com: http://plan9.bell-labs.com/
[4] сохранённая копия в Wayback Machine: https://web.archive.org/web/20160114184818/http://plan9.bell-labs.com/plan9/
[5] 9p.io: https://9p.io/plan9/
[6] Image: https://habrastorage.org/web/341/9ed/14b/3419ed14bc3942099205711ccf704d9d.jpeg
[7] The future that everyone forgot: https://medium.com/@chrisdesalvo/the-future-that-everyone-forgot-d823af31f7c
[8] самый популярный: https://www.tiobe.com/tiobe-index/
[9] делаются попытки: https://www.wikiwand.com/en/Java_processor
[10] HotSpot: https://www.wikiwand.com/en/HotSpot
[11] формат байткода: https://source.android.com/devices/tech/dalvik/dalvik-bytecode
[12] Dalvik executable: https://source.android.com/devices/tech/dalvik/dex-format
[13] обычно: https://developer.android.com/studio/build/multidex.html
[14] специальной утилитой: https://r8.googlesource.com/r8
[15] обратное преобразование: https://github.com/pxb1988/dex2jar
[16] just-in-time compilation: https://source.android.com/devices/tech/dalvik/jit-compiler
[17] ahead-of-time compilation: https://www.wikiwand.com/en/Ahead-of-time_compilation
[18] profile-guided optimization: https://www.wikiwand.com/en/Profile-guided_optimization
[19] превосходит её: http://www.androidauthority.com/java-vs-c-app-performance-689081/
[20] Jython: http://www.jython.org/
[21] JRuby: http://jruby.org/
[22] Rhino: https://developer.mozilla.org/docs/Mozilla/Projects/Rhino
[23] Clojure: https://clojure.org/
[24] Groovy: http://groovy-lang.org/
[25] Scala: https://www.scala-lang.org/
[26] Kotlin: https://kotlinlang.org/
[27] поддерживается: https://android-developers.googleblog.com/2017/05/android-announces-support-for-kotlin.html
[28] Java Native Interface: https://www.wikiwand.com/en/Java_Native_Interface
[29] Native Development Kit: https://developer.android.com/ndk
[30] Rust: https://github.com/tomaka/android-rs-glue
[31] Swift: https://github.com/apple/swift/blob/master/docs/Android.md
[32] Python: https://python-for-android.readthedocs.io/en/latest/
[33] JavaScript: https://www.nativescript.org/
[34] Haskell: https://medium.com/@zw3rk/a-haskell-cross-compiler-for-android-8e297cb74e8a
[35] возможность: http://www.apportable.com/
[36] Google Play Store: https://play.google.com/store/apps
[37] F-Droid: https://f-droid.org/
[38] Amazon Appstore: https://amazon.com/appstore
[39] Яндекс.Store: https://store.yandex.ru/
[40] Baidu App Store: http://shouji.baidu.com/
[41] Xiaomi App Store: http://app.mi.com
[42] Huawei App Store: http://appstore.huawei.com/
[43] Некоторые из разрешений: https://android.googlesource.com/platform/frameworks/base/+/master/data/etc/platform.xml
[44] Источник: https://habrahabr.ru/post/338292/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.