«Hello World!» на C массивом int main[]

в 14:57, , рубрики: C, hello world, Visual Studio

Я хотел бы рассказать о том, как я писал реализацию «Hello, World!» на C. Для подогрева сразу покажу код. Кого интересует как до этого доходил я, добро пожаловать под кат.

#include <stdio.h>
const void *ptrprintf = printf;
#pragma section(".exre", execute, read)
__declspec(allocate(".exre")) int main[] =
{
    0x646C6890, 0x20680021, 0x68726F57,
    0x2C6F6C6C, 0x48000068, 0x24448D65,
    0x15FF5002, &ptrprintf, 0xC314C483
};

Предисловие

Итак, начал я с того, что нашел эту статью. Вдохновившись ею я стал думать как сделать это на windows.

В той статье вывод на экран был реализован с помощью syscall, но в windows мы сможем использовать только функцию printf. Возможно я ошибаюсь, но ничего иного я так и не нашел.

Набравшись смелости и взяв в руки visual studio я стал пробовать. Не знаю, зачем я так долго возился с тем, чтобы подставлять entry point в настройках компиляции, но как выяснилось позже компилятор visual studio даже не кидает warning если main является массивом, а не функцией.

Основной список проблем, с которыми мне пришлось столкнуться:

1) Массив находится в секции данных и не может быть исполнен
2) В windows нет syscall и вывод нужно реализовать с помощью printf

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


Решение проблемы «исполняемых данных»

Первая проблема с которой я столкнулся, ожидаемо оказалось то, что простой массив хранится в секции данных и не может быть исполнен, как код. Но немного покопав stackoverflow и msdn я все же нашел выход. Компилятор visual studio поддерживает препроцессорную директиву section и можно объявить переменную так, чтобы она оказалась в секции с разрешением на исполнение.

Проверив, так ли это, я убедился, что это работает и функция массив main спокойно исполняет opcode ret и не вызывает ошибки «Access violation».

#pragma section(".exre", execute, read)
__declspec(allocate(".exre")) char main[] = { 0xC3 };

Немного ассемблера

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

Я решил, что сообщение «Hello, World» я буду хранить в ассемблерном коде. Сразу скажу, что ассемблер я понимаю достаточно плохо, поэтому прошу сильно тапками не кидаться, но критика приветствуется. В понимании того, какой ассемблерный код можно вставить и не вызывать лишних функций мне помог этот ответ на stackoverfow
Я взял notepad++ и с помощью функции plugins->converter->«ASCII -> HEX» получил код символов.

Hello, World!

48656C6C6F2C20576F726C6421

Далее нам нужно разделить по 4 байта и положить на стек в обратном порядке, не забыв перевернуть в little-endian.

Делим, переворачиваем.
Добавим в конец терминальный ноль.

48656C6C6F2C20576F726C642100

Делим с конца на 4 байтные hex числа.

00004865 6C6C6F2C 20576F72 6C642100

Переворачиваем в little-endian и меняем порядок на обратный

0x0021646C 0x726F5720 0x2C6F6C6C 0x65480000

Я немного опустил момент с тем, как я пытался напрямую вызывать printf и чтобы сохранить потом этот адрес в массиве. Получилось у меня только сохранив указатель на printf. Позже будет видно почему так.

#include <stdio.h>
const void *ptrprintf = printf;
void main() {
    __asm {
        push 0x0021646C ; "ld!"
        push 0x726F5720 ; " Wor"
        push 0x2C6F6C6C ; "llo," 
        push 0x65480000 ; "He"
        lea  eax, [esp+2] ; eax -> "Hello, World!"
        push eax ; указатель на начало строки пушим на стек
        call ptrprintf ; вызываем printf
        add  esp, 20 ; чистим стек
    }
}

Компилируем и смотрим дизассемблер.

