Как ядро ​​получает исполняемый двоичный файл, работающий под Linux?

Как ядро ​​получает исполняемый двоичный файл, работающий под Linux?

Кажется, простой вопрос, но кто-то может помочь мне копать глубоко? Как файл загружается в память и как запускается код выполнения?

Может ли кто-нибудь помочь мне и рассказать, что происходит шаг за шагом?

Ответ 1

Лучшие моменты системного вызова exec в Linux 4.0

Лучший способ выяснить это - пошаговая отладка ядра GDB с помощью QEMU: Как отладить ядро Linux с помощью GDB и QEMU?

  • fs/exec.c определяет системный вызов в SYSCALL_DEFINE3(execve

    Просто пересылается в do_execve.

  • do_execve

    do_execveat_common к do_execveat_common.

  • do_execveat_common

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

    Начинает struct linux_binprm *bprm для описания программы и передает ее в exec_binprm для выполнения.

  • exec_binprm

    Еще раз, следуйте возвращаемому значению, чтобы найти следующий основной вызов.

  • search_binary_handler

    • Обработчики определяются первыми магическими байтами исполняемого файла.

      Двумя наиболее распространенными обработчиками являются обработчики для интерпретируемых файлов (#! Magic) и для ELF (\x7fELF magic), но есть и другие встроенные в ядро, например, a.out. И пользователи также могут зарегистрировать свои собственные, хотя /proc/sys/fs/binfmt_misc

      Обработчик ELF определен в fs/binfmt_elf.c.

      См. Также: Почему люди пишут #!/Usr/bin/env pyb shebang в первой строке скрипта Python?

    • Список formats содержит все обработчики.

      Каждый файл обработчика содержит что-то вроде:

      static int __init init_elf_binfmt(void)
      {
          register_binfmt(&elf_format);
          return 0;
      }
      

      и elf_format - это struct linux_binfmt определенная в этом файле.

      __init и помещает этот код в магический раздел, который вызывается при запуске ядра: Что означает __init в коде ядра Linux?

      Инъекция зависимостей на уровне линкера!

    • Существует также счетчик рекурсии, если интерпретатор выполняет себя бесконечно.

      Попробуй это:

      echo '#!/tmp/a' > /tmp/a
      chmod +x /tmp/a
      /tmp/a
      
    • Еще раз мы следуем возвращаемому значению, чтобы увидеть, что будет дальше, и увидеть, что оно исходит из:

      retval = fmt->load_binary(bprm);
      

      где load_binary определен для каждого обработчика в структуре: полиморфизм C-стиля.

  • fs/binfmt_elf.c:load_binary

    Фактическая работа:

    • анализирует файл ELF в соответствии со спецификациями
    • устанавливает начальное состояние программы процесса на основе проанализированного ELF (память в struct linux_binprm, записывается в struct pt_regs)
    • вызовите start_thread, где он действительно может начать планироваться

ТОДО: продолжить исходный анализ. Что я ожидаю, что произойдет дальше:

  • ядро анализирует заголовок INTERP в ELF, чтобы найти динамический загрузчик (обычно устанавливается в /lib64/ld-linux-x86-64.so.2).
  • если он присутствует:
    • ядро отображает динамический загрузчик и ELF, которые будут выполнены в память
    • динамический загрузчик запускается, принимая указатель на ELF в памяти.
    • Теперь в пространстве пользователя, загрузчик как - то разбирает эльф заголовки, и не dlopen на них
    • dlopen использует настраиваемый путь поиска, чтобы найти эти библиотеки (ldd и friends), отобразить их в памяти и каким-то образом сообщить ELF, где найти пропущенные символы
    • загрузчик вызывает _start
  • в противном случае ядро загружает исполняемый файл в память напрямую, без динамического загрузчика.

    Поэтому он должен, в частности, проверить, является ли исполняемый файл PIE или нет, если он помещает его в память в произвольном месте: какова опция -fPIE для независимых от позиции исполняемых файлов в gcc и ld?

Ответ 2

Два системные вызовы из linux kernel являются релевантными, Системный вызов fork (или, возможно, vfork или clone) используется для создания нового процесса, аналогичного вызывающему ( каждый пользовательский процесс Linux, за исключением init создается fork или друзьями). Системный вызов execve заменяет пространство адресов процесса новым (по существу по типу mmap - сегменты из исполняемого и анонимного сегментов ELF, а затем инициализация регистров, включая указатель стека). x86-64 ABI дополнение и сборка Linux howto дают подробности.

Динамическое связывание происходит после execve и включает в себя файл /lib/x86_64-linux-gnu/ld-2.13.so, который для ELF рассматривается как "интерпретатор".

Ответ 3

После прочтения ELF docs уже упоминалось, вы должны просто прочитать код ядра, который на самом деле это делает.

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

Ответ 4

Вы можете начать с понимания исполняемых форматов файлов, таких как ELF. http://en.wikipedia.org/wiki/Executable_and_Linkable_Format

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

Затем я предлагаю прочитать часть Linux, которая загружает двоичные файлы и обрабатывает динамическое связывание, ld-linux. Это также хорошее описание ld-linux: http://www.cs.virginia.edu/~dww4s/articles/ld_linux.html