Написание простейшей программы под Linux в машинных кодах

от автора

Всем привет. Я давно хотел прикоснуться к этой теме и написать что-то подобное, но никак руки не доходили. Сегодня я решился, и мы с вами разберем структуру ELF-файла (исполняемый файл на *nix-подобных системах), и напишем простую программу под x86 Linux в машинных кодах, которая выведет сообщение на экран. Но тут не все так однозначно, поверьте мне :).

Начать бы я хотел с конца. А именно с того, что будет делать наша программа. Наша программа — не что иное, как куча машинного кода, который, впоследствии, будет исполняться системой. В качестве заместителя системы счисления Hex я буду использовать «Wct», ибо он гораздо удобнее, потому что имеется онлайн компилятор и возможность вставлять строки на ходу и использовать десятичные числа.

image

Вообще, ELF — формат двоичных файлов, используемый во многих современных UNIX-подобных операционных системах, таких как FreeBSD, Linux, Solaris и др. Но, как говорится, лучше один раз увидеть, чем много раз услышать. Прошу, ниже Вы можете лицезреть разбор исполняемого файла для ОС «Linux».

Сразу оговорюсь, таблицу оп-кодов для архитектуры x86 вы можете найти здесь.
Перед тем, как мы с вами погрузимся с головой в эту "няшу" (в прямом и переносном смысле), я хотел бы предупредить вас, что данная статья может вынести ваш мозг, или же частично его повредить. Вы предупреждены. Если же вы отважитесь читать далее — прошу.

Как я уже говорил ранее, ELF представляет из себя тип исполняемых файлов под *nix-подобные системы. Но как же система будет определять, является ли исполняемый файл подходящим под неё, а также, как система определяет, что это именно ELF-структура, а не PE, допустим?

Все предельно просто, в самом начале ставится синхробайт «Ho», после чего следует последовательность ASCII-символов «ELF», что помогает оси разобраться, что же это за зверь такой, наш файл.

		// Заголовок исполняемого файла Ho "ELF"	// Подпись .ELF, где "Ho" - специальный символ, а 		// "ELF" - ASCII символы 

Важно — байт "Ho" — нулевой байт. Это надо знать наизусть, ибо можно сбиться со счета, и потеряться. 🙂

Итак, поздравляю, мы повстречали такую часть программы, как «заголовок». В нём содержится важная информация для исполнения файла, такая, как — разрядность системы, версия ELF-структуры файла, OS ABI… но об этих странных словах мы поговорим чуть позднее.

Так, ну и раз мы используем Wct, давайте я расскажу, что и к чему.
Всего в Wct используется 16 символов — A B C D E F G H I J K L M N P O, где O идет после P, это сбивает новичков с толку, но со временем привыкаешь. Кстати, аббревиатура «Wct» расшифровывается как «Weird Coding Tool» — странная вещь для программирования, кодинга. Да-да, и Wct я использую лишь потому, что он легче запоминается и удобнее используется — вставки десятичных чисел это удобно, не правда ли?

Итак, мы ступили на землю зла. Мы продолжаем исследовать наш файл.
Далее идет разрядность системы, которая представляет из себя один байт, который может быть либо «B», либо «C», где «B» указывает на то, что система 32-х разрядна, а C — 64-х разрядна. Это очень важно, потому что в дальнейшем нам нужно будет использовать таблицу заголовка либо для 32-х разрядной системы, либо для 64-х разрядной, и они кардинально отличаются друг от друга.

Ab		// или 01, где 1 (B) - 32-х битная архитектура, 		// а 2 (C) - 64-х битная, думаю, что это уж точно понятно 

Ну хорошо, хорошо. Да, разрядность системы — важный аргумент, но нам ещё нужно знать, какую последовательность байтов мы будем использовать — Little Endian, или Big Endian. В чем же заключается их отличие, да и вообще, что это такое?

Little Endian — это порядок байтов, в данном случае — от младшего к старшему, тобишь так — Ab aa aa aa, и это будет число «Bw», то есть, единица. В случае Big Endian все с точностью, да наоборот — это порядок от старшего к младшему или (англ. big-endian, дословно: «тупоконечный»): An — Ao, запись начинается со старшего и заканчивается младшим. Кстати, про Little-Endian и Big-Endian уже писали на «Хабрахабре» тут.

