Всем привет!
Я бы хотел создать цикл статей, который будет посвящен созданию слоя совместимости для запуска Windows приложений на ОС семейства Linux. При этом хочу сделать акцент на реализации собственного формата исполняемого файла и использования метода дистилляции для перевода программного кода из формата в формат. Это очень обширная тема, чтобы уместить её в одну статью, а также достаточно сложная для понимания «извне», поэтому я буду стараться излагать всё на доступном языке и подходить к этой тематике, так скажем, издалека.
В этой статье мы посмотрим, как написать код и запустить его не самым тривиальным и "велосипедным" способом.
Всю работу мы будем делать на Linux с архитектурой процессора x86-64 (AMD64), а также использовать инструменты: gcc, NASM и IDA.
Наш план:
Напишем самую простую программу, которую только можно вообразить.
Запустим её "ручками".
Придумаем и опишем свой формат исполняемого файла.
Упакуем программу и запустим её через импровизированный загрузчик.
Profit!
1. По настоящему самая простая программа
В качестве тестовой программы почти всегда на ум приходит "Hello, world!", но, на данный момент, работа с ней будет очень сложной, поэтому мы начнём с более простого примера и напишем программу, которая просто завершится не сделав ничего полезного :-)
При этом, мы еще больше упростим задачу и сделаем это на языке ассемблера используя ассемблер NASM.
section .text
global _start
_start:
mov eax, 74
ret
Итак, мы имеем код для ассемблера NASM, в первых двух строках которого, мы говорим, что в нашей секции (название для блока данных внутри исполняемого файла) .text (там стандартно содержится исполняемый машинный код) будет доступна функция _start. Она имеет такое название, потому как именно по этому названию NASM будет понимать, где находится точка входа (entry point, место, откуда начнётся исполнение) в нашей программе. Конечно, можно было бы написать иное название, например привычное main, но тогда это явно потребуется указать NASM'у. Далее, в 4-й строке, мы объявляем нашу точку входа для _start. В 5-й строке мы помещаем значение 74 в регистр EAX (регистры это такие, так скажем, переменные в памяти процессора, своеобразные кармашки для данных, с которыми происходит работа на данный момент). И, наконец, в 6-й строке мы делаем return, то есть выходим из функции.
Стандартом принято, что регистр EAX используется для передачи возвращаемого значения, поэтому функция _start просто возвращает значение 74, а так как это самая главная функция в нашей программе и возвращаться некуда — вся программа завершается с этим кодом.
Теперь преобразуем наш код в объектный файл (промежуточный тип файла между файлом с кодом и исполняемым файлом).
> nasm -f elf64 test.asm -o test.o
Предлагаю оставить нашу программу в виде объектного файла и посмотреть, что стало с кодом. Для этого воспользуемся графическим дизассемблером под названием IDA.

Здесь мы видим тот же самый код, но самое интересное для нас появится, когда мы посмотрим на код в режиме Hex View.

