Ответы:
Другая причина, по которой компиляторы производят сборку, а не правильный машинный код:
add eax,2
может быть переведен в 83 c0 02
или в 66 83 c0 02
зависимости от последней появившейся директивы, например use16
.
Компилятор обычно конвертирует высокоуровневый код непосредственно в машинный язык, но он может быть построен модульным образом, так что один бэкэнд генерирует машинный код, а другой - ассемблерный (например, GCC). На этапе генерации кода создается «код», представляющий собой некоторое внутреннее представление машинного кода, который затем необходимо преобразовать в пригодный для использования формат, такой как машинный язык или ассемблерный код.
Исторически ряд известных компиляторов выводили машинный код напрямую. Однако с этим есть некоторые трудности. Обычно кому-то, кто пытается подтвердить, что компилятор работает правильно, будет легче исследовать вывод кода сборки, чем машинный код. Кроме того, возможно (и было исторически распространено) использовать однопроходный компилятор C или Pascal для создания файла на языке ассемблера, который затем может быть обработан с использованием двухпроходного ассемблера. Непосредственное создание кода потребовало бы либо использования двухпроходного компилятора C или Pascal, либо использования однопроходного компилятора, за которым следовали некоторые способы обратного исправления адресов прямого перехода [если среда выполнения делает размер запущенной программы доступным в фиксированное место, компилятор может написать список исправлений в конце кода и заставить код запуска применять эти исправления во время выполнения; такой подход увеличил бы размер исполняемого файла примерно на четыре байта на точку исправления, но улучшил бы скорость генерации программы].
Если цель состоит в том, чтобы иметь быстро работающий компилятор, прямая генерация кода может работать хорошо. Однако для большинства проектов стоимость генерации кода на ассемблере и его сборки в настоящее время не является серьезной проблемой. Наличие компиляторов, создающих код в форме, которая может хорошо взаимодействовать с кодом, созданным другими компиляторами, обычно является достаточно большим преимуществом, чтобы оправдать увеличение времени компиляции.
Даже платформы, которые используют один и тот же набор команд, могут иметь разные перемещаемые форматы объектных файлов. Я могу думать о «a.out» (ранний UNIX), OMF, MZ (MS-DOS EXE), NE (16-битная Windows), COFF (UNIX System V), Mach-O (OS X и iOS) и ELF (Linux и другие), а также их варианты, такие как XCOFF (AIX), ECOFF (SGI) и переносимый исполняемый файл (PE) на основе COFF в 32-битной Windows. Компилятору, который создает язык ассемблера, не нужно много знать о форматах объектных файлов, что позволяет ассемблеру и компоновщику инкапсулировать эти знания в отдельный процесс.
См. Также Разница между OMF и COFF при переполнении стека.
Обычно компиляторы работают внутренне с последовательностями инструкций. Каждая инструкция будет представлена структурой данных, представляющей ее имя операции, операнды и так далее. Когда операнды являются адресами, эти адреса обычно будут символическими ссылками, а не конкретными значениями.
Вывод ассемблера относительно прост. Это в значительной степени вопрос взятия внутренней структуры данных компилятора и вывода ее в текстовый файл в определенном формате. Вывод ассемблера также относительно легко читается, что полезно, когда вам нужно проверить, что делает компилятор.
Вывод двоичных объектных файлов - это значительно больше работы. Автор компилятора должен знать, как кодируются все инструкции (что может быть далеко не тривиально на некоторых CPUS), он должен преобразовывать некоторые символические ссылки в относительные адреса счетчиков программ и другие в метаданные некоторого вида в двоичном объектном файле. , Они должны записать все в формате, который сильно зависит от системы.
Да, вы, безусловно, можете создать компилятор, который может выводить двоичные объекты напрямую, не выписывая ассемблер в качестве промежуточного шага. Вопрос, как и многие другие в разработке программного обеспечения, заключается в том, стоит ли сокращение времени компиляции дополнительной работы по разработке и сопровождению.
Компилятор, с которым я наиболее знаком (freepascal), может выводить ассемблер на всех платформах, но может выводить только двоичные объекты непосредственно на подмножестве платформ.
Компилятор должен иметь возможность создавать выходные данные ассемблера в дополнение к обычному перемещаемому коду для пользы программиста.
Однажды я просто не нашел ошибку в программе на C, работающей в Unix System V на компьютере с LSI-11. Казалось, ничего не работает. Наконец в отчаянии у меня был переносимый компилятор C, выделяющий версию его перевода на ассемблере. Я наконец нашел ошибку! Компилятор выделял больше регистров, чем было в машине! (Компилятор разместил регистры с R0 по R8 на машине только с регистрами с R0 по R7.) Мне удалось обойти ошибку в компиляторе, и моя программа сработала.
Еще одним преимуществом вывода ассемблера является попытка использовать «стандартные» библиотеки, которые используют другие протоколы передачи параметров. Более поздние компиляторы C позволяют мне устанавливать протокол с параметром («pascal» заставляет компилятор добавлять параметры в указанном порядке, а не в стандарте C, меняющем порядок).
Еще одно преимущество позволяет программисту видеть, какую ужасную работу выполняет его компилятор. Простое утверждение C занимает около 44 машинных инструкций. Значения загружаются из памяти, а затем быстро удаляются. и т. д., и т. д. ...
Я лично считаю, что иметь компилятор вместо перемещаемого объектного модуля действительно глупо. При компиляции вашей программы компилятор собирает много информации о вашей программе. Обычно эта информация хранится в так называемой Таблице символов. После выделения кода ассемблера он выбрасывает всю эту информационную таблицу. Затем ассемблер проверяет выделенный код и повторно собирает часть информации, которая уже была у компилятора. Однако ассемблеру ничего не известно об операторах If в операторах For и операторах While. Так что вся эта информация отсутствует. Затем ассемблер создает перемещаемый объектный модуль, который компилятор не сделал.
Почему???