Проверка границ в 64-битном оборудовании

Я читал блог о 64-битной версии Firefox на hacks.mozilla.org.

Автор заявляет:

За asm.js код, увеличенное адресное пространство также позволяет нам использовать аппаратную защиту памяти для безопасного удаления проверок границ из asm.js куча доступов. Прибыль довольно впечатляющая: 8%-17% в тестах пропускной способности asmjs-apps-*- как сообщается на http://arewefastyet.com/.

Я пытался понять, как 64-битное оборудование имеет автоматическую проверку границ (при условии, что компилятор поддерживает аппаратное обеспечение) для C/C++. Я не мог найти ответы в SO. Я нашел один технический документ на эту тему, но я не могу понять, как это делается.

Может кто-нибудь объяснить 64-битные аппаратные средства в проверке границ?

2 ответа

Решение

Большинство современных процессоров реализуют виртуальную адресацию / виртуальную память - когда программа ссылается на определенный адрес, этот адрес является виртуальным; отображение на физическую страницу, если таковое имеется, осуществляется посредством MMU ЦПУ (блока управления памятью). ЦП переводит каждый виртуальный адрес в физический адрес, просматривая его в таблице страниц ОС, настроенной для текущего процесса. Эти поиски кэшируются TLB, поэтому в большинстве случаев дополнительная задержка отсутствует. (В некоторых конструкциях ЦП, отличных от x86, ошибки TLB обрабатываются программным обеспечением операционной системой.)

Таким образом, моя программа обращается к адресу 0x8050, который находится на виртуальной странице 8 (при условии стандартного размера страницы 4096 байт (0x1000)). ЦП видит, что виртуальная страница 8 сопоставлена ​​с физической страницей 200, и поэтому выполняет чтение по физическому адресу. 200 * 4096 + 0x50 == 0xC8050, (Подобно тому, как TLB кэширует поиск в таблице страниц, более привычные L1/L2/L3 кэшируют доступ к кешу в физическую оперативную память.)

Что происходит, когда ЦП не имеет сопоставления TLB для этого виртуального адреса? Такое часто случается, потому что TLB имеет ограниченный размер. Ответ заключается в том, что процессор генерирует ошибку страницы, которая обрабатывается ОС.

В результате сбоя страницы может произойти несколько результатов:

  • Во-первых, операционная система может сказать: "О, ну, это просто не было в TLB, потому что я не мог соответствовать". ОС извлекает запись из TLB и заполняет новую запись, используя карту таблицы страниц процесса, а затем позволяет процессу продолжать работу. Это происходит тысячи раз в секунду на машинах с умеренной нагрузкой. (На процессорах с аппаратной обработкой пропусков TLB, таких как x86, этот случай обрабатывается аппаратно и даже не является "незначительной" ошибкой страницы.)
  • Во-вторых, ОС может сказать: "О, хорошо, что виртуальная страница не отображается сейчас, потому что физическая страница, которую она использовала, была перенесена на диск, потому что мне не хватило памяти". Операционная система приостанавливает процесс, находит некоторую память для использования (возможно, путем замены некоторого другого виртуального сопоставления), ставит в очередь чтение с диска для запрошенной физической памяти, а когда чтение диска завершается, возобновляет процесс с использованием недавно заполненного сопоставления таблицы страниц. (Это "главная" ошибка страницы.)
  • В-третьих, процесс пытается получить доступ к памяти, для которой не существует сопоставления - это чтение памяти, которой не должно быть. Это обычно называется ошибкой сегментации.

Соответствующий случай - номер 3. Когда происходит сбой, поведение операционной системы по умолчанию - прерывание процесса и выполнение таких действий, как запись файла ядра. Однако процессу разрешено перехватывать свои собственные ошибки и пытаться их обработать, возможно, даже без остановки. Здесь вещи становятся интересными.

Мы можем использовать это в наших интересах для выполнения проверок индекса с "аппаратным ускорением", но есть несколько камней преткновения, с которыми мы сталкиваемся, пытаясь это сделать.

