Код, независимый от позиции

Последнее обновление: 29.09.2023

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

Но это означает, что библиотеки и приложение должны быть написаны специально, чтобы загрузка их в память не приводила к ошибкам. Данный тип кода приложения известен как позиционно-независимый код или PIC (position-independent code).

Так, в прошлой теме при компиляции программы мы использовали опцию -no-pie

gcc hello.s -no-pie -lten -L . -o hello

Эта опция сообщает gcc не создавать исполняемый файл, независимый от позиции (PIE). Фактически при компиляции в заголовке исполняемого файла устанавливается один бит, который сигнализирует загрузчику о том, что код зависит от позиции. Затем загрузчик всегда будет загружать код в одно и то же фиксированное место виртуальной памяти, указанное в заголовке исполняемого файла.

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

Теперь рассмотрим, как сделать наш код независимым от позиции. Есть три основные области, где код необходимо изменить, чтобы код был независимым от позиции:

  • Ссылки на внешние функции

  • Ссылки на раздел .data библиотеки

  • Ссылки на внешние объекты

Ссылки на внешние функции

Ссылки на внешние функции обрабатываются компоновщиком/загрузчиком автоматически с помощью таблиц PLT/GOT. Так, вызов call printf заменяется на вызов call printf@plt, который обращается по адресу функции printf, указанному в таблице GOT.

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

call printf

или

call printf@plt

можно использовать следующую инструкцию

call *printf@GOTPCREL(%rip)

Это может немного ускорить выполнение, но также немного увеличит время загрузки приложения.

Ссылки на секцию .data

Ссылки на адреса в разделе .data библиотеки обрабатываются в режиме адресации, известном как PC-relative addressing или адресация относительно регистра PC (Program Counter) он же регистр %rip (Instruction Pointer). Этот режим адресации записывает адреса данных как смещение относительно указателя текущей команды.

Например, пусть у нас есть некоторые данные в секции .data:

.data
message: .asciz "Hello METANIT.COM\n"
arg: .quad 10

Для загрузки этих данных в коде применяется адресация относительно %rip:

movq arg(%rip), %rax

В данном случае мы применяем адресацию относительно регистра PC (RIP). Сама инструкция указывает, что нам нужен адрес переменной arg, но закодировать его нужно как смещение относительно указателя инструкции в этом месте. Следовательно, независимо от того, где в памяти загружается код, программа все равно будет знать, где находится переменная arg, поскольку это будет фиксированное смещение от текущего места в коде.

Если нам нужен адрес переменной, можно использовать инструкцию leaq:

leaq message(%rip), %rsi

То есть опять же мы получаем адрес переменной message относительно регистра %rip.

Ссылка на внешние объекты

Для ссылки на внешние данные (например, объект стандартного вывода stdout) применяется двухэтапный поиск с помощью таблицы GOT. Для этого нужно использовать специальный символ GOTPCREL.

Допустим, мы хотим загрузить объект стандартного вывода stdout в регистр %rdi. В общем случае мы могли бы это сделать так:

movq stdout, %rdi

Но в действительности мы не знаем, где будет располагаться код объекта stdout. Поэтому нам нужно найти адрес этого объекта в таблице GOT, а затем использовать этот адрес для загрузки фактического значения. Для этого применяются две инструкции:

movq stdout@GOTPCREL(%rip), %rdi
movq (%rdi), %rdi

Первая инструкция находит местоположение переменной в глобальной таблице смещений, используя адресацию относительно PC, а затем загружает ее в %rdi. Специальный символ GOTPCREL обозначает местоположение GOT относительно PC. Затем вторая инструкция использует это место для поиска значения самого объекта stdout.

Независимые от позиции приложения

Теперь определим независимое от позиции приложение (Position Independent Executable) или PIE (которое используют независимый от позиции код (PIC)). Например, определим файл hello.s со следующим кодом:

.globl main
 
.data
message: .asciz "Hello METANIT.COM\n"

.text
main:
    subq $8, %rsp           # выравнивание должно быть по 16 байтам              

    movq stdout@GOTPCREL(%rip), %rdi    # stdout@GOTPCREL(%rip) - загрузка относительно RIP
    movq (%rdi), %rdi

    leaq message(%rip), %rsi    # message(%rip) - загрузка относительно RIP
    call fprintf

    addq $8, %rsp
    ret

Для демонстрации применения глобального объекта stdout здесь используется стандартная библиотечная функция fprintf, а не printf. Функция fprintf получает в качестве первого параметра глобальный объект stdout через адрес из таблицы GOT. В качестве второго параметра в регистр %rsi загружается адрес переменной message. И опять же, поскольку идет обращение к данным в секции .data, применяется адрес относительно регистра %rip:

leaq message(%rip), %rsi

Для компиляции приложения используем команду:

gcc -pie  hello.s  -o hello

Флаг -pie как раз и указывает, что будет компилироваться приложение, независимое от позиции. Нередко эта опция уже применяется по умолчанию, поэтому можно сократить команду компиляции:

gcc hello.s  -o hello

Независимые от позиции библиотеки

Подобно тому, как мы можем определять независимые от позиции приложения, мы также может определять незываисимые от позиции библиотеки. Так, пусть для примера у нас будет файл ten.s, который содержит простенькую функцию умножения некоторого числа, которое передается через регистр %rdi, на 10:

.globl ten

.data
arg: .quad 10

.text
# Функция возвращает число умноженное на 10
# Параметр:
# %rdi - число, для умножения на 10
# Результат функции возвращается через регистр %rax
ten:
    movq arg, %rax      # результат в %rax
    mulq %rdi          # %rax = %rax *%rdi
    ret

Попробуем скомпилировать этот файл в разделяемую библиотеку libten.so:

root@Eugene:~/asm# gcc -shared ten.s -o libten.so

В этом случае мы можем столкнуться с ошибкой:

root@Eugene:~/asm# gcc -shared ten.s -o libten.so
/usr/bin/ld: /tmp/ccBOOTZZ.o: relocation R_X86_64_32S against `.data' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: failed to set dynamic section sizes: bad value
collect2: error: ld returned 1 exit status
root@Eugene:~/asm#

Теперь исправим код файла:

.globl ten

.data
arg: .quad 10

.text
ten:
    movq arg(%rip), %rax
    mulq %rdi          # %rax = %rax *%rdi
    ret

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

movq arg(%rip), %rax

И если мы повторим компиляцию библиотеки, то у нас не возникнет никаких проблем:

gcc -shared ten.s -o libten.so

Используем эту библиотеку, например, в файле hello.s:

.globl main
 
.data
message: .asciz "%d * 10 = %d\n"  # строка форматирования
num: .quad 5
.text
main:
    subq $8, %rsp           # выравнивание должно быть по 16 байтам

    movq num(%rip), %rdi    # параметр функции ten - загрузка относительно регистра %rip
    call ten                # вызываем функцию ten

    leaq message(%rip), %rdi    # первый параметр функции printf - строка форматирования
    movq num(%rip), %rsi       # второй параметр функции printf - первый аргумент строки форматирования
    movq %rax, %rdx         # третий параметр функции printf - второй аргумент строки форматирования
    call printf

    addq $8, %rsp
    ret

Скомпилируем программу:

gcc -rdynamic hello.s -lten -L . -o hello

Затем мы можем ее выполнить. Полный вывод компиляции и выполнения программы:

root@Eugene:~/asm# gcc -rdynamic hello.s -lten -L . -o hello
root@Eugene:~/asm# export LD_LIBRARY_PATH=.
root@Eugene:~/asm# ./hello
5 * 10 = 50
root@Eugene:~/asm#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850