Под капотом вашей программы

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

Как вы, наверное, уже знаете, компьютеры работают с битами. Бит - это то, что просто представляет два состояния. Включено или выключено, 1 или 0, истина или ложь, плюс или минус и т. Д. Мы можем использовать любые две вещи для представления этих двух состояний, но в целом мы используем 1 и 0, чтобы сказать, что бит включен и выключен соответственно. Мы знаем, что компьютеры могут хранить слова, изображения, звуки и т. Д. Но на самом деле все это не что иное, как биты, и мы, люди, сгруппировали их и придали им какое-то значение (код). Для примера возьмем байт, который на самом деле составляет 8 бит. Сколько шаблонов мы можем составить из 8 бит? С 1 битом есть только две возможности: 1 или 0. С 2 битами 4 шаблона; аналогично с 8 битами у нас есть 256 возможных способов расположения битов. Так что же сделали люди? Они собрались вместе, провели несколько встреч, согласовали код и сделали его стандартом. Возьмем, к примеру, ASCII, битовая комбинация 01000001 представляет букву A. Мы просто придали некоторый смысл битовому шаблону. Но имейте в виду, что то, как эти шаблоны интерпретируются, зависит от контекста, в котором они используются.

На высоком уровне в основном то, что происходит, когда вы запускаете программу, заключается в том, что ваша программа сначала загружается в ОЗУ, а затем процессор начинает выполнять инструкции в этой программе. Инструкция - это просто последовательность из одного или нескольких байтов, и разные процессоры следуют разным архитектурам набора инструкций (ISA) в своей кодировке инструкций. (В Linux вы можете использовать следующую команду, чтобы узнать, какая ISA используется на вашем компьютере. В моем случае это x86_64)

uname -m

Давайте возьмем простую программу, написанную на C, чтобы напечатать "Hello World!" и посмотрим, что именно происходит под капотом, когда мы его запускаем.

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

Когда я запускаю следующую команду, она генерирует двоичный файл с именем «hello.o», и если мы откроем его в текстовом редакторе, он покажется тарабарщиной.

gcc -Og -c hello.c

Теперь, чтобы просмотреть содержимое этого сгенерированного машинного кода (hello.o), в Linux у нас есть программа под названием «OBJDUMP», которую мы можем использовать для просмотра сгенерированных инструкций в формате сборки.

Теперь, чтобы сгенерировать фактический исполняемый код, нам нужен компоновщик для запуска с этим сгенерированным объектным файлом (hello.o).

gcc -o output hello.o

Теперь снова, если мы дизассемблируем этот сгенерированный исполняемый файл (вывод) с помощью objdump, вы увидите следующее.

Если вы посмотрите на программу C, которую мы написали в ASCII, мы использовали функцию printf, которая фактически предоставляется стандартной библиотекой C. Здесь компоновщик объединил программу hello.o с предварительно скомпилированным printf.o и создал исполняемый файл с именем «output». Основное отличие, которое вы можете увидеть здесь в коде сборки от кода перемещаемого объектного файла, заключается в том, что компоновщик заполнил правильный адрес для инструкции callq.

Теперь мы готовы загрузить наш исполняемый файл (вывод) в основную память и позволить процессору выполнить его.

Ссылки