Во-первых, общая идея: для каждого массива мы помещаем его в собственную область виртуальной памяти, причем все страницы, содержащие данные массива, отображаются как обычно. По обе стороны от данных реального массива мы создаем виртуальные отображения страниц, которые не читаются и не могут быть записаны. Если вы попытаетесь читать вне массива, вы сгенерируете ошибку страницы. Компилятор вставляет свой собственный обработчик ошибок страницы, когда он создал программу, и обрабатывает ошибку страницы, превращая ее в исключение индекса за пределами границ.

Камнем преткновения номер один является то, что мы можем пометить только целые страницы как читаемые или нет. Размеры массива могут не быть даже кратными размеру страницы, поэтому у нас есть проблема - мы не можем поставить заборы точно до и после конца массива. Лучшее, что мы можем сделать, - это оставить небольшой промежуток либо до начала массива, либо после конца массива между массивом и ближайшей страницей "забора".

Как они обходят это? Ну, в случае Java нелегко скомпилировать код, который выполняет отрицательную индексацию; и если это так, то это все равно не имеет значения, потому что отрицательный индекс обрабатывается так, как если бы он был беззнаковым, что ставит индекс намного впереди начала массива, что означает, что он очень вероятно попадет в не отображенную память и в любом случае вызовет ошибку,

Так что они делают, чтобы выровнять массив так, чтобы конец массива вставлялся прямо в конец страницы, вот так ('-' означает не отображенный, '+' означает сопоставленный):

-----------++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
|  Page 1  |  Page 2  |  Page 3  |  Page 4  |  Page 5  |  Page 6  |  Page 7  | ...
                 |----------------array---------------------------|

Теперь, если индекс находится за концом массива, он попадет на страницу 7, которая не отображена, что приведет к ошибке страницы, которая превратится в исключение индекса вне границ. Если индекс находится перед началом массива (то есть, он отрицательный), то, поскольку он обрабатывается как значение без знака, он станет очень большим и положительным, что снова приведет нас далеко за пределы страницы 7, что приведет к чтению не отображенной памяти, что приведет к ошибка страницы, которая снова превратится в исключение индекса вне границ.

Камнем преткновения № 2 является то, что мы действительно должны оставить много не отображенной виртуальной памяти за концом массива, прежде чем мы отобразим следующий объект, в противном случае, если индекс находится за пределами границ, но далеко, далеко, далеко за пределами, это может попадать на допустимую страницу и не вызывать исключение индекса вне пределов, а вместо этого читать или записывать произвольную память.

Чтобы решить эту проблему, мы просто используем огромные объемы виртуальной памяти - мы помещаем каждый массив в свою область памяти объемом 4 ГиБ, из которой на самом деле отображаются только первые N страниц. Мы можем сделать это, потому что мы просто используем здесь адресное пространство, а не реальную физическую память. 64-битный процесс имеет ~4 миллиарда блоков памяти по 4 ГиБ, поэтому у нас есть достаточно адресного пространства для работы, прежде чем мы закончим. На 32-битном процессоре или процессе у нас очень мало адресного пространства, с которым можно поиграться, поэтому этот метод не очень выполним. Сегодня многие 32-разрядные программы исчерпывают виртуальное адресное пространство, просто пытаясь получить доступ к реальной памяти, не говоря уже о том, чтобы сопоставить пустые "заборные" страницы в этом пространстве, чтобы попытаться использовать их как "аппаратно ускоренные" проверки диапазона индекса.

Техника, которую они используют, похожа на режим отладки подкачки Windows, только вместо кучи, которая вставляет каждый VirtualAlloc() на своей собственной странице виртуальной памяти это система, которая прикрепляет каждый массив (статический или на основе стека) к своей собственной странице виртуальной памяти (точнее, она размещает выделение в конце страницы, поскольку выполняется за пределами конца массив встречается гораздо чаще, чем пытаться получить доступ до его начала); затем он размещает недоступную "защитную страницу" после страницы размещения или даже значительное количество страниц в их случае.

При этом проверки границ не являются проблемой, потому что доступ за пределами границ вызовет нарушение доступа (SIGSEGV) вместо повреждения памяти. Это было невозможно на более раннем оборудовании просто потому, что 32-битный компьютер имел только 1M страниц для воспроизведения, и этого было недостаточно для работы с не игрушечными приложениями.

Другие вопросы по тегам