Что на самом деле компилируют C и Assembler? - PullRequest
46 голосов
/ 26 января 2010

Итак, я обнаружил, что программы на C (++) на самом деле не компилируются в простой «бинарный» (возможно, я ошибся здесь, в этом случае, извините: D), но для ряда вещей (таблица символов, вещи, связанные с ОС, ...) но ...

  • Ассемблер "компилируется" в чистый двоичный файл? Это означает, что нет ничего лишнего, кроме ресурсов, таких как предопределенные строки и т. Д.

  • Если C компилируется в нечто иное, чем простой двоичный файл, как этот маленький загрузчик ассемблера может просто скопировать инструкции с жесткого диска в память и выполнить их? Я имею в виду, если ядро ​​ОС, которое, вероятно, написано на C, компилируется в нечто отличное от простого бинарного файла - как загрузчик справится с этим?

edit: я знаю, что ассемблер не "компилируется", потому что в нем есть только набор инструкций вашей машины - я не нашел хорошего слова для того, что ассемблер "собирает". Если он у вас есть, оставьте его здесь как комментарий, и я его заменю.

Ответы [ 12 ]

43 голосов
/ 26 января 2010

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

Код сборки всегда собирается (не «компилируется») в перемещаемый объектный код . Вы можете думать об этом как о двоичном машинном коде и двоичных данных, но с большим количеством украшений и метаданных. Ключевые части:

  • Код и данные отображаются в именованных «разделах».

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

  • Перемещаемые объектные файлы могут содержать «дыры», которые должны быть заполнены значениями меток, определенных в другом месте. Официальное название для такой дыры - запись о перемещении .

Например, если вы компилируете и собираете (но не связываете) эту программу

int main () { printf("Hello, world\n"); }

вы можете получить перемещаемый объектный файл с

  • A text раздел, содержащий машинный код для main

  • Определение метки для main, которое указывает на начало текстового раздела

  • A rodata (данные только для чтения), содержащие байты строкового литерала "Hello, world\n"

  • Запись о перемещении, которая зависит от printf и указывает на «дыру» в инструкции вызова в середине текстового раздела.

Если вы работаете в системе Unix, перемещаемый объектный файл обычно называется .o-файлом, как в hello.o, и вы можете изучить определения и использование меток с помощью простого инструмента nm, и вы можете получить более подробная информация от более сложного инструмента под названием objdump.

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

36 голосов
/ 26 января 2010

Давайте возьмем программу на C.

