Как это изменит код, например вызовы функций?
Какова опция -fPIE для автономных исполняемых файлов в gcc и ld?
Ответ 1
PIE должен поддерживать рандомизацию размещения пространства адресов (ASLR) в исполняемых файлах.
До того, как был создан режим PIE, исполняемый файл программы не мог быть помещен на случайный адрес в памяти, динамические библиотеки с независимым положением (PIC) могли быть перенесены на случайное смещение. Он очень похож на то, что PIC делает для динамических библиотек, разница в том, что таблица привязки к процедуре (PLT) не создана, вместо этого используется перенос PC-relative.
После включения поддержки PIE в gcc/linkers тело программы скомпилировано и связано как независимый от позиции код. Динамический компоновщик выполняет полную обработку перемещения в программном модуле, как и динамические библиотеки. Любое использование глобальных данных преобразуется в доступ через глобальную таблицу смещений (GOT) и добавляются переадресации GOT.
PIE хорошо описан в этой презентации OpenBSD PIE.
Изменения в функциях отображаются в этом слайде (PIE vs PIC).
x86 pic vs pie
Локальные глобальные переменные и функции оптимизированы в pie
Внешние глобальные переменные и функции такие же, как pic
и в этот слайд (PIE vs old-style linking)
x86 pie vs no-flags (исправлено)
Локальные глобальные переменные и функции аналогичны фиксированным
Внешние глобальные переменные и функции такие же, как pic
Обратите внимание, что PIE может быть несовместим с -static
Ответ 2
Минимальный исполняемый пример: GDB исполняемый файл дважды
Для тех, кто хочет увидеть какое-то действие, давайте посмотрим, как ASLR работает над исполняемым файлом PIE и меняет адреса при каждом запуске:
main.c
#include <stdio.h>
int main(void) {
    puts("hello");
}
 main.sh
#!/usr/bin/env bash
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
for pie in no-pie pie; do
  exe="${pie}.out"
  gcc -O0 -std=c99 "-${pie}" "-f${pie}" -ggdb3 -o "$exe" main.c
  gdb -batch -nh \
    -ex 'set disable-randomization off' \
    -ex 'break main' \
    -ex 'run' \
    -ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
    -ex 'run' \
    -ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
    "./$exe" \
  ;
  echo
  echo
done
  Для одного с -no-pie все скучно:
Breakpoint 1 at 0x401126: file main.c, line 4.
Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126
Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126
  Перед началом выполнения break main устанавливает 0x401126 останова на 0x401126.
 Затем во время обоих выполнений run останавливается по адресу 0x401126.
 Однако с -pie гораздо интереснее:
Breakpoint 1 at 0x1139: file main.c, line 4.
Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x5630df2d6139
Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x55763ab2e139
  Перед началом выполнения GDB просто берет "фиктивный" адрес, который присутствует в исполняемом файле: 0x1139.
 Однако после запуска GDB разумно замечает, что динамический загрузчик поместил программу в другое место, и первый разрыв остановился на 0x5630df2d6139.
 Затем второй прогон также разумно заметил, что исполняемый файл снова переместился, и в итоге 0x55763ab2e139 на 0x55763ab2e139.
 echo 2 | sudo tee/proc/sys/kernel/randomize_va_space echo 2 | sudo tee/proc/sys/kernel/randomize_va_space гарантирует, что ASLR включен (по умолчанию в Ubuntu 17.10): как я могу временно отключить ASLR (рандомизация расположения адресного пространства)? | Спросите Ubuntu.
 set disable-randomization off, иначе GDB, как следует из названия, по умолчанию отключает ASLR для процесса, чтобы дать фиксированные адреса при каждом прогоне для улучшения процесса отладки: разница между адресами GDB и "реальными" адресами? | Переполнение стека.
  readelf анализ
Кроме того, мы также можем наблюдать, что:
readelf -s ./no-pie.out | grep main
 выдает фактический адрес загрузки во время выполнения (pc указывает на следующую инструкцию через 4 байта):
64: 0000000000401122    21 FUNC    GLOBAL DEFAULT   13 main
 в то время как:
readelf -s ./pie.out | grep main
 дает только смещение:
65: 0000000000001135    23 FUNC    GLOBAL DEFAULT   14 main
  Выключая ASLR (либо с помощью randomize_va_space либо с set disable-randomization off), GDB всегда дает main адрес: 0x5555555547a9, поэтому мы делаем вывод, что адрес -pie состоит из:
