Модификация двоичного файла во время исполнения

10

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

Тем не менее, скажем, a.outбыл огромный файл, возможно, сравнимый с размером оперативной памяти. Что будет в этом случае? И скажите, что это связано с общим объектным файлом libblas.so, что, если я изменил libblas.soво время выполнения? Что случилось бы?

Мой главный вопрос - гарантирует ли ОС, что при запуске a.outисходный код всегда будет работать нормально, в соответствии с исходным двоичным .soфайлом , независимо от размера двоичного файла или файлов, на которые он ссылается, даже если эти файлы .oи .soфайлы были изменены во время во время выполнения?

Я знаю, что есть эти вопросы, которые касаются подобных проблем: /programming/8506865/when-a-binary-file-runs-does-it-copy-its-entire-binary-data-into-memory -при однократный Что происходит при редактировании сценария во время выполнения? Как сделать живое обновление во время работы программы?

Что помогло мне понять немного больше об этом, но я не думаю, что они спрашивают, что именно я хочу, что является общим правилом для последствий изменения двоичного файла во время выполнения

texasflood
источник
Мне вопросы, которые вы связали (особенно вопрос переполнения стека), уже оказывают существенную помощь в понимании этих последствий (или их отсутствия). Поскольку ядро ​​загружает вашу программу в текстовые области / сегменты памяти , на нее не должны влиять изменения, сделанные через файловую подсистему.
Джон У. С. Смит,
@JohnWHSmith В Stackoverflow верхний ответ гласит if they are read-only copies of something already on disc (like an executable, or a shared object file), they just get de-allocated and are reloaded from their source, поэтому у меня сложилось впечатление, что если ваш двоичный файл огромен, то если часть вашего двоичного файла выходит из ОЗУ, но затем требуется снова, он «перезагружается из источника» - поэтому любые изменения в .(s)oфайл будет отражен во время выполнения. Но, конечно, я, возможно, неправильно понял - вот почему я
задаю
@JohnWHSmith Также во втором ответе говорится No, it only loads the necessary pages into memory. This is demand paging., что у меня сложилось впечатление, что то, что я просил, не может быть гарантировано.
texasflood

Ответы:

11

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

Как вы, возможно, знаете, системы UNIX обычно делятся на две подсистемы: файловую подсистему и подсистему процессов. Теперь, если в системном вызове не указано иное, ядро ​​не должно взаимодействовать между этими двумя подсистемами. Однако есть одно исключение: загрузка исполняемого файла в текстовые области процесса . Конечно, можно утверждать, что эта операция также запускается системным вызовом ( execve), но обычно известно, что это тот случай, когда подсистема процесса делает неявный запрос к файловой подсистеме.

Поскольку у подсистемы процессов, естественно, нет способа обработки файлов (иначе не было бы смысла делить все это на две части), она должна использовать все, что файловая подсистема предоставляет для доступа к файлам. Это также означает, что подсистема процесса подчиняется любым мерам, которые файловая подсистема принимает в отношении редактирования / удаления файла. На данный момент, я рекомендую прочитать ответ Жиля на этот U & L вопрос . Остальная часть моего ответа основана на более общем ответе Жиля.

Первое, что следует отметить, это то, что внутренне файлы доступны только через inode . Если ядру дан путь, его первым шагом будет преобразование его в inode, который будет использоваться для всех других операций. Когда процесс загружает исполняемый файл в память, он делает это через свой инод, который был предоставлен файловой подсистемой после преобразования пути. Иноды могут быть связаны с несколькими путями (ссылками), а программы могут удалять только ссылки. Чтобы удалить файл и его индекс, пользовательская область должна удалить все существующие ссылки на этот индекс и убедиться, что он полностью не используется. Когда эти условия будут выполнены, ядро ​​автоматически удалит файл с диска.

Если вы посмотрите на заменяющую исполняемую часть ответа Жиля, вы увидите, что в зависимости от того, как вы редактируете / удаляете файл, ядро ​​будет реагировать / адаптироваться по-разному, всегда через механизм, реализованный в файловой подсистеме.

  • Если вы попробуете первую стратегию ( открыть / усечь до нуля / записать или открыть / записать / усечь до нового размера ), вы увидите, что ядро ​​не потрудится обработать ваш запрос. Вы получите ошибку 26: Текстовый файл занят ( ETXTBSY). Никаких последствий.
  • Если вы попробуете стратегию два, первым шагом будет удаление вашего исполняемого файла. Однако, поскольку он используется процессом, файловая подсистема включается и не позволяет файлу (и его inode) быть действительно удаленным с диска. С этой точки зрения, единственный способ получить доступ к содержимому старого файла - это сделать его через его inode, что и делает подсистема процесса всякий раз, когда ей нужно загрузить новые данные в текстовые разделы (внутренне нет смысла использовать пути, кроме при переводе их в иноды). Даже если вы не связаныфайл (удалил все его пути), процесс все еще может использовать его, как будто вы ничего не сделали. Создание нового файла со старым путем ничего не меняет: новому файлу будет предоставлен совершенно новый индекс, о котором исполняющий процесс не знает.

