При работе с разделяемыми библиотеками основная идея заключается в том, что загрузчик может загружать их в любое место вида памяти. В действительности, это одна из мер безопасности, которые обычно принимают дистрибутивы Linux, — рандомизация адресных пространств, чтобы библиотеки и части приложения можно было загружать в любое место в памяти.
Но это означает, что библиотеки и приложение должны быть написаны специально, чтобы загрузка их в память не приводила к ошибкам. Данный тип кода приложения известен как позиционно-независимый код или PIC (position-independent code).
Теперь рассмотрим, как сделать наш код независимым от позиции. Есть три основные области, где код необходимо изменить, чтобы код был независимым от позиции:
Ссылки на внешние функции
Ссылки на раздел .data библиотеки
Ссылки на внешние объекты
Ссылки на внешние функции обрабатываются компоновщиком/загрузчиком автоматически с помощью таблиц PLT/GOT. Так, вызов call print
заменяется на вызов
call print wrt ..plt
, который обращается по адресу функции printf, указанному в таблице GOT.
call print wrt ..plt
wrt является сокращением от "with regard to" (cс учетом PLT)
Ссылки на адреса в разделе .data библиотеки обрабатываются в режиме адресации, известном как PC-relative addressing или адресация относительно регистра PC (Program Counter) он же регистр rip (Instruction Pointer). Этот режим адресации записывает адреса данных как смещение относительно указателя текущей команды.
Например, пусть у нас есть некоторые данные в секции .data:
section .data message: db "Hello World!",10, 0 count: db $ - message
Для загрузки этих данных в коде применяется адресация относительно rip
. И если глобальные переменные применяются в текущем файле, то для обращения к ним применяется оператор rel. Например, получим значение переменной count
mov rdi, [rel count]
В данном случае мы применяем адресацию относительно регистра RIP. Сама инструкция указывает, что нам нужен адрес переменной count, но закодировать его нужно как смещение относительно указателя инструкции в этом месте. Следовательно, независимо от того, где в памяти загружается код, программа все равно будет знать, где находится переменная count, поскольку это будет фиксированное смещение от текущего места в коде.
Если нам нужен адрес переменной, можно использовать инструкцию lea:
lea rsi, [rel message]
То есть опять же мы получаем адрес переменной message относительно регистра rip.
Если глобальные переменные определены в одном файле (например, приложении), а используются в другом (например в библиотеке или наоборот), то применяется адресация относительно таблицы GOT с помощью выражения:
rel переменная wrt ..got
Например:
mov rsi, [rel message wrt ..got] mov rdx, [rel count wrt ..got]
Таким образом, мы получаем адрес обоих переменных.
Теперь определим независимое от позиции приложение (Position Independent Executable) или PIE (которое используют независимый от позиции код (PIC)). Пусть у нас будет следующий файл print.asm, который определяет функцию print для вывода строки на консоль:
global print:function ; Функция print выводит текст на консоль ; Параметры ; RDI - количество символов ; RSI - ссылка на строку section .text print: mov rdx, rdi mov rdi, 1 mov rax, 1 syscall ret
Эта функция принимает через регистр RSI ссылку на строку и через регистр RDI количество символов строки и с помощью системного вызова номер 1 (системный вызов write) выводит строку на консоль.
Скомпилируем этот файл в динамическую библиотеку print.so
nasm -felf64 print.asm -o print.o ld -shared print.o -o print.so
И пусть в файле app.asm расположен основной код программы, который использует функцию print:
global _start extern print section .data message: db "Hello World!",10, 0 count: db $ - message section .text _start: mov rdi, [rel count] lea rsi, [rel message] call print wrt ..plt mov rax, 60 syscall
Здесь вызывается функция print, в которую передается строка и ее размер из секции .data. Так, в качестве второго параметра в регистр rsi загружается адрес переменной message. И поскольку идет обращение к данным в секции .data, применяется адрес относительно регистра rip:
lea rsi, [rel message]
А для вызова функции print применяется адресация с помощью PLT:
call print wrt ..plt
Для компоновки приложения, независимое от позиции, применяется флаг -pie. Так, скомпилируем объектный файл app.o и файл исполняемого приложения:
nasm -felf64 app.asm -o app.o ld --dynamic-linker=/lib64/ld-linux-x86-64.so.2 -pie app.o print.so -o app
После этого мы можем запускать исполняемый файл app.
Полный вывод:
root@Eugene:~/asm# nasm -felf64 print.asm -o print.o root@Eugene:~/asm# ld -shared print.o -o print.so root@Eugene:~/asm# nasm -felf64 app.asm -o app.o root@Eugene:~/asm# ld --dynamic-linker=/lib64/ld-linux-x86-64.so.2 -pie app.o print.so -o app root@Eugene:~/asm# export LD_LIBRARY_PATH=. root@Eugene:~/asm# ./app Hello World! root@Eugene:~/asm#
Мы также можем проверить заголовок исполняемого файла с помощью команды readelf -h
root@Eugene:~/asm# readelf -h app ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Position-Independent Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x1020 Start of program headers: 64 (bytes into file) Start of section headers: 12800 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 8 Size of section headers: 64 (bytes) Number of section headers: 16 Section header string table index: 15 root@Eugene:~/asm#
В поле Type мы можем увидеть соответствующую запись о типе файл - "Position-Independent Executable file"
Все, что касается приложения, относится и к библиотекам, где применяются такие же способы обращения к функциям и глобальным переменным, как и в исполняемом приложении. Например, изменим код библиотеки print следующим образом:
global print:function extern message ; импортируем строку для вывода extern count ; импортируем размер строки ; Функция print выводит текст на консоль section .text print: mov rsi, [rel message wrt ..got] ; загружаем адрес message из GOT mov rdx, [rel count wrt ..got] ; загружаем адрес count из GOT mov rdx, [rdx] ; загружаем значение count по вышезагруженному адресу mov rdi, 1 mov rax, 1 syscall ret
Здесь библиотека ожидает получить извне строку для вывода на консоль и ее размер в виде глобальных объектов message и count. Для обращения к этим объектам применяется адресация относительно таблицы GOT. Например, получим адрес переменной count:
mov rdx, [rel count wrt ..got]
В GOT хранятся адреса переменных, то есть, чтобы получить значение count, нам надо снова обратиться по полученному адресу:
mov rdx, [rdx]
В коде исполняемого приложения в app.asm определим соответствующие глобальные переменные:
global _start global message:data global count:data extern print section .data count: dq message.end - message message: db "Hello METANIT.COM!",10, 0 .end: section .text _start: call print wrt ..plt mov rax, 60 syscall
Стоит отметить, что для обоих переменных устанавливается тип data
:
global message:data global count:data
Аналогичным образом, как в предыдущем примере, скомпилируем библиотеку и приложение и запустим программу на выполнение:
root@Eugene:~/asm# nasm -felf64 app.asm -o app.o root@Eugene:~/asm# nasm -felf64 print.asm -o print.o root@Eugene:~/asm# ld -shared print.o -o print.so root@Eugene:~/asm# ld --dynamic-linker=/lib64/ld-linux-x86-64.so.2 -pie app.o print.so -o app root@Eugene:~/asm# ./app Hello METANIT.COM! root@Eugene:~/asm#