MasterMan342
|
|
« : 29-08-2017 09:56 » |
|
Привет всем! пытаюсь разобраться в принципе реализации PIC в Unix-системах... Саму суть я понял - за счет того, что код приложения строится относительно IP на точке входа, его можно загрузить по любому адресу без ущерба работоспособности, и без необходимости "переадресации" https://habrahabr.ru/company/badoo/blog/323904/Однако, я не понимаю следующего момента: Если можно заменить любые обращения к данным на относительный код, зачем использовать посредника в виде таблиц GOT и GOT-PLT? Как я понимаю это сделано специально, чтобы каждый процесс имел индивидуальные секции с данными разделяемой библиотеки, да? Тогда как "общий" код должен обращаться к индивидуальным данным? ведь судя по примеру на хабре (и по изложенной мысли) этот самый код будет всегда обращаться к одной и той-же области памяти, содержащей GOT/PLT
|
|
|
Записан
|
|
|
|
darkelf
Молодой специалист
Offline
|
|
« Ответ #1 : 29-08-2017 12:14 » |
|
Как я понимаю это сделано специально, чтобы каждый процесс имел индивидуальные секции с данными разделяемой библиотеки, да? Тогда как "общий" код должен обращаться к индивидуальным данным? ведь судя по примеру на хабре (и по изложенной мысли) этот самый код будет всегда обращаться к одной и той-же области памяти, содержащей GOT/PLT
это реализуется за счёт виртуальной памяти и Copy On Write (COW). В прикладном процессе за загрузку динамических библиотек отвечает ld.so, который выполняет все необходимые действия по загрузке библиотек в память и их настройку. Делает он это используя механизмы ядра такие как mmap(), в котором есть возможность отображать участки файла в адресное пространство процесса. При этом код может отображаться, например, с указанием флага MAP_SHARED, а таблицы - с MAP_PRIVATE. Как я понимаю, MAP_PRIVATE говорит, что несмотря на то, что запрошено разрешение записи в страницы, на самом деле для процессора устанавливаются флаги разрешающие только чтение. Таким образом при попытке записи в память в которых располагаются таблицы (настройка адресов и т.д.), происходит исключение и управление принимает ядро ОС. Оно обнаруживает, что данная страница памяти отображена специфичным способом и выполняет следующие действия: 1) выделяет новую физическую страницу памяти; 2) копирует содержимое исходной страницы в эту новую; 3) для новой страницы выставляет разрешение изменения; 4) отображает новую страницу в адресное пространства прикладного процесса по адресу, по которому была отображена старая; 5) отдаёт управление прикладному процессу. Таким образом для прикладного процесса всё происходит прозрачно, при этом получается, что код библиотеки и самого процесса содержится в физической памяти в единственном экземпляре, а данные, стек, таблицы оказываются свои собственные для каждого из процессов.
|
|
|
Записан
|
|
|
|
Aether
|
|
« Ответ #2 : 29-08-2017 15:09 » |
|
darkelf, Вы не могли бы рассказать, как происходит привязка функций к этой таблице, то есть, как происходит вызов со стороны приложения, и как происходит настройка со стороны загрузчика библиотеки. В Windows есть GetProcAddress, который образует поиск функции по имени, а здесь как?
|
|
|
Записан
|
|
|
|
darkelf
Молодой специалист
Offline
|
|
« Ответ #3 : 29-08-2017 15:12 » |
|
Aether, в unix-ах в библиотеке libdl есть аналогичная функция dlsym(), имеющая интерфейс, полностью аналогичный GetProcAddress()
|
|
|
Записан
|
|
|
|
Aether
|
|
« Ответ #4 : 29-08-2017 15:29 » |
|
Aether, в unix-ах в библиотеке libdl есть аналогичная функция dlsym(), имеющая интерфейс, полностью аналогичный GetProcAddress()
Тогда нужно ли записывать что-то в таблицу смещений функций загружаемой библиотеки? По этой таблице функция dlsym() или аналогичная может найти её смещение, прибавить его к адресу загрузки библиотеки в текущем процессе, и выдать программе его актуальное значение.
|
|
|
Записан
|
|
|
|
darkelf
Молодой специалист
Offline
|
|
« Ответ #5 : 29-08-2017 15:48 » |
|
Надо смотреть исходный код. Если я правильно помню, то ld.so не требует для своей работы libdl. Соответственно, возможно, что всё взаимодействие строится с точностью до наоборот, например libdl просто предоставляет интерфейс, а вся реальная функциональность находится в ld.so, а может ещё как.
Мой рассказ MasterMan342 был в основном про то, как получаются индивидуальные данные для общего кода - т.е. как прикладная часть для этого взаимодействует с ядром ОС.
|
|
|
Записан
|
|
|
|
MasterMan342
|
|
« Ответ #6 : 29-08-2017 20:03 » |
|
Как я понимаю это сделано специально, чтобы каждый процесс имел индивидуальные секции с данными разделяемой библиотеки, да? Тогда как "общий" код должен обращаться к индивидуальным данным? ведь судя по примеру на хабре (и по изложенной мысли) этот самый код будет всегда обращаться к одной и той-же области памяти, содержащей GOT/PLT
это реализуется за счёт виртуальной памяти и Copy On Write (COW). В прикладном процессе за загрузку динамических библиотек отвечает ld.so, который выполняет все необходимые действия по загрузке библиотек в память и их настройку. Делает он это используя механизмы ядра такие как mmap(), в котором есть возможность отображать участки файла в адресное пространство процесса. При этом код может отображаться, например, с указанием флага MAP_SHARED, а таблицы - с MAP_PRIVATE. Как я понимаю, MAP_PRIVATE говорит, что несмотря на то, что запрошено разрешение записи в страницы, на самом деле для процессора устанавливаются флаги разрешающие только чтение. Таким образом при попытке записи в память в которых располагаются таблицы (настройка адресов и т.д.), происходит исключение и управление принимает ядро ОС. Оно обнаруживает, что данная страница памяти отображена специфичным способом и выполняет следующие действия: 1) выделяет новую физическую страницу памяти; 2) копирует содержимое исходной страницы в эту новую; 3) для новой страницы выставляет разрешение изменения; 4) отображает новую страницу в адресное пространства прикладного процесса по адресу, по которому была отображена старая; 5) отдаёт управление прикладному процессу. Таким образом для прикладного процесса всё происходит прозрачно, при этом получается, что код библиотеки и самого процесса содержится в физической памяти в единственном экземпляре, а данные, стек, таблицы оказываются свои собственные для каждого из процессов. т.е. как я вас понял: При загрузке "Shared Library" новым процессом старые страницы "общего кода" выгружаются из памяти (если они есть), и заменяются новыми, настроенными на адресное пространство этого нового процесса (и соответственно знают о местонахождении индивидуальных GOT и PLT)... и так поочередно отображаются в одну и ту-же физическую область памяти по запросу?
|
|
|
Записан
|
|
|
|
RXL
Технический
Администратор
Offline
Пол:
|
|
« Ответ #7 : 29-08-2017 23:11 » |
|
ld.so управляет разделяемыми библиотеками. Интерфейс libdl (dlopen, dpsym, dpclose) управляет динамическими библиотеками. Он предназначен для программно управляемой загрузки и выгрузки библиотек типа плагинов. Т.е. когда есть заранее известный интерфейс библиотеки, но он не линкуется автоматически, вместо этого программно получают нужные адреса функций. Динамически загружаемая библиотека также имеет таблицу импорта и может линковаться к символам уже загруженных модулей.
|
|
|
Записан
|
... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
|
|
|
RXL
Технический
Администратор
Offline
Пол:
|
|
« Ответ #8 : 29-08-2017 23:33 » |
|
|
|
|
Записан
|
... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
|
|
|
darkelf
Молодой специалист
Offline
|
|
« Ответ #9 : 30-08-2017 05:17 » |
|
т.е. как я вас понял: При загрузке "Shared Library" новым процессом старые страницы "общего кода" выгружаются из памяти (если они есть), и заменяются новыми, настроенными на адресное пространство этого нового процесса (и соответственно знают о местонахождении индивидуальных GOT и PLT)... и так поочередно отображаются в одну и ту-же физическую область памяти по запросу?
Не совсем.. реально разделяемые страницы - код процесса, код разделяемых библиотек содержатся в одних и тех-же физических страницах, но могут отображаться на разные виртуальные адреса (например, библиотеки могут быть подгружены по разным адресам, для чего и собственно нужен PIC). Персональные данные - стек, данные и прочие структуры могут наоборот - иметь в двух процессах одинаковые виртуальные адреса, но должны располагаться в разных физических страницах (в противном случае процессы будут разделять между собой и данные и стек, причём при разделении данных получится нечто вроде потоков, а вот при разделении стека ничего хорошего, имхо, не получится). За установление соответствия отвечает ОС, которая для этого специальным образом настраивает MMU процессора.
|
|
« Последнее редактирование: 30-08-2017 05:19 от darkelf »
|
Записан
|
|
|
|
MasterMan342
|
|
« Ответ #10 : 30-08-2017 08:39 » |
|
т.е. как я вас понял: При загрузке "Shared Library" новым процессом старые страницы "общего кода" выгружаются из памяти (если они есть), и заменяются новыми, настроенными на адресное пространство этого нового процесса (и соответственно знают о местонахождении индивидуальных GOT и PLT)... и так поочередно отображаются в одну и ту-же физическую область памяти по запросу?
Не совсем.. реально разделяемые страницы - код процесса, код разделяемых библиотек содержатся в одних и тех-же физических страницах, но могут отображаться на разные виртуальные адреса (например, библиотеки могут быть подгружены по разным адресам, для чего и собственно нужен PIC). Персональные данные - стек, данные и прочие структуры могут наоборот - иметь в двух процессах одинаковые виртуальные адреса, но должны располагаться в разных физических страницах (в противном случае процессы будут разделять между собой и данные и стек, причём при разделении данных получится нечто вроде потоков, а вот при разделении стека ничего хорошего, имхо, не получится). За установление соответствия отвечает ОС, которая для этого специальным образом настраивает MMU процессора. Вот допустим общая часть библиотеки: 0000040c <function>: 0000040c: 55 push %ebp 0000040d: 89 e5 mov %esp,%ebp 0000040f: e8 0e 00 00 00 call 422 <__i686.get_pc_thunk.cx> 00000414: 81 c1 5c 11 00 00 add $0x115c,%ecx 0000041a: 8b 81 18 00 00 00 mov 0x18(%ecx),%eax 00000420: 5d pop %ebp 00000421: c3 ret 00000422 <__i686.get_pc_thunk.cx>: 00000422: 8b 0c 24 mov (%esp),%ecx 00000425: c3 ret Допустим создался некоторый процесс, и динамический компоновщик загрузил эту разделяемую библиотеку (вместе с определенными сегментами данных, стека и кучи библиотеки) в физическую память так, что сегменты (кроме сегмента кода) находятся физически в области памяти процесса, а сегмент кода находится в другой физической области памяти, но проецируется на страницы виртуальной памяти в адресном пространстве каждого процесса: 00001588 <global>: 1588: 64 00 00 add %al,%fs:(%eax) И получается что общий код прозрачно работает с данными, не замечая что они находятся в других областях физической памяти, так?
|
|
|
Записан
|
|
|
|
Aether
|
|
« Ответ #11 : 30-08-2017 09:06 » |
|
Допустим создался некоторый процесс, и динамический компоновщик загрузил эту разделяемую библиотеку в физическую память так, что сегменты находятся физически в области памяти процесса, а сегмент кода находится в другой физической области памяти, но проецируется на страницы виртуальной памяти в адресном пространстве каждого процесса:
Пользовательские процессы вообще не работают с физической памятью, только с виртуальной, поэтому у каждого пользовательского процесса своё пространство. За соответствие пространства процесса и физической памяти отвечает специальный механизм и устройство(MMU). Как только процесс обращается к памяти, MMU по таблице преобразует виртуальный адрес в физический, и запрашивает или записывает туда значение, проверяет привилегии и т.д. Можно настроить так, что одна физическая страница памяти будет отображаться на несколько виртуальных страниц в разных процессах. Причём виртуальные адреса начала этих страниц в каждом процессе будут различными, а физический адрес будет один и тот же. Именно так разделяется код. Данные же у каждого процесса свои, поэтому каждая виртуальная страница данных обычно соответствует отдельной физической странице. По крайней мере, так это понимаю я.
|
|
|
Записан
|
|
|
|
darkelf
Молодой специалист
Offline
|
|
« Ответ #12 : 30-08-2017 09:51 » |
|
Попробую пояснить слова Aether-а рисунками.. сорри, за псевдографику. Можно, не вдаваясь в разделяемые библиотеки, посмотреть на примере статически собранного исполняемого модуля, выполнившего системный вызов fork(). Как все знают, после этого системного вызова в памяти находятся два идентичных процесса. До вызова, очень упрощённо, память выглядит следующим образом: Процесс 1 Физическая память (виртуальные адреса) (физические адреса) ------------------------------------------------------------------ 0x00 --------------------+ 0x01 Отображение секции |\ 0x02 кода программы | \ 0x03 | \ 0x04 --------------------+ \+-------------+ 0x05 Отображение секции |\ | Секция кода | 0x06 данных программы | \ | программы | 0x07 | \ | | 0x08 --------------------+ \+-------------+ 0x09 \ |Секция данных| 0x0a \ | программы | 0x0b \ | | 0x0c \| | 0x0d +-------------+
После вызова: Процесс 1 Физическая память Процесс 2 (виртуальные адреса) (физические адреса) (виртуальные адреса) ------------------------------------------------------------------ 0x00 --------------------+ +------------------- 0x01 Отображение секции |\ /| Отображение секции 0x02 кода программы | \ / | кода программы 0x03 | \ / | 0x04 --------------------+ \+-------------+/ +------------------- 0x05 Отображение секции |\ | Секция кода | /| Отображение секции 0x06 данных программы | \ | программы | //| данных программы 0x07 | \ | | / || 0x08 --------------------+ \+-------------+/ / +------------------- 0x09 \ |Секция данных| |/ 0x0a \ | программы | / | 0x0b \ |для процесса || / 0x0c \| 1 |/| 0x0d +-------------+ / 0x0e |Секция данных| | 0x0f | программы | / 0x10 |для процесса || 0x11 | 2 |/ 0x12 +-------------+
Из рисунков видно, что код (условные физические адреса 0x04 - 0x07) разделяется между процессами и отображается по одним и тем-же условным виртуальным адресам 0x00 - 0x03, а вот данные у каждого из процессов свои - у первого из процессов они располагаются по физическим адресам 0x08 - 0x0c, а у второго по 0x0d - 0x11, при этом они отображаются в одни и те-же виртуальные адреса (0x04 - 0x07) у обоих процессов. Аналогичным способом делается и для разделяемых библиотек, только там наоборот - секция кода библиотеки может размещаться по разным виртуальным адресам в разных процессах, а для настройки на глобальные переменные и функции используются таблицы ptl/got. PS. Возможно лучше это обсуждение перенести в тему "Unix и другие", т.к. пока она больше относится к той теме, чем к ассемблеру как таковому.
|
|
|
Записан
|
|
|
|
MasterMan342
|
|
« Ответ #13 : 30-08-2017 10:47 » |
|
PS. Возможно лучше это обсуждение перенести в тему "Unix и другие", т.к. пока она больше относится к той теме, чем к ассемблеру как таковому.
PLT - это способ кодирования, а не механизм ОС... я создал тему в разделе по ассемблеру не просто так)
|
|
|
Записан
|
|
|
|
Aether
|
|
« Ответ #14 : 30-08-2017 12:38 » |
|
Если разбираться, то с самого начала. Прикреплю файл.
|
|
|
Записан
|
|
|
|
Aether
|
|
« Ответ #15 : 30-08-2017 14:39 » |
|
Не обратил сразу внимание: Вот допустим общая часть библиотеки: 0000040c <function>: 0000040c: 55 push %ebp 0000040d: 89 e5 mov %esp,%ebp 0000040f: e8 0e 00 00 00 call 422 <__i686.get_pc_thunk.cx> 00000414: 81 c1 5c 11 00 00 add $0x115c,%ecx 0000041a: 8b 81 18 00 00 00 mov 0x18(%ecx),%eax 00000420: 5d pop %ebp 00000421: c3 ret
00000422 <__i686.get_pc_thunk.cx>: 00000422: 8b 0c 24 mov (%esp),%ecx 00000425: c3 ret
Если это фрагмент относительного кода, то инструкции: CALL 422 быть не может, то есть её и нет, но написано коряво. Вообще CALL имеет следующие машинные подвиды: E8 cw — CALL rel16 E8 cd — CALL rel32FF /2 — CALL r/m16 // Вот это всё работает с абсолютными значениями. FF /2 — CALL r/m32 // И обычно не используется в относительном коде. 9A cd — CALL ptr16:16 // -//- 9A cp — CALL ptr16:32 // -//- FF /3 — CALL m16:16 // -//- FF /3 — CALL m16:32 // -//- Таким образом, у нас команда: CALL rel32 +0x0E; То есть, 0x422-0x414. А весь код функции похоже нацелен вернуть актуальное значение eip, и по нему как бы привязаться в абсолютных координатах. Если я правильно понял.
|
|
« Последнее редактирование: 30-08-2017 14:56 от Aether »
|
Записан
|
|
|
|
|