| 
							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
							
						 | 
						
							
								  | 
								
									
									 « Ответ #7 : 29-08-2017 23:11 »   | 
								
								 | 
							  
							 
							ld.so управляет разделяемыми библиотеками. Интерфейс libdl (dlopen, dpsym, dpclose) управляет динамическими библиотеками. Он предназначен для программно управляемой загрузки и выгрузки библиотек типа плагинов. Т.е. когда есть заранее известный интерфейс библиотеки, но он не линкуется автоматически, вместо этого программно получают нужные адреса функций. Динамически загружаемая библиотека также имеет таблицу импорта и может линковаться к символам уже загруженных модулей.
  
						 | 
					 
					
						
							
								| 
								 | 
							 
								| 
								 | 
								
									 
									Записан
								 | 
							  
							 
							... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С. 
						 | 
					 
				 
			 |  
		 
	 | 
	
		
		
			
				
					
						| 
							RXL
							
						 | 
						
							
								  | 
								
									
									 « Ответ #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 »
								 | 
								
									 
									Записан
								 | 
							  
						 | 
					 
				 
			 |  
		 
	 | 
	 |