Ab		// B = Little Endian, C = Big Endian 		// Это порядок байтов. В нашем случае - 		// Little Endian, или же, порядок от младшего 		// к старшему, тобишь - Ao - An... 

Так, с этим мы разобрались. Но это, отнюдь, далеко не все, уж поверьте мне на слово, мы ещё и половину не рассмотрели.
Сейчас начнется самое интересное.

Ab		// Версия ELF-структуры файла 

Мы используем оригинальную версию ELF-структуры файла, так что просто оставим «B».
А вот теперь мы дошли до «OS ABI».

Двоичный (бинарный) интерфейс приложений — это набор соглашений между программами, библиотеками и операционной системой, обеспечивающих взаимодействие этих компонентов на низком уровне на данной платформе. Он нужен для предоставления разрядности типов данных, формата передачи аргументов и возвращаемого значения при вызове ф-ции, состав и формат системных вызовов и файлов. Мы будем использовать «System V» ABI для написания программы на линуксе, ведь ABI, с точки зрения программы — ни что иное, как операционная система, ведь полностью реализовав ABI той или иной операционной системы в своей системе, вы сможете выполнять «неродные» программы так, как они выполняются на «родной» платформе.

Ad		// Это у нас "OS ABI" - двоичный интерфейс приложений, 		// набор соглашений между программами, библиотеками 		// и операционной системой, обеспечивающих взаимодействие 		// этих компонентов на низком уровне на данной платформе. 		// В данном случае - ABI для 32-х битного линукса. 

Дальше идут зарезервированные байты, которые используются для «пэддинга», или же не используются вообще.

Aa aa aa aa	// Не используется... aa aa aa aa	// В любом случае, оно для чего-то нужно :) 

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

Исполни́мый (исполня́емый) мо́дуль, исполняемый файл — файл, содержащий программу в виде, в котором она может быть исполнена компьютером. Перед исполнением программа загружается в память, и выполняются некоторые подготовительные операции.

Ac aa		// Тип исполняемого файла, где 	        	// B = изменяемый, C = исполняемый, D = общий, E = ядро 

Очень интересная часть — набор инструкций. Набор инструкций — это то, чем будет пользоваться процессор при исполнении программы. В данном случае, будет использоваться этот набор инструкций для архитектуры x86.
Мы увидим команды для процессора в самом конце, где будет располагаться код программы.

Памятка.
Чтобы использовать набор инструкций для x86 — надо указать «Ad» первым байтом, чтобы x86_64 — «Dp», ARM — «Ci», и так далее.

Ad aa		// Набор инструкций. Сейчас мы работаем с набором 		// инструкций процессора типа "x86", но если захотим 		// писать программу для другого проца, то и сет инструкций 		// там будет другой. 

Мы добрались до точки входа в программу. Точка входа в программу — указатель, который показывает системе на то место, где заканчиваются заголовки и начинается программа. У нас программа начинается с помещения числа «Ae» (4) в EAX, но об этом чуть позднее.

