§7.4. Что содержит файл программы

Интерпретация и компиляция

Интерпретацией программы называется непосредственное исполнение её исходного кода (кода, написанного программистом) интерпретатором языка — программой, которая считывает исходный код и выполняет предписанные в нём действия.

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

Компиляция — необратимое преобразование: так, имена переменных и функций в скомпилированный файл не включаются, а сложные вложенные блоки условий и циклов преобразуются в простую и прямолинейную последовательность простейших команд, между которыми совершаются переходы. Кроме того, компилятор может оптимизировать некоторые действия: например, если в программе есть строка a = a + 24 * 60 * 60, в машинный код попадёт единственное действие — прибавление готового числа 86400; под силу компиляторам и гораздо более сложные оптимизации.

Разобраться в машинном коде, конечно, возможно, но почти всегда это гораздо сложнее, чем изучить исходный код. Иногда в этом помогают декомпиляторы — программы, которые пытаются по машинному коду получить эквивалентный исходный. Код, порождённый декомпилятором, может быть мало похож на тот, который компилировали, однако даже в таком коде что-то понять зачастую гораздо легче.

Интерпретируемыми языками программирования называют языки, программы на которых обычно интерпретируют. Это знакомые вам по предыдущим главам Python и JavaScript; bash и язык .bat-файлов; используемые в веб-разработке PHP и Ruby. Программы же на компилируемых языках, как правило, компилируют. К таким языкам относят Си и Си++; Паскаль и его многочисленные вариации; современные языки Go и Rust.

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

Программы на языке Java компилируются в так называемый байткод. Байткод похож на машинный код: это последовательность простейших команд, набор которых сильно ограничен. Однако, в отличие от машинного кода, который может быть различным для каждого вида процессоров, байткод универсален и может работать на любом компьютере. Потребуется лишь исполнитель байткода, называемый виртуальной машиной Java, или JVM (англ. Java Virtual Machine). Байткод как бы является машинным кодом для виртуальной машины Java, и она выполняет его, транслируя (переводя) инструкции байткода в инструкции того процессора, на котором она запущена.

Некоторые реализации Python преобразуют исходный код в промежуточное представление, которое может кешироваться в файлы с расширением .pyc. Оно по своему устройству похоже на байткод Java и выполняются подобной виртуальной машиной.

Исполняемые файлы

Исполняемые файлы для Windows называются PE (англ. Portable Executable). Они начинаются с букв MZ — именно они, наряду с расширением .exe и некоторыми другими, служат для системы признаком, что файл можно выполнить.

Основной формат бинарных файлов программ для Linux — ELF (англ. Executable and Linkable Format). Кроме того, текстовые файлы, начинающиеся с #!, также предназначены для исполнения: в первой строке такого файла указывается, каким интерпретатором следует его исполнять, далее следует исходный код на соответствующем языке. Часто в таком формате можно встретить шелл-скрипты или программы на Python.

В Linux исполнимость файла определяется его атрибутами в файловой системе — а именно, правами доступа. Если пользователь, согласно настройкам доступа к файлу, имеет право его исполнять, то файл может быть запущен. Для запуска нужно обязательно указать не только имя, но и пусть к файлу: так, если файл находится в текущей директории и называется server, то запустить его проще всего так: ./server. Если это файл формата ELF или одного из других форматов, распознаваемых системой, он будет выполнен. Если распознать формат не получилось, шелл попытается проинтерпретировать его как шелл-скрипт.

Что содержит бинарный файл

Бинарные файлы состоят из секций различного назначения. В ELF-файлах обычно представляют интерес секции .text (содержащая собственно машинный код), а также секции .data и .rodata (содержащие данные — изменяемые и неизменяемые соответственно — которые загружаются в память вместе с программой). Изучить содержимое ELF-файла и извлечь из него данные помогут утилиты objdump, readelf и dumpelf.

В PE-файлах можно найти аналогичные секции (впрочем, их названия могут несколько отличаться). Представляет интерес секция .rsrc: в отличие от .data и .rodata, содержащих, как правило, все необходимые программе данные подряд без определённой структуры, .rsrc представляет собой целый структурированный каталог ресурсов. PE-файлы под Windows можно исследовать утилитами, такими как NikPEViewer; каталог ресурсов они позволяют обозревать достаточно наглядно.

Не следует забывать и про уже знакомый вам binwalk: порой он может обнаружить в программе именно то, что вам нужно.

Если не на чем запустить

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

Разумеется, вы всегда можете установить нужную систему в виртуальную машину и запустить файл в ней. Однако такое решение может не устраивать: это требует значительного количества оперативной памяти и места на диске; если программе для работы необходимы какие-то данные, сеть или что-либо ещё, придётся обеспечивать всё это в виртуальной машине; да и использовать (и исследовать) программу внутри виртуальной машины будет, скорее всего, менее удобно. Существуют и менее громоздкие способы, однако, не дающие стопроцентной гарантии успеха.

Если у вас Windows, а вы хотите исполнить ELF-файл, возможно, вам поможет WSL — Windows Subsystem for Linux. Если же вы из-под Linux хотите исполнить Windows-программу, вам поможет программа Wine. И тот, и другой механизм разбирают и выполняют файлы неродного для системы формата, предоставляя совместимую реализацию системных вызовов, которые могут потребоваться программе, и необходимые для этого условия. Например, Windows-приложение может ожидать наличия в системе диска C: — и Wine при попытке приложения открыть файл с диска C: открывает файл в директории ~/.wine/drive_c/ — и приложение работает так, как если бы оно открыло настоящий файл на настоящем диске C:.

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

Выводы

  1. Программы можно интерпретировать и компилировать — выбор обычно зависит от языка. Компиляция — необратимое преобразование, однако в чём-то может помочь декомпилятор.

  2. Основные форматы исполняемых файлов — ELF (для Linux) и PE (для Windows). На Linux исполняемыми файлами также являются скрипты на различных языках, начинающиеся с #!.

  3. Бинарные файлы состоит из секций — каждая со своим назначением. Содержимое секций можно исследовать.

  4. Выполнить программу в формате другой системы можно с помощью не только виртуальных машин, но и механизмов совместимости: Windows Subsystem for Linux и Wine. Иногда это помогает исследовать программу.

§7.5*. Память, Си и ассемблер ⟶