Что такое опция -fPIE для не зависящих от позиции исполняемых файлов в gcc и ld? - PullRequest
59 голосов
/ 17 марта 2010

Как это изменит код, например, вызовы функций?

Ответы [ 2 ]

70 голосов
/ 17 февраля 2011

PIE для поддержки рандомизации макета адресного пространства (ASLR) в исполняемых файлах.

До того, как был создан режим PIE, исполняемый файл программы не мог быть размещен по случайному адресу в памяти, только динамические библиотеки с позиционным кодом (PIC) могли быть перемещены на случайное смещение. Это работает очень похоже на то, что делает PIC для динамических библиотек, разница в том, что таблица процедурных связей (PLT) не создается, вместо этого используется относительное перемещение с ПК.

После включения поддержки PIE в gcc / linkers тело программы компилируется и связывается как позиционно-независимый код. Динамический компоновщик выполняет полную обработку перемещения в программном модуле, как динамические библиотеки. Любое использование глобальных данных преобразуется в доступ через Глобальную таблицу смещений (GOT) и добавляются перемещения GOT.

PIE хорошо описан в этой презентации OpenBSD PIE .

Изменения функций показаны на этом слайде (PIE vs PIC).

x86 рис против пирога

Локальные глобальные переменные и функции оптимизированы в круговом режиме

Внешние глобальные переменные и функции такие же, как рис.

и этот слайд (PIE vs по-старому)

x86 pie против no-flags (исправлено)

Локальные глобальные переменные и функции аналогичны фиксированным

Внешние глобальные переменные и функции такие же, как pic

Обратите внимание, что PIE может быть несовместим с -static

19 голосов

Пример минимального запуска: 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.

Затем во время обоих казней 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.

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

дает фактический адрес загрузки во время выполнения (компьютер указывает на следующую инструкцию через 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 hello world:

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

GitHub upstream

и он собирается и работает нормально:

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

Проверено в Ubuntu 18.10.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...