Всем привет. Я давно хотел прикоснуться к этой теме и написать что-то подобное, но никак руки не доходили. Сегодня я решился, и мы с вами разберем структуру 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.
Продолжать цикл статей про низкоуровневое программирование?

Проголосовало 199 человек. Воздержалось 34 человека.

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

Комментарии (14)


  1. CodeRush
    24.11.2015 00:06
    +36

    Дружище, вот это твое «WCT» — это не ненормальное программирование, а не знаю даже, как это назвать. И то, что для этого «WCT» есть онлайн компилятор — это далеко не преимущество перед нормальным hex, т.к. для последнего вообще никакой «компилятор» не нужен. Использование этого «WCT» делает статью непригодной ни для какого практического применения, а без него — это очередное введение в формат ELF, которое можно целиком заменить на вот эту картинку, отчего смысла и пользы только прибавится.
    Я работаю с низким уровнем каждый день, и иногда даже вынужден писать в машинных кодах потому, что в 64-битные компиляторы MS не завезли ключевое слово __asm, а вставка ассемблерная нужна позарез. И я хочу, чтобы в теме разобрались новички, в том числе и благодаря статьям на хабре, но эта статья выглядит как издевательство над ними.
    Пожалуйста, дружище, направь свою энергию и свои умения писать и читать машинный код в конструктивное русло — присоединяйся к проекту radare2, там толковые ребята нужны постоянно, и тебе потом полмира спасибо скажет. А то, что получается сейчас — это какая-то феерическая чушь, извини.


    1. Mihip
      24.11.2015 00:12
      -7

      Друг, Wct = странная вещь для кодинга, о чем вообще может идти речь после этого? =)
      Твои слова приму к сведению, спасибо!

      А насчет чуши — ты прав, но мне это нравится, это… это мое хобби :)


      1. CodeRush
        24.11.2015 00:31
        +24

        Понимаешь, WCT не интересен от слова вообще. Ни как странная вещь для кодинга, ни как ЯП, ни как ассеблер — вообще. Это просто шифр замены, очень старая идея, которую в 21 веке додумались применить к дампу бинарного файла — и получился аж целый новый язык, который «взорвет нам всем мозг».
        Есть интересные изотерические языки — с предельно уменьшеным (пока вообще одна стрелка Пирса не станется) набором команд, с интересными правилами (Malbolge), с тритами вместо битов и так далее. На них трудно писать, но это вызов самому себе. С WCT же никакого вызова нет, а замена эта воспринимается как досадная помеха. По сути, написание программ на WCT — это то же самое, что написание сообщений WOT TAKIM WOT SPOSOBOM — CHITAT STANOVITSA TJAZHELEE, NO NA NOVIY JAZIK ETO NE PRETENDUET, A ESLI JA NACHNU TAK PISAT VSE VREMJA — MENJA ZAGONJAT V MINUSA, и это будет совершенно заслуженно.


    1. REU
      24.11.2015 11:22

      присоединяйся к проекту radare2, там толковые ребята нужны постоянно

      Да, а то пока они всё как-то не так делают…


  1. Maysoft
    24.11.2015 10:24
    +1

    На сайте wasm.ru (не знаю существует ли сейчас) был цикл статей для дзенствующих. В этих статьях создавались программы на машинных кодах в debug.exe. Было очень интересно, хотя и бесполезно. Прочитав вашу статью, могу констатировать, что «не зацепило». Анализ статьи делать не буду. Если хотите научиться писать «научно-популярным» языком найдите указанный цикл статей. То есть я хочу сказать, что сделать привлекательным можно любое бредовое увлечение (и в этом нет ничего постыдного), но пока у вас этого не получилось. Желаю удачи


  1. Alexeyslav
    24.11.2015 12:36

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


  1. demoth
    24.11.2015 19:27
    +3

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


    1. Mihip
      24.11.2015 20:41

      demoth, давай не будем смотреть в прошлое? Сейчас же я все сам делаю.


      1. demoth
        24.11.2015 22:54
        +2

        Ок, взглянем на настоящее
        В репозитории, на который ты ссылаешься этой статье, лежат «исходники» wct-editor'а.
        После «компиляции» (wct->hex->binary) получаем исполняемые файлы, сжатые при помощи UPX с потёртой сигнатурой.

        Распаковываем:
        • В windows версии несложно увидеть строчку «SOFTWARE\Borland\Delphi\RTL», которая тонко намекает на истинное происхождение «компилятора».
        • Для версии под linux пришлось гуглить строчку «Recompile the application with a unicodestrings-manager in the program uses clause» — первая же ссылка указывает на Free Pascal Compiler.


        1. catnikita255
          26.11.2015 14:43

          Я знаю. Он мне писал, что писал старые версии на фрипаскале. А вот насчет новых не знаю.


  1. catnikita255
    26.11.2015 14:39
    +1

    О, Тарс! Привет.


  1. stychos
    26.11.2015 23:50
    +1

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


    1. StrangerInRed
      27.11.2015 11:05
      -1

      В любом деле необходим некий порог вхождения, увы.


      1. Alexeyslav
        27.11.2015 11:23
        +1

        Высокий порог вхождения не гарантирует качества. Граблей на входе можно наставить великое множество.