Компоновщик GCC: переместить символ в указанный раздел

Можно ли переместить некоторые функции в коде в определенный раздел исполняемого файла? Если да, то как?

Для приложения, скомпилированного с помощью gcc, у нас есть больше исходных файлов, включая X.c. Каждый объект компилируется из связанного источника (X.o получается из X.c), а компоновщик создает большой исполняемый файл.

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

Моя проблема в том, что я не могу изменить исходный X.c, иначе я бы использовал определенный флаг, например __attribute__ ((section ("magic_section"))) для функций.

Я кое-что прочитал в документации по компоновщику и написал собственный сценарий для компоновщика, но я не удалось указать, в каком разделе должен быть размещен конкретный символ. Мне удалось переместить только целый раздел.


person MathPlayer    schedule 24.03.2015    source источник


Ответы (2)


По пути вы могли бы это сделать (не очень хорошо, но теоретически должно работать) - использовать --function-sections и --data-sections, предполагая, что ваша версия / архитектура GCC поддерживает эти параметры, а затем вручную вызвать все функции и переменные, которые должны войти заданный файл со сценарием компоновщика.

Это создает разделы, называемые как вещи .text.function_name или .data.variable_name. Если вы знакомы с назначением разделов с помощью атрибутов gcc, я предполагаю, что вы знаете, что делать в компоновщике.

В качестве преимущества это позволит вам выбирать функции, если вы на самом деле не хотите, чтобы весь файл помещался в волшебный раздел.

person Brian McFarland    schedule 24.03.2015
comment
Привет! Отличный комментарий, я не знал о свойствах --data-section и --function-section. Но как вы это используете, если не вы компилируете код? - person Ishay Peled; 24.03.2015
comment
@IshayPeled - я думал, что MathPlayer компилирует код, и ему просто не разрешено изменять исходный код. Такая ситуация может возникнуть, например, когда у вас есть лицензия с полным исходным кодом на коммерческое программное обеспечение для целей отладки, но вы аннулируете гарантию и / или нарушаете условия использования, если вы их измените. Сгенерированный компьютером исходный код - еще одна распространенная причина, по которой не следует изменять файл. - person Brian McFarland; 24.03.2015
comment
@IshayPeled - к ​​сведению, --function-sections чаще всего используется для удаления мертвого кода путем объединения его с опцией --gc-sections компоновщика binutils (ld). Я никогда не видел, чтобы он использовался для позиционирования символов, но я не вижу причин, по которым это не сработает. - person Brian McFarland; 24.03.2015
comment
Спасибо за вклад! Похоже, это не решит эту конкретную проблему, но я определенно вижу, что буду использовать это в будущем. - person Ishay Peled; 24.03.2015
comment
@Brian McFarland Да, я полностью контролирую компиляцию кода, и мне не разрешено изменять некоторые источники, они довольно большие, и мне нужно оптимизировать вызовы только для двух функций, поэтому это решение кажется мне идеальным. - person MathPlayer; 25.03.2015

К сожалению, без изменения ваших двоичных объектов, динамического компоновщика или динамического загрузчика вы не сможете этого сделать, и в любом случае это очень сложная задача.

Вариант 1 - манипуляции с ELF

Каждый исполняемый файл ELF состоит из разделов, которые содержат фактический код / ​​данные / строки символов / ... и сегменты, которые помогают загрузчику решать такие вещи, как, например, куда загрузить ваш код в память, какие символы предоставляет этот ELF, какие символы он требует от другие места, где загрузить определенный код / ​​данные и т. д.

Вы можете наблюдать за сегментами в вашем двоичном файле, набрав

readelf -l [ваш двоичный файл]

Результат будет похож на следующий (я выбрал ls как двоичный):

[ishaypeled @ ishay-dev bin] $ readelf -l --wide ./ls

Elf file type is EXEC (Executable file)
Entry point 0x4048bf
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R E 0x8
  INTERP         0x000238 0x0000000000400238 0x0000000000400238 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x01b694 0x01b694 R E 0x200000
  LOAD           0x01bdf0 0x000000000061bdf0 0x000000000061bdf0 0x000864 0x0016d0 RW  0x200000
  DYNAMIC        0x01be08 0x000000000061be08 0x000000000061be08 0x0001f0 0x0001f0 RW  0x8
  NOTE           0x000254 0x0000000000400254 0x0000000000400254 0x000044 0x000044 R   0x4
  GNU_EH_FRAME   0x01895c 0x000000000041895c 0x000000000041895c 0x00071c 0x00071c R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x01bdf0 0x000000000061bdf0 0x000000000061bdf0 0x000210 0x000210 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 

