Должны ли сравнения указателей быть подписанными или неподписанными в 64-разрядной версии x86?

При написании сборки пользовательского пространства x86 и сравнении двух значений указателя следует использовать подписанные условия, такие как jl а также jge или неподписанные условия, такие как jb а также jae?

Интуитивно я считаю указатели как беззнаковые, работающие от 0 до 2^64-1 в случае 64-битного процесса, и я думаю, что эта модель точна для 32-битного кода. Я думаю, именно так большинство людей думают о них.

В 64-битном коде, однако, я не думаю, что вы когда-либо сможете действительно пересечь подписанный разрыв в 0x7FFFFFFFFFFFFFFFи многие интересные области памяти имеют тенденцию к кластеризации около 0 со знаком (часто для кода и статических данных, а иногда и кучи в зависимости от реализации) и вблизи максимального адреса в нижней половине канонического адресного пространства (что-то вроде 0x00007fffffffffff на большинстве систем сегодня) для стековых расположений и кучи на некоторых реализациях1.

Поэтому я не уверен, каким образом они должны рассматриваться: подписанное имеет преимущество в том, что оно безопасно в районе 0, поскольку там нет разрыва, а неподписанное имеет то же преимущество около 2^63, поскольку там нет разрыва. Однако на практике вы не видите никаких адресов где-либо близко к 2^63, поскольку виртуальное адресное пространство текущего стандартного оборудования ограничено менее чем 50 битами. Это указывает на подписанный?


1... и иногда куча и другие сопоставленные области не находятся близко ни к нижней, ни к верхней части адресного пространства.

1 ответ

Это зависит именно то, что вы хотите знать о двух указателях!

Предыдущая редакция вашего вопроса дала ptrA < ptrB - C в качестве варианта использования, который вас интересует. например, проверка перекрытия с ptrA < ptrB - sizeA или, может быть, состояние развернутого цикла SIMD с current < endp - loop_stride, Обсуждение в комментариях было и о подобных вещах.

Итак, что вы на самом деле делаете, формируется ptrB - C как указатель, который потенциально находится за пределами интересующего вас объекта и может быть обернут (без знака). ( Хорошее наблюдение, что подобные вещи могут быть причиной того, что C и C++ делают это UB для формирования указателей вне объектов, но они допускают один конец, который имеет беззнаковое перенос в конце самой старшей страницы, если ядро ​​даже позволяет сопоставить его.) В любом случае, вы хотите использовать подписанное сравнение, чтобы оно "все еще работало", не проверяя наличие обтекания и не проверяя знак C или что-нибудь из этого. Это все еще намного более конкретно, чем большинство вопросов.

Да, для "связанных" указателей, полученных из одного и того же объекта с разумными размерами, сравнение со знаком безопасно на текущем оборудовании и может работать только на машинах маловероятного / далекого будущего с аппаратной поддержкой полных 64-битных виртуальных адресов. Проверки перекрытия также безопасны с unsigned, если оба указателя находятся в нижней половине канонического диапазона, что, как мне кажется, относится к адресам пользовательского пространства во всех основных операционных системах x86-64.


Как вы указываете, без знака ptrA < ptrB - C может "потерпеть неудачу", если ptrB - C обертывания (без знака обтекания). Это может произойти на практике для статических адресов, которые ближе к 0, чем размер C,

Обычно низкие 64 кБ не отображаются (например, в Linux большинство дистрибутивов поставляются с sysctl vm.mmap_min_addr = 65536 или, по крайней мере, 4096. Но в некоторых системах есть =0 для вина). Тем не менее, я думаю, что для ядра нормально не давать вам нулевую страницу, если вы не запросите этот адрес специально, потому что это предотвращает сбой NULL по умолчанию (что обычно очень желательно из соображений безопасности и отладки).

Это означает, что случай loop_stride обычно не является проблемой. sizeA версия обычно может быть сделана с ptrA + sizeA < ptrB и в качестве бонуса вы можете использовать LEA для добавления вместо копирования + вычитания. ptrA+sizeA гарантированно не переносится, если у вас нет объектов, которые переносят свой указатель от 2^64-1 до нуля ( что работает даже при загрузке с разбиением страницы при переносе, но вы никогда не увидите это в "нормальной" системе, потому что адреса обычно рассматриваются как неподписанные.)


Так, когда это может потерпеть неудачу с подписанным сравнением? когда ptrB - C подписал перенос по переполнению. Или, если у вас когда-либо есть указатели на объекты с высокой половиной (например, на страницы vDSO в Linux), сравнение адресов с высокой и низкой половиной может дать вам неожиданный результат: вы увидите, что адреса с "высокой половиной" меньше "младшие" адреса. Это происходит, хотя ptrB - C расчет не оборачивается.

