32-разрядные абсолютные адреса больше не разрешены в x86-64 Linux?

64-разрядная Linux по умолчанию использует небольшую модель памяти, которая ставит все код и статические данные ниже предела адреса 2 ГБ. Это позволяет использовать 32-битные абсолютные адреса. Более старые версии gcc используют 32-разрядные абсолютные адреса для статических массивов, чтобы сохранить дополнительную инструкцию для вычисления относительных адресов. Однако это больше не работает. Если я попытаюсь создать 32-разрядный абсолютный адрес в сборке, я получаю ошибку компоновщика: "Перемещение R_X86_64_32S против`.data 'не может использоваться при создании общего объекта, перекомпиляция с -fPIC". Это сообщение об ошибке вводит в заблуждение, конечно, потому что я не делаю общий объект, а -fpIC не помогает. То, что я выяснил до сих пор, следующее: gcc версия 4.8.5 использует 32-битные абсолютные адреса для статических массивов, gcc-версия 6.3.0 этого не делает. версия 5, вероятно, тоже. Компилятор в binutils 2.24 допускает 32-битные абсолютные адреса, verson 2.28 - нет.

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

Теперь я хочу спросить: когда это было сделано? Это где-то задокументировано? И есть ли опция компоновщика, которая позволяет принимать 32-битные абсолютные адреса?

Ответы

Ответ 1

Ваш дистрибутив сконфигурировал gcc с --enable-default-pie, поэтому он по умолчанию делает независимые от позиции исполняемые файлы (с учетом ASLR исполняемого файла, а также библиотек). В наши дни большинство дистрибутивов делают это.

Вы на самом деле создаете общий объект: исполняемые файлы PIE являются своего рода хаком, использующим общий объект с точкой входа. Динамический компоновщик уже поддерживал это, и ASLR хорош для безопасности, так что это был самый простой способ реализовать ASLR для исполняемых файлов.

32-разрядное абсолютное перемещение недопустимо в разделяемом объекте ELF; это предотвратит их загрузку за пределы младшего 2 ГБ (для 32-разрядных адресов с расширенными знаками). Допускаются 64-битные абсолютные адреса, но обычно вы хотите использовать их только для таблиц переходов или других статических данных, а не для инструкций. 1

Часть recompile with -fPIC сообщения об ошибке является фиктивной для рукописного asm; он написан для людей, которые компилируют с gcc -c, а затем пытаются связать с gcc -shared -o foo.so *.o, с gcc, где -fPIE не используется по умолчанию. Сообщение об ошибке, вероятно, должно измениться, потому что многие люди сталкиваются с этой ошибкой при связывании рукописного асма.


Как использовать RIP-относительную адресацию: основы

Всегда используйте RIP-относительную адресацию для простых случаев, когда нет недостатков. См. также сноску 1 ниже и этот ответ для синтаксиса. Рассматривайте возможность использования 32-битной абсолютной адресации только тогда, когда она полезна для размера кода, а не вредна например NASM default rel вверху файла.

В AT & T foo(%rip) или в GAS .intel_syntax noprefix используйте [rip + foo].


Отключите режим PIE, чтобы 32-битная абсолютная адресация работала

Используйте gcc -fno-pie -no-pie, чтобы переопределить это обратно к старому поведению. -no-pie - это опция компоновщика, -fno-pie - это опция генерации кода. Только с -fno-pie gcc создаст код, подобный mov eax, offset .LC0, который не связывается с все еще включенным -pie.

(clang может также включать PIE по умолчанию: используйте clang -fno-pie -nopie. Патч от июля 2017 года сделал -no-pie псевдонимом для -nopie, для совместим с gcc, но в clang4.0.1 его нет.)


Стоимость PIE для 64-битного (младшего) или 32-битного кода (мажорного)

Только с -no-pie, (но все еще -fpie) сгенерированный компилятором код (из источников C или C++) будет немного медленнее и больше, чем необходимо, но все равно будет связан с зависимым от позиции исполняемым файлом, который не получит выгоды от ASLR. "Слишком много PIE вредно для производительности" сообщает о среднем замедлении на 3% для x86-64 на SPEC CPU2006 (у меня нет копии документа, так что IDK, какое оборудование было на нем) :/). Но в 32-битном коде среднее замедление составляет 10%, в худшем - 25% (на SPEC CPU2006).

Наказание для исполняемых файлов PIE в основном за такие вещи, как индексация статических массивов, как описывает Агнер в вопросе, где использование статического адреса в качестве 32-битного непосредственного или как часть режима адресации [disp32 + index*4] сохраняет инструкции и регистры по сравнению с RIP -относительный LEA, чтобы получить адрес в реестре. также 5-байтовый mov r32, imm32 вместо 7-байтового lea r64, [rel symbol] для получения статического адреса в регистр удобен для передачи адреса строкового литерала или других статических данных в функцию.

-fPIE по-прежнему не предполагает вставки символов для глобальных переменных/функций, в отличие от -fPIC для разделяемых библиотек, которые должны пройти через GOT для доступа к глобальным переменным (что является еще одной причиной для использования static для любых переменных, которые могут быть ограничены). подать область вместо глобальной). См. Плохое состояние динамических библиотек в Linux.

Таким образом, -fPIE намного менее плох, чем -fPIC для 64-битного кода, но все же плохо для 32-битной, поскольку относительная к RIP адресация недоступна. Смотрите некоторые примеры в проводнике компилятора Godbolt. В среднем, -fPIE имеет очень маленький недостаток производительности/размера кода в 64-битном коде. Худший случай для конкретного цикла может составлять всего несколько%. Но 32-битный пирог может быть намного хуже.