00A8B001 68 6C 64 21 00       push        21646Ch  
00A8B006 68 20 57 6F 72       push        726F5720h  
00A8B00B 68 6C 6C 6F 2C       push        2C6F6C6Ch  
00A8B010 68 00 00 48 65       push        65480000h  
00A8B015 8D 44 24 02          lea         eax,[esp+2]  
00A8B019 50                   push        eax  
00A8B01A FF 15 00 90 A8 00    call        dword ptr [ptrprintf (0A89000h)]  
00A8B020 83 C4 14             add         esp,14h  
00A8B023 C3                   ret  

Отсюда нам нужно взять байты кода.

Чтобы вручную не убирать ассемблерный код можно воспользоваться регулярными выражениями в notepad++.
Регулярное выражение для последовательности после байтов кода:

 {2} *.*

Начало строк можно убрать с помощью плагина для notepad++ TextFx:

TextFX->«TextFx Tools»->«Delete Line Numbers or First Word», выделив все строки.

После чего у нас уже будет почти готовая последовательность кода для массива.

68 6C 64 21 00
68 20 57 6F 72
68 6C 6C 6F 2C
68 00 00 48 65
8D 44 24 02
50
FF 15 00 90 A8 00 ; После FF 15 следующие 4 байта должны быть адресом вызываемой фунцкии
83 C4 14
C3


Вызов функции с «заранее известным» адресом

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

#include <stdio.h>
const void *ptrprintf = printf;
void main()
{
    void *funccall = &ptrprintf;
    __asm {
        call ptrprintf
    }
}

«Hello World!» на C массивом int main[] - 1

Как видно в указателе лежит именно тот самый вызываемый адрес. То что нужно.

Собираем все вместе

Итак, у нас есть последовательность байт ассемблерного кода, среди которых нам нужно оставить выражение, которое компилятор преобразует в адрес, нужный нам для вызова printf. Адрес у нас 4 байтный(т.к. пишем для код для 32 разрядной платформы), значит и массив должен содержать 4 байтные значения, причем так, чтобы после байт FF 15 у нас шел следующий элемент, куда мы и будем помещать наш адрес.

Путем нехитрых подстановок получаем искомую последовательность.
Берем полученную ранее последовательность байт нашего ассемблерного кода. Отталкиваясь от того, что 4 байта после FF 15 у нас должны составлять одно значение форматируем под них. А недостающие байты заменим на операцию nop с кодом 0x90.

90 68 6C 64
21 00 68 20
57 6F 72 68
6C 6C 6F 2C
68 00 00 48
65 8D 44 24 
02 50 FF 15
00 90 A8 00 ; адрес для вызова printf
83 C4 14 C3

И опять составим 4 байтные значения в little-endian. Для переноса столбцов очень полезно использовать многострочное выделение в notepad++ с комбинацией alt+shift:

646C6890
20680021
68726F57
2C6F6C6C
48000068
24448D65
15FF5002
00000000 ; адрес для вызова printf, далее будет заменен на выражение
C314C483

Теперь у нас есть последовательность 4 байтных чисел и адрес для вызова функции printf и мы можем наконец заполнить наш массив main.

#include <stdio.h>
const void *ptrprintf = printf;
#pragma section(".exre", execute, read)
__declspec(allocate(".exre")) int main[] =
{
    0x646C6890, 0x20680021, 0x68726F57,
    0x2C6F6C6C, 0x48000068, 0x24448D65,
    0x15FF5002, &ptrprintf, 0xC314C483
};

Для того чтобы вызывать break point в дебаггере visual studio надо заменить первый элемент массива на 0x646C68CC
Запускаем, смотрим.

«Hello World!» на C массивом int main[] - 2

Готово!

Заключение

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

Оставлю тут все приведенные ссылки:

Статья «main usually a function»
Описание section на msdn
Некоторое объяснение ассемблерного кода на stackoverflow

И на всякий случай оставлю ссылку на 7z архив с проектом под visual studio 2013

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

Буду рад вашим отзывам и замечаниям.

Автор: ComradeAndrew

Источник

Поделиться новостью

* - обязательные к заполнению поля