(Мы говорим только об asm напрямую, а не о C, так что UB нет, я просто использую нотацию C для sub или же lea / cmp / jl.)

Подписанный перенос может произойти только вблизи границы между 0x7FFF... а также 0x8000..., Но эта граница чрезвычайно далека от любого канонического адреса. Я воспроизведу диаграмму адресного пространства x86-64 (для текущих реализаций, где виртуальный адрес 48 бит) из другого ответа. См. Также Почему в 64-битном виртуальном адресе длина 4 бита (48 бит) по сравнению с физическим адресом (52 бита)?,

Помните, что ошибки x86-64 на неканонических адресах. Это означает, что он проверяет, что 48-битный виртуальный адрес должным образом расширен до 64 бит, т.е. что биты [63:48] бит соответствия 47 (нумерация от 0).

+----------+
| 2^64-1   |   0xffffffffffffffff
| ...      |                       high half of canonical address range
| 2^64-2^47|   0xffff800000000000
+----------+
|          |
| unusable |   Not to scale: this is 2^15 times larger than the top/bottom ranges.
|          |
+----------+
| 2^47-1   |   0x00007fffffffffff
| ...      |                       low half of canonical range
| 0        |   0x0000000000000000
+----------+

Корпорация Intel предложила 5-уровневое расширение таблицы страниц для 57-битных виртуальных адресов (т.е. еще один 9-битный уровень таблиц), но это все еще оставляет большую часть адресного пространства неканонической. т. е. любой канонический адрес все равно будет на расстоянии 2^63 - 2^57 от подписанного переноса.

В зависимости от операционной системы все ваши адреса могут находиться в нижней или верхней половине. например, в Linux x86-64 высокие ("отрицательные") адреса являются адресами ядра, а низкие (подписанные положительные) адреса - в пространстве пользователя. Но обратите внимание, что Linux отображает страницы ядра vDSO / vsyscall в пользовательское пространство очень близко к вершине виртуального адресного пространства. (Но он оставляет страницы не отображенными вверху, например ffffffffff600000-ffffffffff601000 [vsyscall] в 64-разрядном процессе на моем рабочем столе, но страницы vDSO находятся в верхней части нижней половины канонического диапазона, 0x00007fff..., Даже в 32-битном процессе, где теоретически весь 4GiB может использоваться в пространстве пользователя, vDSO - это страница ниже самой высокой страницы, и mmap(MAP_FIXED) не работал на этой самой высокой странице. Возможно, потому что C позволяет указатели один за другим?)

Если вы когда-нибудь возьмете адрес функции или переменной в vsyscall страница, вы можете иметь сочетание положительных и отрицательных адресов. (Я не думаю, что кто-то когда-либо делает это, но это возможно.)

Поэтому сравнение подписанных адресов может быть опасным, если у вас нет разделения на ядро ​​/ пользователя, отделяющего положительный знак от отрицательного со знаком, и ваш код выполняется в далеком будущем, когда / если x86-64 был расширен до полных 64-битных виртуальных адресов Таким образом, объект может охватывать границы. Последнее кажется маловероятным, и если вы можете ускориться, предполагая, что этого не произойдет, это, вероятно, хорошая идея.

Это означает, что сравнение со знаком уже опасно для 32-битных указателей, потому что 64-битные ядра оставляют весь 4GiB доступным для пользователя. (И 32-битные ядра могут быть настроены с разделением ядра на пользователя 3:1). Там нет непригодного канонического диапазона. В 32-битном режиме объект может охватывать границу со знаком обтекания. (Или в ILP32 x32 ABI: 32-битные указатели в длинном режиме.)


Преимущества производительности:

В отличие от 32-битного режима, там нет процессора, где jge быстрее чем jae в 64-битном режиме или другой комбо. (И разные условия для setcc / cmovcc никогда не имеют значения). Таким образом, любой Per diff отличается только от окружающего кода, если вы не можете сделать что-то умное с adc или же sbb вместо cmov или setcc.

Семейство Sandybridge может выполнять макроклапан test / cmp (и sub, add и другие инструкции, не предназначенные только для чтения) со сравнениями со знаком или без знака (не для всех JCC, но это не является фактором). Семейство бульдозеров может объединить cmp / test с любым JCC.

Core2 может только макро-предохранитель cmp с беззнаковыми сравнениями, без подписи, но Core2 вообще не может слиться в макросе в 64-битном режиме. (Это может макро-предохранитель test со знаком сравнения в 32-битном режиме, кстати.)

Нехалем может макро-предохранитель test или же cmp со знаком или без знака сравнения (в том числе в 64-битном режиме).

Источник: микроарх Агнера Фога pdf.

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