Теперь давайте рассмотрим этот вывод:

В первой таблице (Заголовки программ):
[Тип] - Тип сегмента, для чего предназначен этот раздел
[Смещение] - Смещение в файле, где начинается этот сегмент
[VirtAddr] - Где мы хотим для загрузки этого раздела в адресное пространство процесса (если этот сегмент должен быть загружен вообще, загружаются не все из них)
[PhysAddr] - То же, что и VirtAddr для всех современных ОС, с которыми я сталкивался
[FileSiz] - Насколько велик есть ли этот раздел в файле. Это ссылка на ваши разделы - текущий сегмент состоит из всех разделов в диапазоне от Offset to Offset + FileSiz
[MemSiz] - Насколько велик этот раздел в виртуальной памяти (это НЕ обязательно должно быть таким же, как размер в файле! если он превышает размер файла, избыток устанавливается в 0)
[Flg] - Флаги разрешений, R-чтение E-выполнение W-запись. [Align] - Требуется выравнивание памяти в памяти.

Ваше внимание сосредоточено на сегментах типа LOAD (PT_LOAD). Эти сегменты группируют данные из разделов, указывают загрузчику, куда их поместить в адресное пространство процесса, и определяют их разрешения.

Вы можете увидеть удобный раздел для сопоставления сегментов в таблице сопоставления раздела с сегментами.

Давайте рассмотрим два сегмента LOAD 2 и 3:
Мы видим, что сегмент 2 имеет разрешения на чтение и выполнение и что он охватывает (среди прочего) разделы .text и .rodata.

Итак, для достижения вашей цели с помощью манипуляции с ELF:

  1. Найдите двоичные данные, которые делают ваши функции в файле (утилита readelf - ваш друг)
  2. Изменив заголовок ELF (я не знаю ни одного инструмента, который делает это автоматически, вам, вероятно, придется написать свой собственный), разделите сегмент, содержащий раздел .text, на два последовательных сегмента LOAD, оставив свой код функции
  3. Изменив заголовок ELF, создайте новый сегмент LOAD, содержащий только две ваши функции.
  4. Обновите все ссылки (если есть) со старого местоположения функции на новое.

Если вы дочитали до этого места и все поняли, вы должны знать, что это чрезвычайно утомительная и практически невыполнимая задача для реальных случаев.

Вариант 2 - Динамическое изменение компоновщика. Обратите внимание на тип сегмента INTERP в приведенном выше примере. Это строка ASCII, которая указывает, какой динамический компоновщик вам следует использовать.
Роль динамического компоновщика заключается в анализе сегментов и выполнении всех динамических операций, таких как разрешение символов во время выполнения, загрузка сегментов из файла .so и т. Д.

Возможной манипуляцией здесь может быть изменение кода динамического компоновщика (ПРИМЕЧАНИЕ: это изменение всей системы!) Для загрузки двоичных данных функций в определенный адрес памяти в адресном пространстве процесса. Обратите внимание, что у этого подхода есть несколько недостатков:

  1. Требуется модификация динамического компоновщика
  2. Вам необходимо обновить все ссылки на ваши функции в файле ELF.

Вариант 3 - манипуляции с динамическим загрузчиком. Очень похоже на вариант 2, но изменяет возможности библиотеки ld вместо динамического компоновщика.

Заключение. То, что вы хотите делать, - очень сложная и утомительная задача. В настоящий момент я работаю над инструментом, который позволяет внедрять произвольные функции в существующие общие объектные файлы, и я гарантирую, что это займет как минимум несколько хороших недель работы.
Вы уверены, что нет другого способа добиться того, что ты хочешь? Зачем вам эти две функции по отдельному адресу? Возможно, есть решение попроще ...

person Ishay Peled    schedule 24.03.2015
comment
Я пометил другой ответ как принятый, потому что для моей конкретной проблемы это проще исправить. Как ни странно, некоторое время назад я работал над подобным проектом, мне пришлось переписать некоторые символы и ввести новый код в существующие объекты, я понимаю, почему вы говорите, что это утомительная задача. - person MathPlayer; 25.03.2015
comment
Да, я не думал, что вы можете скомпилировать код, гораздо проще указать компоновщику, как это сделать, если вы можете. - person Ishay Peled; 25.03.2015