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-битные абсолютные адреса, а 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
не по умолчанию. Сообщение об ошибке, вероятно, должно измениться, потому что многие люди сталкиваются с этой ошибкой при связывании рукописного asm.
использование 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
, для compat с gcc, но в clang4.0.1 его нет.)
Только с -no-pie
, (но до сих пор -fpie
) сгенерированный компилятором код (из источников C или C++) будет немного медленнее и больше, чем необходимо, но все равно будет связан с зависимым от позиции исполняемым файлом, который не получит преимущества от ASLR. "Слишком много PIE вредно для производительности" сообщает о среднем замедлении на 3% для x86-64 на SPEC CPU2006 (у меня нет копии документа, так что IDK, какое оборудование было на:/). Но в 32-битном коде среднее замедление составляет 10%, в худшем - 25% (на SPEC CPU2006).
Наказание для исполняемых файлов PIE в основном за такие вещи, как индексация статических массивов, как описывает Агнер в вопросе, где использование статического адреса в качестве 32-битного непосредственного или в качестве части [disp32 + index*4]
Режим адресации сохраняет инструкции и регистры по сравнению с REA-относительным 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 -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-байтовый способ кодирования небольших абсолютных адресов (address-size + 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 # doesn't 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, ...
Об этом также спрашивали: Как проверить, был ли двоичный файл Linux скомпилирован как независимый от позиции код?
Полусвязанные (но не совсем): еще одна недавняя функция gcc gcc -fno-plt
, Наконец, вызовы в общие библиотеки могут быть просто call [rip + symbol@GOTPCREL]
(AT&T call *puts@GOTPCREL(%rip)
), без батута PLT.
Надеюсь, дистрибутивы скоро начнут его включать, поскольку он также избавляет от необходимости записывать + исполняемые страницы памяти. Это значительное ускорение для программ, которые выполняют много вызовов совместно используемой библиотеки, например, x86-64. clang -O2 -g
компиляция tramp3d идет с 41.6 до 36.8 с на любом оборудовании, на котором тестировал автор патча. (Clang, возможно, наихудший сценарий для вызовов из общей библиотеки.)
Это требует раннего связывания вместо ленивого динамического связывания, поэтому оно медленнее для больших программ, которые выходят сразу. (например clang --version
или составление hello.c
). Это замедление можно было бы уменьшить с помощью предварительной ссылки, по-видимому.
Однако это не снимает накладные расходы GOT для внешних переменных в коде PIC общей библиотеки. (См. Ссылку Godbolt выше).
Сноски 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
, 64-битная немедленная версия медленнее извлекается из кэша UOP на процессорах семейства Sandybridge, согласно микроарху pdf Агнера Фога. (Да, тот же человек, который задал этот вопрос.:)
Вы можете использовать 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, и больше исполнительных портов может его запустить.