Одним томным пятничным вечером взбрела мне в голову безумная идея: а почему бы мне не поразмять мозг, и не написать HelloWorld на ассемблере. Однако это показалось слишком простым. А давайте соберем не x86 программу, а java class? Сказано — сделано.

Первым делом находим спецификацию JVM. Как мы видим, файл класса Java состоит из:

  • Магическое число, оно всегда равно 0xCAFEBABE
  • Минорная и мажорная версии, для Java 7 они равны 0 и 51 соответственно.
  • Количество элементов пула констант и сами элементы — об этом ниже.
  • Флаги доступа, номер константы, указывающей на текущий класс, номер константы, указывающей на класс-родитель.
  • Количество реализуемых интерфейсов и массив номеров их дескрипторов в пуле.
  • Количество полей, массив дескрипторов полей.
  • Количество методов, массив дескрипторов методов.
  • Количество атрибутов, массив атрибутов.

В Java принят порядок big endian, в то время как FASM — ассемблер под x86, где принят little endian, поэтому сразу напишем макросы для перевода чисел из одного порядка в другой:

u1 equ db ; u1 - 1 байт, переворачивать нечего

macro u2 [data] { ; u2 - 2 байта
  forward ; передаем в макрос несколько элементов через запятую, например u2 0x1234, 0x5678
; поэтому нужно указать порядок их следования
  u1 (((data) shr 8) and 0xFF) ; сначала старший байт
  u1 ((data) and 0xFF) ; потом младший
}

macro u4 [data] { ; u4 - 4 байта, по аналогии с u2
  forward
  u2 (((data) shr 16) and 0xFFFF) 
  u2 ((data) and 0xFFFF)
}

Вообще, язык макросов FASM настолько мощный, что на нем можно написать еще один язык, причем не одним способом.

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

Элементы константного пула в общем случае выглядят так:

cp_info {
    u1 tag;
    u1 info[];
}

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

Конструкция вида const#name — склеивает текст const и значение из константы name. Конструкция аналогична такой же из макросов языка C.

const_count = 0 
macro const name* { ; макрос подсчета
  const_#name: const_count = const_count + 1 
  name = const_count ; объявляем константу (FASM константу) и присваиваем ей значение счетчика
}

Хоть в терминологии FASM-a константы называются константами, на самом деле они ведут себя как переменные, и с ними можно производить многие манипуляции.

Дальше объявляем макросы для начала и для конца:

macro ConstantPool {
  u2 const_count_end + 1 ; кладем количество констант в вывод
; +1 потому что 0 константа не существует, но учитывать ее надо
}

Но ведь такой переменной не существует, скажете вы. И будете правы. Пока не существует. FASM — многопроходной ассемблер, и что не было найдено при первом проходе, он запомнит и подключит при втором или дальнейших.

macro ConstantPoolEnd {
  UTF8 code_attr, 'Code' 
  const_count_end = const_count ; а вот мы и присваиваем значение. 
}

При следующем проходе ассемблер подставит вместо const_count_end ровно столько, сколько он насчитал констант.

Методы и поля организованы схожим образом. Приведу пример макроса, генерирующего константу Method


macro UTF8 name, text {
  const name
  u1 1 ; tag
  u2 .end - .start
  .start: u1 text
  .end:
}

macro Class name, className {
  UTF8 className_#name, className
  const name
  u1 7 ; tag
  u2 className_#name
}

macro NameAndType name, nameText, typeText {
  UTF8 name_#name, nameText
  UTF8 type_#name, typeText
  const name
  u1 12 ; tag
  u2 name_#name
  u2 type_#name
}

macro Method name, className, methodName, methodType {
  Class class_#name, className ; константа типа Class
  NameAndType nameAndType_#name, methodName, methodType ; константа типа NameAndType
  const name
  u1 10 ; tag
  u2 class_#name
  u2 nameAndType_#name
}