0x555555554000 + random offset + symbol offset (79a)
 TODO где 0x555555554000 жестко запрограммирован в ядре Linux/загрузчик glibc/где угодно? Как определяется адрес текстового раздела исполняемого файла PIE в Linux?
Пример минимальной сборки
Еще одна крутая вещь, которую мы можем сделать, это поиграться с некоторым ассемблерным кодом, чтобы более конкретно понять, что означает PIE.
Мы можем сделать это с помощью автономной сборки Linux x86_64:
main.S
.text
.global _start
_start:
asm_main_after_prologue:
    /* write */
    mov $1, %rax   /* syscall number */
    mov $1, %rdi   /* stdout */
    mov $msg, %rsi  /* buffer */
    mov $len, %rdx /* len */
    syscall
    /* exit */
    mov $60, %rax   /* syscall number */
    mov $0, %rdi    /* exit status */
    syscall
msg:
    .ascii "hello\n"
len = . - msg
  и он собирается и работает нормально с:
as -o main.o main.S
ld -o main.out main.o
./main.out
 Однако, если мы попытаемся связать его как пирог с:
ld --no-dynamic-linker -pie -o main.out main.o
 тогда ссылка потерпит неудачу с:
ld: main.o: relocation R_X86_64_32S against '.text' can not be used when making a PIE object; recompile with -fPIC
ld: final link failed: nonrepresentable section on output
 Потому что линия:
mov $msg, %rsi  /* buffer */
  жестко кодирует адрес сообщения в операнде mov и поэтому не зависит от позиции.
 --no-dynamic-linker требуется, как описано в разделе: Как создать статически связанный независимый от позиции исполняемый файл ELF в Linux?
Если мы вместо этого напишем это независимым способом:
lea msg(%rip), %rsi
 тогда ссылка PIE работает нормально, и GDB показывает нам, что исполняемый файл каждый раз загружается в другое место в памяти.
 Разница здесь в том, что lea кодирует адрес msg относительно текущего адреса ПК из-за синтаксиса rip, см. Также: Как использовать относительную адресацию RIP в 64-битной программе сборки?
Мы также можем понять это, разобрав обе версии с помощью:
objdump -S main.o
 которые дают соответственно:
e:   48 c7 c6 00 00 00 00    mov    $0x0,%rsi
e:   48 8d 35 19 00 00 00    lea    0x19(%rip),%rsi        # 2e <msg>
000000000000002e <msg>:
  2e:   68 65 6c 6c 6f          pushq  $0x6f6c6c65
  Таким образом, мы ясно видим, что lea уже имеет полный правильный адрес msg закодированный как текущий адрес + 0x19.
 Однако версия mov установила адрес 00 00 00 00, что означает, что там будет выполнено перемещение: что делают компоновщики? Загадочный R_X86_64_32S в сообщении об ошибке ld - это фактический тип перемещения, который был необходим и который не может произойти в исполняемых файлах PIE.
 Еще одна забавная вещь, которую мы можем сделать, это поместить msg в раздел data вместо .text с помощью:
.data
msg:
    .ascii "hello\n"
len = . - msg
  Теперь .o собирается:
e:   48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # 15 <_start+0x15>
  поэтому смещение RIP теперь равно 0, и мы предполагаем, что ассемблер запросил перемещение. Мы подтверждаем это:
readelf -r main.o
 который дает:
Relocation section '.rela.text' at offset 0x160 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000011  000200000002 R_X86_64_PC32     0000000000000000 .data - 4
  настолько ясно, что R_X86_64_PC32 - относительное перемещение ПК, которое ld может обрабатывать для исполняемых файлов PIE.
Этот эксперимент научил нас, что компоновщик сам проверяет, что программа может быть PIE, и помечает ее как таковую.
 Затем при компиляции с GCC -pie говорит GCC сгенерировать независимую от позиции сборку.
Но если мы пишем сборку сами, мы должны вручную убедиться, что мы достигли независимости позиции.
В ARMv8 aarch64 независимый от положения привет мир может быть достигнут с помощью инструкции ADR.
Как определить, является ли ELF независимым от позиции?
Помимо простого запуска через GDB, некоторые статические методы упоминаются в:
- исполняемый файл: https://unix.stackexchange.com/info/89211/how-to-test-whether-a-linux-binary-was-compiled-as-position-independent-code/435038#435038
 - библиотека: Как я могу узнать, с помощью чего-то вроде objdump, был ли объектный файл создан с помощью -fPIC?
 
Проверено в Ubuntu 18.10.