Это, опять же, наш код, но представленный в виде машинного байт-кода (низкоуровневое представление кода, которое понятно процессору). Здесь присутствуют 2 команды:
B8 4A 00 00 00 — B8, это команда переместить последующее 4-х байтовое значение в регистр EAX, а 4A 00 00 00, это наше 4-х байтовое значение в формате LE (представление многобайтовых данных Little-Endian, это когда байты идут в обратном порядке, то есть наше значение на самом деле равно 0x0000004A), которое представляет из себя число 74.
C3 — return:)
2. Не самый простой запуск самой простой программы
Теперь мы возьмем этот байт-код и попробуем запустить его "самостоятельно". Для этого мы напишем небольшую программу на языке C.
int main() {
char program_data[] = { 0xB8, 0x4A, 0x00, 0x00, 0x00, 0xC3 };
int (*program)() = (int (*) ())program_data;
int result = program();
return result;
}
Что мы тут делаем?
Мы создали 6-ти байтовый массив и заполнили эти 6 байт машинным байт-кодом нашей программы, потом привели этот массив к типу функции, а потом вызвали эту функцию и завершили всю программу с кодом, равным её возвращаемому значению.
Скомпилируем и запустим.
> gcc main.c -o main.out
> ./main.out
< segmentation fault (core dumped) ./main.out
Упс, мы получаем любимую "сегу" (segmentation fault, ошибка сегментации, это когда программа нарушает что-то при работе с памятью), что означает, что мы явно что-то сделали не так. Но что именно?
Всё дело в том, что ОС реализует механизм модификаторов доступа (права на выполнение каких-то операций) к памяти, он, можно сказать, такой же как у файлов: чтение, запись и исполнение. Иными словами, не все операции можно делать со всеми данными, что, на самом-то деле, вообще прекрасно, иначе мы бы достаточно часто и быстро убивали систему своими кривыми ручками :D
Так вот, компилятор при создании исполняемого файла, записывается информацию, которая при запуске задает для нашего массива модификаторы доступа на чтение и запись, но не на исполнение, что, думаю, логично, кто в здравом уме будет запускать массив?
Для решения данной проблемы мы будем использовать один из главным механизмов в ОС, без которого не обходится ни один запуск программы, так как именно он отображает (копирует) файл в память перед исполнением. Прошу любить и жаловать — mmap!
Эта функция, а точнее системный вызов (syscall, специальная функция-прослойка для работы в режиме ядра ОС), похожа на привычные функции выделения динамической памяти, такие как: malloc, calloc и realloc. Но эта функция имеет гораздо больше возможностей, хотя, всё же, самое главное для нас сейчас — она позволяет задать модификаторы доступа к памяти самостоятельно.
Теперь изменим нашу программу и превратим её в это:
#include <sys/mman.h>
#include <string.h>
int main() {
char program_data[] = { 0xB8, 0x4A, 0x00, 0x00, 0x00, 0xC3 };
char* program_mmaped = mmap((void*)0, sizeof(program_data),
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
memcpy(program_mmaped, program_data, sizeof(program_data));
int (*program)() = (int (*) ())program_mmaped;
int result = program();
return result;
}
Теперь мы добавили выделение памяти с помощью mmap с модификаторами доступа RWX (Read, Write, Execute), после в эту память мы скопировали наш байт-код и проделали те же манипуляции, что и в прошлый раз. Кстати, MAP_ANONYMOUS это флаг, который указывает, что выделенная память не привязана к дескриптору файла, то есть, другими словами, что это просто память, а не отображенный файл. А MAP_PRIVATE означает, что выделенная область памяти будет существовать только для нашего процесса.
Проверим, что будет теперь...
> gcc main.c -o main.out
> ./main.out
> echo $?
< 74
Шалость удалась!
Скажу честно, когда этот трюк я сделал в первый раз, то сначала не поверил, а потом посмотрел на всё программирование другими глазами. Всё-таки, когда человек исполняет массив, его жизнь делится на до и после :D
3. Придумываем свой формат исполняемого файла
Назовем наш супер-крутой формат SEF — Simple Executable File.

В нём не будет ничего кроме магического значения (специальная последовательность первых байт файла, чтобы можно было определить его тип, например в Windows исполняемые файлы имеют магическое значение MZ, а в Linux — ELF), смещения в байтах от начала файла до точки входа и данных, то есть кода.
В целом, все, что нам потребуется для работы с таким форматом, это такая структурка:
typedef struct _SEF_HEADER {
char magic[4];
unsigned int entry;
} SEF_HEADER;
4. Пишем утилиту для создания и запуска нашего формата
Здесь, в целом, все просто, поэтому быстренько накидываем код программы для запуска программы:)
И получаем что-то такое:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
typedef struct _SEF_HEADER {
char magic[4];
unsigned int entry;
} SEF_HEADER;
int create_new_sef_file() {
char filename_buffer[512 + 1] = "";
printf("Enter new SEF filename (max 512 symbols): ");
fgets(filename_buffer, sizeof(filename_buffer), stdin);
filename_buffer[strcspn(filename_buffer, "\n")] = 0;
FILE* new_file = fopen(filename_buffer, "w+b");
if (new_file == NULL) {
fprintf(stderr, "Can't open file '%s'!\n", filename_buffer);
return -1;
}
char data_buffer[512 * 2 + 1] = "";
printf("Enter byte data without spaces (max 512 bytes): ");
fgets(data_buffer, sizeof(data_buffer), stdin);
filename_buffer[strcspn(filename_buffer, "\n")] = 0;
unsigned short entry = 0;
printf("Enter integer value as offset to entry point in data (2 bytes, unsigned): ");
scanf("%hu", &entry);
SEF_HEADER sef_header = {
.magic = { 'S', 'E', 'F', 0 },
.entry = entry + sizeof(SEF_HEADER)
};
fwrite(&sef_header, sizeof(sef_header), 1, new_file);
for (size_t i = 0; i < strlen(data_buffer) / 2; ++i) {
char byte = 0;
sscanf(data_buffer + i * 2, "%2hhx", &byte);
fwrite(&byte, sizeof(char), 1, new_file);
}
fclose(new_file);
fprintf(stderr, "New SEF '%s' created!\n", filename_buffer);
return 0;
}
int execute_sef_file(char* filename) {
int sef_file = open(filename, O_RDWR);
if (sef_file == -1) {
fprintf(stderr, "Can't open '%s'!\n", filename);
return -1;
}
struct stat sef_file_info = {0};
if (fstat(sef_file, &sef_file_info) == -1) {
fprintf(stderr, "Can't get stats of '%s'!\n", filename);
close(sef_file);
return -1;
}
char* mmaped = mmap(NULL, sef_file_info.st_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE, sef_file, 0);
if (mmaped == (char*)-1) {
fprintf(stderr, "Can't mmap '%s'!\n", filename);
close(sef_file);
return -1;
}
close(sef_file);
SEF_HEADER* sef_header = (SEF_HEADER*)mmaped;
int (*entry_point) () = (int (*) ())(mmaped + sef_header->entry);
int result = entry_point();
munmap(mmaped, sef_file_info.st_size);
return result;
}
void exit_with_help(char* program_name, int code) {
fprintf((code ? stderr : stdout),
"Usage %s:\n"
"%s -- create SEF\n"
"%s <path> -- execute SEF\n"
"%s --help -- print this message\n",
program_name, program_name, program_name, program_name
);
exit(code);
}
int main(int argc, char** argv) {
if (argc > 2) {
exit_with_help(argv[0], -1);
} else if (argc == 1) {
return create_new_sef_file();
}
if (!strcmp(argv[1], "--help")) {
exit_with_help(argv[0], 0);
}
return execute_sef_file(argv[1]);
}
Само собой, этот код можно улучшать бесконечно, делая его функциональнее, быстрее, безопаснее и красивее, но, надеюсь, никто не обидится, если мы оставим его таким))
У нашей утилиты будет всего две функции: создание исполняемого файла в формате SEF и исполнение такого файла.
Для создания будем запрашивать имя файла, данные в виде байт без пробелов и сдвиг в байтах относительно введенных данных. Открываем/создаём файл в режиме бинарной записи, вписываем туда заголовок нашего формата, в котором магическим значением будет SEF\0, а сдвиг равен указанному сдвигу + размеру заголовка, ну, чтобы указывать куда нам нужно, ведь к данным добавился заголовок. Ну, а потом, в цикле переводим байты в виде символов в байты в виде байтов и записываем в файл.
Для запуска всё тоже просто — открыли файл, правда тут уже немного другой функцией — open, вместо fopen, так как от файла нам нужен только дескриптор для отображения его в память с помощью mmap. Определили размер файла с помощью получения информации о файле используя fstat. А дальше всё, практически, как и в прошлый раз, только мы указали дескриптор открытого файла, чтоб его отобразить в оперативной памяти. Сразу после отображения можно смело закрывать открытый дескриптор открытого файла, так как оригинал с диска нам больше не понадобится. И всё, запускаем приводя к типу функции.
Пробуем.
> gcc sef.c -o sef.out
> ./sef.out --help
< Usage ./sef.out:
< ./sef.out -- create SEF
< ./sef.out <path> -- execute SEF
< ./sef.out --help -- print this message
> ./sef.out
< Enter new SEF filename (max 512 symbols):
> test.sef
< Enter byte data without spaces (max 512 bytes):
> B84A000000C3
< Enter integer value as offset to entry point in data (2 bytes, unsigned):
> 0
< New SEF 'test.sef' created!
> ./sef.out test.sef
> echo $?
< 74
И это именно то, чего мы и добивались! Поздравляю!
Заключение
В целом, это всё, чем я хотел поделиться в данной статье.
Надеюсь, что это было полезно и увлекательно. А если нет — обязательно напишите об этом, я постараюсь не обидеться и прислушаться к критике :D
Спасибо за внимание!
Комментарии (0)
strok_ova
23.09.2025 13:29Очень подробно написано. Не мой профиль, но даже я все поняла. Спасибо автору
Ivan174z
Круто! Как я сам не додумался до этого! Все гениальное просто!