Тут можно видеть как идут ссылки — Method ссылается на Class, Class ссылается на UTF8, так же и с NameAndType. В макрос передаются аргументы в виде строк, например Method printlnInt, 'java/io/PrintStream', 'println', '(I)V'.

Ну и напоследок сам исходник:

format binary as 'class'
include 'java.inc'
; --- FILE ---
magic: u4 0xCAFEBABE
version: u4 51

ConstantPool
 Class this, 'java'
 Class super, 'java/lang/Object'
 UTF8 main, 'main'
 UTF8 main_sig, '([Ljava/lang/String;)V'
 Field o, 'java/lang/System', 'out', 'Ljava/io/PrintStream;'
 Method println, 'java/io/PrintStream', 'println', '(Ljava/lang/String;)V'
 Method printlnInt, 'java/io/PrintStream', 'println', '(I)V'
 String hello, 'Hello World!'
ConstantPoolEnd

u2 PUBLIC, this, super, 0, 0 ; интерфейсов нет, полей нет, поэтому по нулям

Methods
 MethodStart PUBLIC or STATIC, main, main_sig, 2, 1
  getstatic o
  ldc hello ; указываем имя константы, объявленное выше
  invokevirtual println
  getstatic o
  bipush 42
  invokevirtual printlnInt
  return
 MethodEnd
MethodsEnd

u2 0 ; атрибутов нет

» Полный код можно посмотреть здесь.