Ab aa aa aa	// Повтор версии ELF структуры... He IA AE	// Точка входа в программу. Одна из важнейших 		// частей в программе.Ab aa aa aa	// Повтор версии ELF структуры... He IA AE	// Точка входа в программу. Одна из важнейших 		// частей в программе. 
Ai DE AA	// Расположение таблицы заголовков секций	>———————┐ 		//							│ Aa aa aa aa aa	//							│ Aa aa aa aa aa	//						      	│ 		//						      	│ De aa		// Размер заголовка					│ Ca aa		// Размер таблицы заголовков программы			│ Ac aa		// Кол-во записей в таблице заголовка программы		│ Ci aa		// Размер записи в таблице заголовков			│ Aa aa		// Кол-во записей в таблице раздела заголовков		│ Aa aa		// Список в разделе "таблицы заголовков" с именами	│ 		//							│ /*		//							│ 		//							│ Часть 2 -								│ Заголовок программы							│ 									│ */		//							│ 		//							│ Ab aa aa aa 	// Тип сегмента, у нас - B, значит		<———————┘ 		// байты p_memsz по адресу p_vaddr будут 		// очищены, после чего будет произведено 		// копирование байтов p_filesz со смещением		>———————————————┐ 		// p_offset в p_vaddr...						│ 		//									│ He aa aa aa	// Смещение в файле, по которому могжет быть	>———————┐		│ 		// найдена информация для данного сегмента (p_offset)	│		│ 		//							│		│ He ia ae ai		// Место, где этот сегмент должен	>———————┼———————┐	│ 			// размещаться в виртуальной памяти (p_vaddr)	│	│	│ 		//							│	│	│ He ia ae ai	// UNDEFINED для системы V ABI				│	│	│ Bo aa aa aa	// Размер сегмента в файле (p_filesz)		<———————┼———————┼———————┘ Bo aa aa aa	// Размер сегмента в памяти (p_memsz)			│	│ 		//							│	│ Af aa aa aa aa	// Флаги - EXECUTABLE WRITEABLE READABLE		│	│ Ba aa aa	// Необходимое выравнивание для данного раздела		│	│ 			//						│	│ 			// Необходимая системе информация		│	│ ABAAAAAAJDAAAAAA	// Просто без этого не работает..		│	│ JDJAAEAIJDJAAEAI	// На самом деле, тут содержатся		│	│ ANAAAAAAANAAAAAA	// p_* директивы.				│	│ AGAAAAAAAABAAAAA	//					<———————┴———————┘ 

Итак, мы подошли к центру событий. Это место полно тайн и загадок… ладно, мы-то знаем, что для нас загадок больше нет. Приступим. Название этому место — секция кода.

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

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

Чтобы выполнить какую-либо функцию из таблицы прерываний линукса, нам надо поместить номер функции, которую мы хотим выполнить, в регистр EAX (eXtended AX — расширенный регистр AX, 32-х битный), а в другие регистры (EBX, ECX и EDX) — другую необходимую для выполнения функции информацию.

Таким образом, получаем:

Li AE aa aa aa		// Помещаем число 4 (AE) в регистр EAX Ll AB aa aa aa		// В регистр EBX помещаем число 1 (AB)  Lj JD ja ae ai		// В регистр ECX кладем адрес нашего сообщения Lk AN aa aa aa		// В регистр EDX - размер сообщения 13 (N)  Mn IA			// Выполняем прерывание IA.. Зачем?			 			// Чтобы выполнить определенную функцию.		 			// У нас в EAX - 4, значит мы будем выполнять		 			// действие "вывод строки на экран", где -		 			// EAX - номер функции. После выполнения прерывания	 			// "IA" будет выведена строка "Wct One Love"		 			//							 Li AB aa aa aa		// И опять в EAX кладем единичку			 			//							 Db NL			// Обнуляем регистр EAX					 Mn IA			// Знакомое нам прерывание IA..				 			// Кстати, так звали ослика из мульта "Винни Пух", только тогда "IA-IA" :)	 			// Я думаю, что вы знаете, про что я говорю		 

Ну и завершающим этапом для написания нашей программы является объявление данных, у нас — текста.
Для написания текста можно использовать кавычки, либо таблицу ASCII-символов.
«Ca» = пробел, если что.

"Wct" Ca "One" Ca "Love"	// "Wct One Love" Ak				// Конец... 

А ниже вы можете лицезреть результат работы нашей программы:

image

Ну вот и все. Боюсь, что в тексте я мог допустить какие-либо ошибки, ошибки в объяснениях, и т.д… Прошу простить меня, я пишу это поздно вечером, уставший. Если вы обнаружите ошибки, будьте добры, сообщите мне, я обязательно исправлю! Спасибо за то, что ты уделил внимание к моей статье! Воспользуйтесь онлайн компилятором для сборки исходника. Всего наилучшего тебе, дружище 🙂

Скачать исходник.
Онлайн компилятор.
Ресурсы.

По всем вопросам пишите на e-mail — mihip@yandex.ru.

Продолжать цикл статей про низкоуровневое программирование?

Проголосовал 1 человек. Воздержавшихся нет.

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

ссылка на оригинал статьи http://habrahabr.ru/post/271519/


Комментарии

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

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