Когда вы запускаете 'gcc' или 'cl' в программе c, она проходит следующие этапы:

  1. Лексинг препроцессора (#include, #ifdef, анализ триграфа, переводы кодирования, управление комментариями, макросы ...)
  2. Лексический анализ (создание токенов и лексических ошибок).
  3. Синтаксический анализ (создание синтаксического дерева и синтаксических ошибок).
  4. Семантический анализ (создание таблицы символов, информации об области и ошибок определения / опечатки).
  5. Вывод в сборку (или другой промежуточный формат)
  6. Оптимизация сборки (как указано выше). Вероятно, в строках ASM еще.
  7. Сборка сборки в некоторый двоичный объектный формат.
  8. Связывание сборки с необходимыми статическими библиотеками, а также перемещение при необходимости.
  9. Вывод окончательного исполняемого файла в формате elf или coff.

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

Обратите внимание, что вокруг фактического исполняемого двоичного файла есть «контейнер» в формате elf или coff.

Вы обнаружите, что книга о компиляторах (я рекомендую книгу Dragon , стандартная вводная книга в этой области) будет иметь все необходимую информацию и многое другое.

Как прокомментировал Марко, связывание и загрузка - это большая область, и книга Дракона более или менее останавливается на выходе исполняемого двоичного файла. На самом деле перейти от запуска к операционной системе - довольно сложный процесс, который Левин в компоновщиках и загрузчиках охватывает.

Я викинул этот ответ, чтобы позволить людям подправлять любые ошибки / добавлять информацию.

18 голосов
/ 26 января 2010

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

Исходный код C ++ до ассемблера или промежуточного языка

Некоторые компиляторы фактически переводят код C ++ на язык ассемблера или промежуточный язык. Это не обязательный этап, но он полезен при отладке и оптимизации.

Код сборки объекта

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

Связывание объектного кода (ов)

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

Выполнение Двоичные Файлы

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

Компиляция непосредственно в двоичный файл Некоторые компиляторы, такие как используемые во встроенных системах, имеют возможность компилировать из C ++ непосредственно в исполняемый двоичный код. Этот код будет иметь физические адреса вместо относительных адресов и не требует загрузки ОС.

Преимущества

Одним из преимуществ этих этапов является то, что программы на C ++ можно разбить на части, скомпилировать по отдельности и связать позднее. Они могут даже быть связаны с частями от других разработчиков (a.k.a. библиотеки). Это позволяет разработчикам только компилировать части в разработке и связывать в части, которые уже проверены. В общем, перевод из C ++ в объект является трудоемкой частью процесса. Кроме того, человек не хочет ждать завершения всех фаз, если в исходном коде есть ошибка.

Сохраняйте непредвзятость и всегда ожидайте Третий вариант (опция) .

3 голосов
/ 26 января 2010

Чтобы ответить на ваши вопросы, обратите внимание, что это субъективно, поскольку существуют разные процессоры, разные платформы, разные ассемблеры и компиляторы Си, в этом случае я расскажу о платформе Intel x86.

  1. Ассемблеры не компилируются в чистый двоичный файл, они представляют собой необработанный машинный код, определенный сегментами, такими как данные, текст и bss, но это называется объектным кодом. Линкер вступает и настраивает сегменты, чтобы сделать его исполняемым, то есть готовым к работе. Кстати, вывод по умолчанию при компиляции с использованием gcc - это «a.out», что является сокращением для вывода Ассемблера.
  2. Загрузчики имеют специальную директиву, определенную еще во времена DOS, было бы общепринятым найти директиву, такую ​​как .Org 100h, которая определяет код ассемблера как старый .COM, прежде чем .EXE вступит во владение по популярности. Кроме того, вам не нужно было иметь ассемблер для создания файла .COM, используя старый файл debug.exe, поставляемый с MSDOS, который выполнял небольшие простые программы, файлы .COM не нуждались в компоновщике и были прямо готовы. запустить двоичный формат. Вот простой сеанс с использованием DEBUG.
1:*a 0100
2:* mov AH,07
3:* int 21
4:* cmp AL,00
5:* jnz 010c
6:* mov AH,07
7:* int 21
8:* mov AH,4C
9:* int 21
10:*
11:*r CX
12:*10
13:*n respond.com
14:*w
15:*q

В результате получается готовая к запуску программа .COM с именем 'response.com', которая ожидает нажатия клавиши и не отображает его на экране. Обратите внимание, в начале используется значение «100h», которое показывает, что указатель «Инструкция» начинается с 100h, что является особенностью .COM. Этот старый скрипт в основном использовался в пакетных файлах, ожидая ответа, а не отражая его. Оригинальный скрипт можно найти здесь .

Опять же, в случае с загрузчиками, они конвертируются в двоичный формат, была программа, которая раньше шла с DOS, называемая EXE2BIN . Это была задача преобразования необработанного объектного кода в формат, который можно скопировать на загрузочный диск для загрузки. Помните, что компоновщик не запускает компоновщик, так как компоновщик предназначен для среды выполнения и устанавливает код, чтобы сделать его исполняемым и исполняемым.

BIOS при загрузке ожидает, что код будет в сегменте: смещение, 0x7c00, если моя память мне верна, код (после EXE2BIN'd) начнет выполняться, затем загрузчик переместится ниже в памяти и продолжить загрузку, введя int 0x13 для чтения с диска, включите шлюз A20, включите DMA, переключитесь в защищенный режим, так как BIOS находится в 16-битном режиме, затем данные, считанные с диска, загружаются в память, затем выдается загрузчик далеко прыгнуть в код данных (вероятно, будет написано в C). По сути, это то, как система загружается.

Хорошо, предыдущий абзац звучит отвлеченно и просто, возможно, я что-то упустил, но это в двух словах.

Надеюсь, это поможет, С наилучшими пожеланиями, Том.

1 голос
/ 26 января 2010

У вас есть много ответов, чтобы прочитать, но я думаю, что я могу держать это кратким.

«Двоичный код» относится к битам, которые поступают через микропроцессорные схемы. Микропроцессор последовательно загружает каждую инструкцию из памяти, делая то, что они говорят. Различные семейства процессоров имеют разные форматы для инструкций: x86, ARM, PowerPC и т. Д. Вы указываете процессору на нужную инструкцию, присваивая ему адрес инструкции в памяти, а затем он весело проводит время по всей программе.

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

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

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

1 голос
/ 26 января 2010

Есть две вещи, которые вы можете смешать здесь. Обычно есть две темы:

Последний может компилироваться с первым в процессе сборки. Некоторые промежуточные форматы не собираются, а выполняются виртуальной машиной. В случае C ++ это может быть скомпилировано в CIL, который собран в сборку .NET, поэтому у меня может быть некоторая путаница.

Но в целом C и C ++ обычно компилируются в двоичный файл, или, другими словами, в формат исполняемого файла.

1 голос
/ 26 января 2010

Насколько я понимаю, чипсет (ЦП и т. Д.) Будет иметь набор регистров для хранения данных и понимать набор инструкций для манипулирования этими регистрами. Инструкции будут такими, как «сохранить это значение в этом регистре», «переместить это значение» или «сравнить эти два значения». Эти инструкции часто выражаются в коротких алфавитных кодах, которые могут быть понятны человеку (язык ассемблера или ассемблер), которые сопоставляются с числами, которые понимает набор микросхем - эти числа представляются чипу в двоичном виде (машинный код).

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

1 голос
/ 26 января 2010

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

То есть, если только Ассемблер! = Сборка, и я неправильно понимаю ваш вопрос.

1 голос
/ 26 января 2010

Они компилируются в файл определенного формата (COFF для Windows и т. Д.), Состоящий из заголовков и сегментов, некоторые из которых имеют «простые двоичные» коды операций. Ассемблеры и компиляторы (такие как C) создают один и тот же вид вывода. Некоторые форматы, такие как старые файлы * .COM, не имели заголовков, но все же имели определенные предположения (например, где в память он будет загружен или насколько большим он может быть).

На компьютерах с Windows загрузчик операционной системы находится в секторе диска, загруженном BIOS, где оба они «простые». Как только ОС загрузит свой загрузчик, она сможет читать файлы с заголовками и сегментами.

Это помогает?

0 голосов
/ 01 декабря 2012

Исполняемые файлы (формат PE в Windows) нельзя использовать для загрузки компьютера, поскольку загрузчик PE не находится в памяти.

Способ начальной загрузки состоит в том, что основная загрузочная запись на диске содержит большой двоичный код.BIOS компьютера (в ПЗУ на материнской плате) загружает этот BLOB-объект в память и устанавливает указатель инструкции процессора на начало этого загрузочного кода.

Затем загрузочный код загружает загрузчик «второй стадии» в Windows, называемый NTLDR (без расширения), из корневого каталога.Это необработанный машинный код, который, как и загрузчик MBR, загружается в память и выполняется.

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

...