Спасибо за внимание!
Поделиться с друзьями
-->

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


  1. pengyou
    13.09.2016 19:22
    -3

    Пишем Hello world на WASM https://github.com/kpmy/tiss/blob/master/demo/bf/bf.go Извините, что не хватает воды на целую статью.


  1. sshikov
    13.09.2016 19:35

    Вообще, язык макросов FASM настолько мощный, что на нем можно написать еще один язык, причем не одним способом.

    А можно написать макро для определения управляющих структур типа if/while? С вложенностью и генерацией меток?


    1. shevmax
      13.09.2016 19:43

      Ну так в FASM и так есть while if else


      1. sshikov
        13.09.2016 19:45

        Есть и "можно сделать" — все же немного разные вещи. Чтобы сделать if, нужно иметь таккую структуру, как стек.


        1. MacIn
          13.09.2016 20:27

          Оно эквивалентно в данном случае, потому что оно «есть» сделано на макросах в виде инклюда. Например:

          macro .if [arg]
          {
            common
            __IF equ
            local ..endif
            __ENDIF equ ..endif
            local ..else
            __ELSE equ ..else
            JNCOND __ELSE,arg
          }
          
          macro .else
          {
            jmp __ENDIF
            __ELSE:
            restore __IF
            __IF equ ,
          }
          
          


        1. bigfatbrowncat
          13.09.2016 21:28
          +4

          (ирония)

          Предлагаем вашему вниманию лучший макроязык над ассемблером — Си


          1. MacIn
            13.09.2016 21:40
            +1

            А где ирония-то? Как боженька сказанул.


            1. bigfatbrowncat
              13.09.2016 21:52
              +1

              А если серьезно, то я щупал как-то ассемблер, причем именно FASM. И пытался писать на нем Win32-приложение. 20 строчек на один вызов MessageBox… В общем, я быстро пришел к необходимости писать макросы. И спустя полчаса поймал себя на том, что изобретаю Паскаль :) И на этом заглох.

              Для программирования на Асме нужен особый склад ума, видимо, противоположный моему. Я, приступая к задаче, первым делом стараюсь собрать ее в как можно более высокоуровневые блоки, продумать уровни абстракции… В язык уровня Си или Асма я могу влезать либо из соображений оптимизации, либо из спортивного интереса.


              1. MacIn
                13.09.2016 22:01

                И пытался писать на нем Win32-приложение. 20 строчек на один вызов MessageBox…

                Вообще-то, {уже} нет.

                Вот код примера из стандартной поставки:

                ; example of simplified Windows programming using complex macro features
                
                include 'win32ax.inc' ; you can simply switch between win32ax, win32wx, win64ax and win64wx here
                
                .code
                
                  start:
                	invoke	MessageBox,HWND_DESKTOP,"Hi! I'm the example program!",invoke GetCommandLine,MB_OK
                	invoke	ExitProcess,0
                
                .end start
                
                


                1. bigfatbrowncat
                  13.09.2016 22:19

                  Ну да, примерно так. А если надо эту строку сформировать…


                  1. MacIn
                    13.09.2016 22:54

                    Да тоже в принципе ничего особого. kernel32 экспортирует всякую мелочь типа конкатенации. Через тот же invoke и вызовется. На самом деле, макросы у fasm а очень богаты, сейчас это уже не просто голый мнемокод.


                    1. bigfatbrowncat
                      14.09.2016 21:46

                      … что, собственно, возвращает нас к шутке про Си.

                      Мне с моей чисто «академической» колокольни интересно, для какой надобности нынче пишут именно на Ассемблере, если мнемокод мигом превращает его в более высокоуровневый язык (но с явно «протекающей» абстракцией, мешающей, например, отлаживаться)


                      1. MacIn
                        14.09.2016 22:04

                        Макрокод, видимо его вы имел в виду. Мнемокод и «язык ассемблера» — это синонимы.

                        Зачем? Есть специализированные задачи, шеллкоды какие-нибудь, например.

                        Да и разница между С и ассемблером с макросами все же есть. Вы возьмете FASM, используете макросные ifы, вызовы функций итд и в итоге получите крохотный файл в, скажем, бинарном формате. Без линкера, без заголовков.
                        Я использовал(ю периодически) для разработки своей ОС, как небольшое хобби.


                1. denis_g
                  15.09.2016 01:24

                  Не, ну это не интересно. Где прерывания? Регистры? Оффсеты? Джампы в разные куски кода? А в таком виде никакой романтики…


                  1. MacIn
                    15.09.2016 01:41

                    А откуда прерывания в программировании под Win32? Начиная с XPSP2 используется sysenter.


          1. VioletGiraffe
            13.09.2016 21:50

            И лучший макроязык над С — С++


            1. bigfatbrowncat
              13.09.2016 21:53
              +6

              Ужасный. Но лучше пока не придумали, да :)


              1. mwizard
                13.09.2016 23:10
                -1

                Придумали, господа Брайт и Александреску (да, тот самый).


                1. bigfatbrowncat
                  14.09.2016 21:44
                  -1

                  Видел. Даже использовать пробовал. Обломался об отсутствие нормального кроссплатформенного компилятора, отладчика, IDE с чем-то большим, чем простая подсветка синтаксиса…

                  Короче, там еще «придумывать» и «придумывать»…

                  А идея неплоха, да. За мелкими ньюансами.


    1. napa3um
      13.09.2016 19:43
      +1

      Можно, эти макросы входят в стандартную поставку.


  1. farwayer
    13.09.2016 21:08
    +1

    Синтаксис fasm — няшка! Код просто читается, бо синтаксис лаконичный, в отличие от других ассемблеров. Макросы очень мощные. Еще работает под *nix, и есть порт на ARM.


  1. frog
    13.09.2016 22:31

    Это вот сюда надо (вроде бы такого варианта там ещё нет :)
    https://github.com/leachim6/hello-world


  1. xi-tauw
    14.09.2016 08:46

    C# Randomyst HelloWorld
    Ideone


  1. Sirikid
    14.09.2016 21:42

    Поясните что в итоге получилось, валидный class-файл и программа на ассемблере в одном исходнике?


    1. MacIn
      14.09.2016 22:06

      Это не настоящая программа, это сам class в виде макросов. Если ее транслировать, получится class. Это описательная программа, без действий.