Стратегии 2 и 3 также безопасны для исполняемых файлов: хотя выполняемые исполняемые файлы (и динамически загружаемые библиотеки) не являются открытыми файлами в смысле наличия файлового дескриптора, они ведут себя очень похожим образом. Пока какая-то программа выполняет код, файл остается на диске даже без записи в каталоге.

  • Стратегия три очень похожа, так как mvоперация атомарная. Это, вероятно, потребует использования renameсистемного вызова, и, поскольку процессы не могут быть прерваны в режиме ядра, ничто не может помешать этой операции, пока она не завершится (успешно или нет). Опять же, никакого изменения inode старого файла не происходит: создается новый, и уже запущенные процессы не будут знать об этом, даже если он связан с одной из ссылок старого inode.

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

Перекомпиляция файла : при использовании gcc(и поведение, вероятно, аналогичное для многих других компиляторов), вы используете стратегию 2. Это можно увидеть, запустив straceпроцессы вашего компилятора:

stat("a.out", {st_mode=S_IFREG|0750, st_size=8511, ...}) = 0
unlink("a.out") = 0
open("a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
chmod("a.out", 0750) = 0
  • Компилятор определяет , что файл уже существует через statи lstatсистемные вызовы.
  • Файл не связан . Здесь, хотя он больше не доступен через имя a.out, его индекс и содержимое остаются на диске до тех пор, пока они используются уже запущенными процессами.
  • Новый файл создается и исполняется под именем a.out. Это совершенно новый инод и совершенно новое содержимое, о котором уже не заботятся уже запущенные процессы.

Теперь, когда дело доходит до разделяемых библиотек, применяется то же самое поведение. Пока объект библиотеки используется процессом, он не будет удален с диска, независимо от того, как вы измените его ссылки. Всякий раз, когда что-то должно быть загружено в память, ядро ​​будет делать это через inode файла и, следовательно, будет игнорировать изменения, внесенные вами в его ссылки (например, связать их с новыми файлами).

Джон У. Смит
источник
Фантастический, подробный ответ. Это объясняет мою путаницу. Поэтому я прав, полагая, что, поскольку индекс по-прежнему доступен, данные из исходного двоичного файла все еще находятся на диске, поэтому использование dfколичества свободных байтов на диске с использованием неправильного метода, так как он не принимает иноды, которые все ссылки на файловые системы удалены? Так я должен использовать df -i? (Это просто техническое любопытство, мне не нужно точно знать точное использование диска!)
texasflood
1
Просто чтобы прояснить для будущих читателей - мое замешательство заключалось в том, что при выполнении я думал, что весь двоичный файл будет загружен в ОЗУ, поэтому, если ОЗУ будет маленьким, то часть двоичного файла покинет ОЗУ и будет перезагружена с диска - что вызвать проблемы, если вы изменили файл. Но из ответа стало ясно, что двоичный файл действительно никогда не удаляется с диска, даже если вы rmили mvего как inode в исходный файл не удаляете, пока все процессы не удалят свою ссылку на этот inode.
texasflood
@texasflood Точно. Как только все пути были удалены, ни один новый процесс ( dfвключенный) не может получить информацию об индексе. Какую бы новую информацию вы не нашли, она связана с новым файлом и новым индексом. Основной момент здесь заключается в том, что подсистема процессов не заинтересована в этой проблеме, поэтому понятия управления памятью (подкачки по требованию, подкачки процессов, сбоев страниц и т. Д.) Совершенно неактуальны. Это проблема файловой подсистемы, и она решается файловой подсистемой. Подсистема процессов не беспокоится об этом, это не то, для чего она здесь.
Джон У. С. Смит,
@texasflood Примечание df -i: этот инструмент, вероятно, извлекает информацию из суперблока fs или его кэша, что означает, что он может включать в себя inode старого двоичного файла (для которого все ссылки были удалены). Это не означает, что новые процессы могут свободно использовать эти старые данные.
Джон У. С. Смит,
2

Насколько я понимаю, из-за сопоставления памяти запущенного процесса ядро ​​не позволит обновить зарезервированную часть сопоставленного файла. Я предполагаю, что если процесс запущен, то весь его файл зарезервирован, следовательно, его обновление, потому что вы скомпилировали новую версию своего исходного кода, фактически приводит к созданию нового набора inode. Короче говоря, старые версии вашего исполняемого файла остаются доступными на диске через события сбоя страницы. Так что даже если вы обновляете огромный файл, он должен оставаться доступным и ядро должно еще увидеть нетронутую версию до тех пор , пока процесс запущен. Исходные файловые индексы не должны использоваться повторно до тех пор, пока выполняется процесс.

Это, конечно, должно быть подтверждено.


источник
2

Это не всегда так при замене файла .jar. Ресурсы Jar и некоторые загрузчики классов отражения времени выполнения не читаются с диска, пока программа явно не запросит информацию.

Это только проблема, потому что jar - это просто архив, а не отдельный исполняемый файл, который отображается в памяти. Это немного неестественно, но все еще является ответом на ваш вопрос и кое-чем, с чем я выстрелил себе в ногу.

Так что для исполняемых файлов: да. Для файлов JAR: возможно (в зависимости от реализации).

Zhro
источник