Ни одна из этих опций -f code-gen не имеет никакого значения при простом соединении, или при сборке .S рукописного ассм. gcc -fno-pie -no-pie -O3 main.c nasm_output.o - это случай, когда вам нужны оба варианта.


Проверка настроек GCC

Если ваш GCC был настроен таким образом, gcc -v |& grep -o -e '[^ ]*pie' печатает --enable-default-pie. Поддержка этого параметра конфигурации была добавлена в gcc в начале 2015 года. Ubuntu включил его в 16.10 и Debian примерно в то же время в gcc 6.2.0-7 (что привело к ошибкам сборки ядра: https://lkml.org/lkml/2016/10/21/904).

Связано: Сборка сжатых ядер x86 как PIE также была изменена по умолчанию.

Почему Linux не рандомизирует адрес сегмента исполняемого кода? - более старый вопрос о том, почему он не был установлен по умолчанию ранее или был включен только для нескольких пакетов в более старой Ubuntu, прежде чем он был включен по всем направлениям.


Обратите внимание, что ld сам не изменил значение по умолчанию. Он по-прежнему работает нормально (по крайней мере, в Arch Linux с binutils 2.28). Изменение состоит в том, что gcc по умолчанию передает -pie в качестве опции компоновщика, если вы явно не используете -static или -no-pie.

В исходном файле NASM я использовал a32 mov eax, [abs buf], чтобы получить абсолютный адрес. (Я проверял, есть ли 6-байтовый способ кодирования небольших абсолютных адресов (размер-адреса + mov eax, moffs: 67 a1 40 f1 60 00)) срыв LCP на процессорах Intel. Это делает.)

nasm -felf64 -Worphan-labels -g -Fdwarf testloop.asm &&
ld -o testloop testloop.o              # works: static executable

gcc -v -nostdlib testloop.o            # does not work
...
..../collect2  ... -pie ...
/usr/bin/ld: testloop.o: relocation R_X86_64_32 against '.bss' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: Nonrepresentable section on output
collect2: error: ld returned 1 exit status

gcc -v -no-pie -nostdlib testloop.o    # works
gcc -v -static -nostdlib testloop.o    # also works: -static implies -no-pie

связано: создание статических/динамических исполняемых файлов с/без libc, определение _start или main.


Проверка, является ли существующий исполняемый файл PIE или нет

file и readelf говорят, что PIE являются "общими объектами", а не исполняемыми файлами ELF. Статические исполняемые файлы не могут быть пирогом.

$ gcc -fno-pie  -no-pie -O3 hello.c
$ file a.out
a.out: ELF 64-bit LSB executable, ...

$ gcc -O3 hello.c
$ file a.out
a.out: ELF 64-bit LSB shared object, ...

 ## Or with a more recent version of file:
a.out: ELF 64-bit LSB pie executable, ...

Об этом также спрашивали: Как проверить, был ли двоичный файл Linux скомпилирован как независимый от позиции код?


Полусвязанный (но не совсем): еще одна недавняя функция gcc - gcc -fno-plt. Наконец, вызовы в общие библиотеки могут быть просто call [rip + [email protected]] (AT & T call *[email protected](%rip)), без батута PLT.

Надеюсь, дистрибутивы скоро начнут его включать, потому что он также избавляет от необходимости записываемых + исполняемых страниц памяти. Это значительное ускорение для программ, которые выполняют много вызовов совместно используемой библиотеки, например, x86-64 clang -O2 -g компилирует tramp3d с 41,6 до 36,8 с на любом оборудовании , которое автор патча тестировал на. (Clang, возможно, является наихудшим сценарием для вызовов библиотеки общего доступа.)

Это требует раннего связывания вместо ленивого динамического связывания, поэтому оно медленнее для больших программ, которые выходят сразу. (например, clang --version или компиляция hello.c). Это замедление может быть уменьшено с помощью предварительной ссылки.

Это, однако, не устраняет накладные расходы GOT для внешних переменных в коде PIC совместно используемой библиотеки. (См. ссылку на крестник выше).


Сноски 1

64-разрядные абсолютные адреса фактически разрешены в общих объектах Linux ELF с перемещением текста, чтобы разрешить загрузку по разным адресам (ASLR и общие библиотеки). Это позволяет вам иметь таблицы переходов в section .rodata или static const int *foo = &bar; без инициализатора времени выполнения.

Итак, mov rdi, qword msg работает (синтаксис NASM/YASM для 10-байтового mov r64, imm64, он же AT & T синтаксис movabs, единственная инструкция, которая может использовать 64-битную немедленную передачу). Но это больше и обычно медленнее, чем lea rdi, [rel msg], и это то, что вы должны использовать, если решите не отключать -pie. По словам микроарханта Agner Fog pdf, 64-разрядные операционные системы медленнее извлекаются из кэша UOP на процессорах семейства Sandybridge. (Да, тот же человек, который задал этот вопрос. :)

Вы можете использовать NASM default rel вместо указания его в каждом режиме адресации [rel symbol]. См. также 64-разрядный формат Mach-O не поддерживает 32-разрядные абсолютные адреса. NASM Accessing Array для более подробного описания того, как избежать 32-битной абсолютной адресации. OS X вообще не может использовать 32-битные адреса, поэтому RIP-адресация также является лучшим способом.

В позиционно-зависимом коде (-no-pie) вы должны использовать mov edi, msg, когда вам нужен адрес в регистре; 5-байтовый mov r32, imm32 даже меньше, чем REA-относительный LEA, и больше исполнительных портов может его запустить.