Проверка бит в 64-битном аппаратном обеспечении

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

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

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

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

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

Ответ 1

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

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

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

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

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

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

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

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

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

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

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

-----------++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
|  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-разрядные программы заканчиваются из виртуального адресного пространства, просто пытаясь получить доступ к реальной памяти, невзирая на попытку сопоставить пустые страницы "забора" в этом пространстве, чтобы попытаться использовать их как "аппаратные ускоренные" проверки диапазона индексов.

Ответ 2

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

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