Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Prosition-Independed Code  (Прочитано 29429 раз)
0 Пользователей и 2 Гостей смотрят эту тему.
MasterMan342
Участник

ru
Offline Offline
Пол: Мужской

« : 29-08-2017 09:56 » 

Привет всем! пытаюсь разобраться в принципе реализации PIC в Unix-системах...
Саму суть я понял - за счет того, что код приложения строится относительно IP на точке входа, его можно загрузить по любому адресу без ущерба работоспособности, и без необходимости "переадресации" https://habrahabr.ru/company/badoo/blog/323904/

Однако, я не понимаю следующего момента:

Если можно заменить любые обращения к данным на относительный код, зачем использовать посредника в виде таблиц GOT и GOT-PLT?
Как я понимаю это сделано специально, чтобы каждый процесс имел индивидуальные секции с данными разделяемой библиотеки, да?
Тогда как "общий" код должен обращаться к индивидуальным данным? ведь судя по примеру на хабре (и по изложенной мысли)
этот самый код будет всегда обращаться к одной и той-же области памяти, содержащей GOT/PLT
Записан
darkelf
Молодой специалист

ua
Offline 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
Специалист

ru
Offline Offline
Пол: Мужской

« Ответ #2 : 29-08-2017 15:09 » 

darkelf, Вы не могли бы рассказать, как происходит привязка функций к этой таблице, то есть, как происходит вызов со стороны приложения, и как происходит настройка со стороны загрузчика библиотеки. В Windows есть GetProcAddress, который образует поиск функции по имени, а здесь как?
Записан
darkelf
Молодой специалист

ua
Offline Offline

« Ответ #3 : 29-08-2017 15:12 » 

Aether, в unix-ах в библиотеке libdl есть аналогичная функция dlsym(), имеющая интерфейс, полностью аналогичный GetProcAddress()
Записан
Aether
Специалист

ru
Offline Offline
Пол: Мужской

« Ответ #4 : 29-08-2017 15:29 » 

Aether, в unix-ах в библиотеке libdl есть аналогичная функция dlsym(), имеющая интерфейс, полностью аналогичный GetProcAddress()
Тогда нужно ли записывать что-то в таблицу смещений функций загружаемой библиотеки? По этой таблице функция dlsym() или аналогичная может найти её смещение, прибавить его к адресу загрузки библиотеки в текущем процессе, и выдать программе его актуальное значение.
Записан
darkelf
Молодой специалист

ua
Offline Offline

« Ответ #5 : 29-08-2017 15:48 » 

Надо смотреть исходный код. Если я правильно помню, то ld.so не требует для своей работы libdl. Соответственно, возможно, что всё взаимодействие строится с точностью до наоборот, например libdl просто предоставляет интерфейс, а вся реальная функциональность находится в ld.so, а может ещё как.

Мой рассказ MasterMan342 был в основном про то, как получаются индивидуальные данные для общего кода - т.е. как прикладная часть для этого взаимодействует с ядром ОС.
Записан
MasterMan342
Участник

ru
Offline Offline
Пол: Мужской

« Ответ #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
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« Ответ #7 : 29-08-2017 23:11 » 

ld.so управляет разделяемыми библиотеками.
Интерфейс libdl (dlopen, dpsym, dpclose) управляет динамическими библиотеками. Он предназначен для программно управляемой загрузки и выгрузки библиотек типа плагинов. Т.е. когда есть заранее известный интерфейс библиотеки, но он не линкуется автоматически, вместо этого программно получают нужные адреса функций. Динамически загружаемая библиотека также имеет таблицу импорта и может линковаться к символам уже загруженных модулей.
Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« Ответ #8 : 29-08-2017 23:33 » 

https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html
Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
darkelf
Молодой специалист

ua
Offline Offline

« Ответ #9 : 30-08-2017 05:17 » 

т.е. как я вас понял:
При загрузке "Shared Library" новым процессом старые страницы "общего кода" выгружаются из памяти (если они есть), и заменяются новыми, настроенными на адресное пространство этого нового процесса (и соответственно знают о местонахождении индивидуальных GOT и PLT)... и так поочередно отображаются в одну и ту-же физическую область памяти по запросу?
Не совсем.. реально разделяемые страницы - код процесса, код разделяемых библиотек содержатся в одних и тех-же физических страницах, но могут отображаться на разные виртуальные адреса (например, библиотеки могут быть подгружены по разным адресам, для чего и собственно нужен PIC). Персональные данные - стек, данные и прочие структуры могут наоборот - иметь в двух процессах одинаковые виртуальные адреса, но должны располагаться в разных физических страницах (в противном случае процессы будут разделять между собой и данные и стек, причём при разделении данных получится нечто вроде потоков, а вот при разделении стека ничего хорошего, имхо, не получится). За установление соответствия отвечает ОС, которая для этого специальным образом настраивает MMU процессора.
« Последнее редактирование: 30-08-2017 05:19 от darkelf » Записан
MasterMan342
Участник

ru
Offline Offline
Пол: Мужской

« Ответ #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
Специалист

ru
Offline Offline
Пол: Мужской

« Ответ #11 : 30-08-2017 09:06 » new

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

За соответствие пространства процесса и физической памяти отвечает специальный механизм и устройство(MMU). Как только процесс обращается к памяти, MMU по таблице преобразует виртуальный адрес в физический, и запрашивает или записывает туда значение, проверяет привилегии и т.д.

Можно настроить так, что одна физическая страница памяти будет отображаться на несколько виртуальных страниц в разных процессах. Причём виртуальные адреса начала этих страниц в каждом процессе будут различными, а физический адрес будет один и тот же. Именно так разделяется код. Данные же у каждого процесса свои, поэтому каждая виртуальная страница данных обычно соответствует отдельной физической странице.

По крайней мере, так это понимаю я.
Записан
darkelf
Молодой специалист

ua
Offline 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
Участник

ru
Offline Offline
Пол: Мужской

« Ответ #13 : 30-08-2017 10:47 » 

PS. Возможно лучше это обсуждение перенести в тему "Unix и другие", т.к. пока она больше относится к той теме, чем к ассемблеру как таковому.
PLT - это способ кодирования, а не механизм ОС... я создал тему в разделе по ассемблеру не просто так)
Записан
Aether
Специалист

ru
Offline Offline
Пол: Мужской

« Ответ #14 : 30-08-2017 12:38 » 

Если разбираться, то с самого начала. Прикреплю файл.

* ELF_Format.pdf (148.78 Кб - загружено 901 раз.)
Записан
Aether
Специалист

ru
Offline Offline
Пол: Мужской

« Ответ #15 : 30-08-2017 14:39 » 

Не обратил сразу внимание:
Цитата: MasterMan342 link=topic=30888.msg303319#msg303319
Вот допустим общая часть библиотеки:
 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 rel32
FF /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 » Записан
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines