Как писать на машинном коде
Перейти к содержимому

Как писать на машинном коде

Как написать «Hello World» на машинном коде?

Недавно наткнулся на видео, где некий сумасшедший написал программу, которая выводит в консоль слова «Hello World» на бинарном/машинном коде (если честно я не уверен что это именно).

Расскажите пожалуйста, как можно повторить результат (не советуйте скопировать код из видео, хочу именно понимать как такое сделать)? Может какая-то литература по этому поводу?

  • Вопрос задан более трёх лет назад
  • 26084 просмотра
  • Facebook
  • Вконтакте
  • Twitter

Вам достаточно изучить ассемблер и всё станет понятно.
Каждая команда ассемблера транслируется «дословно» в машинный код.
Т.е. например команда pushl %edx превратится в один байт 82

Например программа на ассемблере:

превратится в машинный код:

Почитать можно здесь и здесь . В целом подойдет любая книга по ассемблеру.

Машинный код и компиляция в него — это как?

Хочу поработать с этим, потому что всегда мечтал попробовать собрать .exe самостоятельно, без помощи готовых компиляторов.

user avatar

Baremetal

Каждый конкретный процессор (например, Intel Core i3-4160 или ARM Cortex-A9) имеет свою микроархитектуру и реализует архитектуру уровня набора команд (англ. instruction set architecture).

Микроархитектура определяет структуру процессора на уровне электронных компонентов и логических вентилей.

Архитектура уровня набора команд (ISA), грубо говоря, определяет то, какие команды может выполнять процессор. Эта архитектура абстрагированна от микроархитектуры. Процессоры разных комнаний могут реализовывать одну и ту же архитектуру (например, многие процессоры Intel и AMD реализует одно и то же семейство архитектур x86).

Если два процессора реализуют одну и ту же ISA, то они могут исполнять одни и те же программы. ISA определяет, какие команды доступны программисту, какие регистры он может использовать, как он может использовать страничную адресацию, виртуальную память и т. д. Кроме того, она определяет формат команд, которые понимает процессор.

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

Написать программу в машинных кодах достаточно просто — нужно лишь взять справочник по ISA (например, Intel 64 and IA-32 Architectures Software Developer Manuals), которую реализует ваш процессор и написать нужные команды байт за байтом.

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

Например, одна инструкция ассемблера x86 MOV может кодировать около 20 различных инструкций процессора MOV 1 . Ассемблер читает вашу программу на языке ассемблера и переводит ее в бинарный файл 2 , который, опять же, является просто последовательность байт, кодирующих подряд идущие инструкции процессора.

Вот так может выглядет отрывок программы на языке ассемблера:

Вот так выглядит программа на машинном языке:

Очевидно, что асссемблерный код и читать, и писать проще.

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

Операционная система

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

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

Если брать в пример ОС Windows 10, она работает с исполняемыми файлами .exe , которые имеют специальный формат, называемый Portable Executable. Он имеет довольно сложную структуру. Помимо собственно набора машинных команд он содержит в себе информацию необходимую для определения адреса и размера секций, таблиц импорта и экспорта, специальную сигнатуру и т. д.

Поэтому чтобы вручную написать программу в машинных кодах, которая будет запускаться в Windows 10, например, нам, по-мимо написания самой программы, потребуется привести ее к формату Portable Executable.

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

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

Можете начать с написания программ на языке ассемблера (да, вам придется еще выучить синтаксис конкретного языка ассемблера и диалект Intel или AT&T). «Hello, World» на языке NASM будет выглядеть так:

А нужно ли вам это?

В наше время компьютеры стали очень сложными, с десятками слоями абстраций. Даже инструкции ISA современных процессоров — не атомарные сущности, и процессоры выполняет каждую такую инструкцию как набор еще более мелких инструкций — микрооперации (из таких мокроопераций складывается микрокод).

На самом деле, умение писать на языке ассемблера (а тем более, на машинном языке) довольно бесполезно. Умение просто читать и понимать ассемблерный листинг гораздо более практично и действительно может вам пригодится.

А непрактично это в первую очередь потому, что ничего сложнее «Hello, World!» в машинных кодах вы не напишете. На ассемблере — да, напишете, но потратите на это колоссальное количество времени, которое можно было бы потратить на более полезные вещи.

1. Что интересно, инструкция MOV в x86 является Тьюринг-полной, т. е. любая программа может быть написана с использованием одной только этой инструкции. Есть даже специальный компилятор, который использует только одну эту инструкцию.

2. Некоторые ассемблеры могут сразу формировать исполняемые файлы в нужном формате. В том числе и Portable Executable.

Как писать на машинном коде

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

Одним словом, есть некий психологический барьер, который бывает трудно преодолеть в одиночку. Известно, что для того, чтобы научиться программировать, надо взять и начать программировать. «ИНФОРКОМ» предлагает Вам следующий компромиссный подход — сначала в рамках этой главы мы, беря «быка за рога», просто начнем программировать, а затем посвятим оставшуюся часть книги систематическому изложению материала.

Итак, давайте напишем первую программу в машинном коде. Прежде всего, выделим для нее область памяти. Если Вы читали нашу книгу "Большие возможности Вашего «ZX-Spectrum`а», то знаете, что для БЕЙСИКа в оперативной памяти компьютера отведена область памяти, начинающаяся с адреса, на который указывает системная переменная PROG и заканчивается адресом, на который указывает системная переменная RAMTOP. Предположим, что Вы хотите записать программу в машинных кодах, начиная с адреса 30000. Дайте команду CLEAR 29999. Эта команда установит RAMTOP в 29999 и Ваша программа будет защищена от возможной порчи из БЕЙСИКа. Даже если Вы дадите команду NEW, области памяти, находящиеся выше RAMTOP, не будут поражены.

Теперь дайте две прямые команды одну за другой:

Мы сейчас записали два числа в нужные нам адреса. Они образуют простейшую программу. Выполнить ее можно командой RANDOMIZE USR 30000. Попробуйте сами. Вам покажется, что ничего не произошло, но это не так. Сначала процессор обратился по адресу 30000 и нашел там число 0, которое обозначает машинный код операции NOP. Операция NOP ( no operation — нет операции) дает команду процессору, что ничего делать не надо. В течение 0,0000014 сек. он действительно ничего не делает, а затем переходит к следующему адресу, где находит число 201.

Это команда RET ( return — возврат). Она дает процессору указание прекратить в этом месте программу в машинных кодах и вернуться туда, откуда она вызывалась, т.е. в нашем случае — в БЕЙСИК. Это самое процессор и сделал, о чем Вы получили сообщение БЕЙСИКа «О.К.».

Если все, что Вы здесь прочитали, Вам понятно, то Вы уже поняли, как составляются программы в машинных кодах. Можно, конечно, возразить, что пользы от такой программы не очень много, но сейчас не в этом суть. Важно, чтобы Вы поняли, что некая последовательность чисел может быть последовательностью команд для процессора Z-80.

К сожалению, для нас мало, что говорит простая последовательность чисел вроде таких, как 0 и 201. Держать в памяти коды всех команд процессора (а их около семисот) непросто, но дело упрощается тем, что есть промежуточный язык между процессором и человеком — язык Ассемблера. У каждого кода есть своя мнемоника Ассемблера. Мнемоника — это набор букв, являющихся сокращением английских слов. Для нашего примера программа на Ассемблере выглядит так:

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

И тех программ и других достаточно много. Часто они объединяются в пакеты. Широко распространены пакеты GENS3/MONS3 фирмы HISOFT и EDITAS / MONITOR 16/48 фирмы PICTURESQUE . Здесь GENS 3 и EDITAS — Ассемблеры, а MONS 3, MONITOR 16 и MONITOR 48 — Дизассемблеры.

Теперь давайте вернемся к нашей первой программе и попробуем ее несколько развить, чтобы она все же что-то делала. Процессор Z-80 имеет несколько регистров, у которых есть имена – «А», «В», «С» и т.д. Каждый из них может содержать одно какое-либо целое число от 0 до 255 (т.е. один байт).

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

Так, например, команда Ассемблера LD B,A (машинный код — 71) означает «загрузить содержимое регистра А в регистр В». LD — это сокращение от LOAD (загрузка).

Точно так же LD C,B (машинный код 72) означает «загрузить в регистр С содержимое регистра В». Можно загружать в регистры и целые числа. Например, LD A, n — означает «загрузить в регистр А целое число n », где n может быть числом от 0 до 255. До этого все команды были однобайтными. Эта же команда — двухбайтная. Сначала идет машинный код — 62, а за ним само число — n . Так, например, LD A, 77 (загрузить в регистр А число 77) будет выглядеть так: 62,77. Здесь 62 — код операции, — он сообщает процессору, что надо сделать, а 77 — это операнд. Заметим здесь же, что бывают операции и трехбайтные и четырехбайтные. Первый байт, как правило, — код операции, а следующие за ним — операнды. Мы говорим «как правило» потому, что есть некоторые операции, код которых записывается двумя байтами [прим.1].

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *