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

Ну а когда оно заработает, предлагаю решить задачу: как сэмулировать побитовые операции and-not-xor-or при помощи четырёх арифметических.

Это уже шестая статья из цикла, для понимания происходящего надо ознакомиться если не со всеми, то хотя бы с предыдущей.

Оглавление цикла

  1. Синтаксические деревья и наивный транслятор в питон

  2. Лексер/парсер

    2'. Про́клятый огонь, или магия препроцессора C

  3. Таблицы символов: области видимости переменных и проверка типов

  4. Стек и транслятор в питон без использования питоновских переменных

  5. Транслятор в ассемблер (эта статья)

  6. Избавляемся от зависимостей: пишем лексер и парсер сами

  7. Оптимизирующий компилятор?

  8. Сборщик мусора?

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

Hello world, или вывод строк на экран

Чаще всего в учебных компиляторах выбирают ассемблер MIPS, но мне не нравится запускать код в эмуляторе, поэтому я недолго думал, и выбрал x86 GNU ассемблер, благо, он идёт в составе gcc. Не могу точно сказать почему, но захотелось мне 32-битную версию. Для наших целей совершенно ни к чему быть ассемблерным гуру, но программы уровня хелловорлд писать надо уметь.

Давайте сделаем заготовку, от которой будем впоследствии отталкиваться. Представим, что у нас есть файл helloworld.s со следующим содержимым:

.global _start
        .data
hello: .ascii "hello world\n"
        hello_len = . - hello
        .align 2
        .text
_start:
        movl $4, %eax         # sys_write system call (check asm/unistd_32.h for the table)
        movl $1, %ebx         # file descriptor (stdout)
        movl $hello, %ecx     # message to write
        movl $hello_len, %edx # message length
        int  $0x80            # make system call

_end:
        movl $1, %eax   # sys_exit system call
        movl $0, %ebx   # error code 0
        int $0x80       # make system call

Тогда мы его можем скомпилировать при помощи команд as и ld следующим образом:

as --march=i386 --32 -o helloworld.o helloworld.s &&
ld -m elf_i386 helloworld.o -o helloworld &&
./helloworld

Если всё пошло хорошо, то на экране должно красоваться гордое приветствие. Теперь давайте разбираться, что же там происходит. А там только два системных вызова — sys_write и sys_exit. На сях то же самое можно было бы написать следующим образом:

#include <sys/syscall.h>
#include <unistd.h>

int main(void) {
        syscall(SYS_write, 1, "hello world\n", 12);
        return 0;
}

Если звёзды правильно сойдутся, то gcc сгенерирует примерно такой же ассемблерный код. Для наших нужд никаких других системных вызовов больше не нужно, write и exit нам хватит за глаза, ведь единственное взаимодействие с внешним миром в wend — это вывод на экран.

Wend не умеет никаких операций со строками, только вывод константных строк на экран, поэтому мой компилятор для каждой строки просто создаёт в заголовке уникальный идентификатор ровно как для нашего hello world. Для вывода на экран булевых значений две константные строки true и false. А что с числами? А вот тут придётся чуть‑чуть поработать. Я лентяй, и мне неохота было разбираться с линковкой glibc и тому подобного, поэтому роскошь printf мне недоступна. Ну и ладно, мы и с sys_write управимся :)

Вывод на экран десятичных чисел

sys_write умеет выводить на экран строки, поэтому нам надо научиться конвертировать числа (у меня только знаковые 32-битные) в строковое представление. Для этого я закатал рукава и написал функцию print_int32:

.global _start
        .data
        .align 2
        .text
_start:
        pushl $-729
        call print_int32
        addl $4, %esp
_end:
        movl $1, %eax   # sys_exit system call
        movl $0, %ebx   # error code 0
        int $0x80       # make system call

print_int32:
        movl 4(%esp), %eax  # the number to print
        cdq
        xorl %edx, %eax
        subl %edx, %eax     # abs(%eax)
        pushl $10           # base 10
        movl %esp, %ecx     # buffer for the string to print
        subl $16, %esp      # max 10 digits for a 32-bit number (keep %esp dword-aligned)
0:      xorl %edx, %edx     #     %edx = 0
        divl 16(%esp)       #     %eax = %edx:%eax/10 ; %edx = %edx:%eax % 10
        decl %ecx           #     allocate one more digit
        addb $48, %dl       #     %edx += '0'       # 0,0,0,0,0,0,0,0,0,0,'1','2','3','4','5','6'
        movb %dl, (%ecx)    #     store the digit   # ^                   ^                    ^
        test %eax, %eax     #                       # %esp                %ecx (after)         %ecx (before)
        jnz 0b              # until %eax==0         #                     <----- %edx = 6 ----->
        cmp %eax, 24(%esp)  # if the number is negative                            |
        jge 0f              #                                                      |
        decl %ecx           # allocate one more character                          |
        movb $45, 0(%ecx)   # '-'                                                  |
0:      movl $4, %eax       # write system call                                    |
        movl $1, %ebx       # stdout                                               |
        leal 16(%esp), %edx # the buffer to print                                  |
        subl %ecx, %edx     # number of digits    <--------------------------------┘
        int $0x80           # make system call
        addl $20, %esp      # deallocate the buffer
        ret

Чужой ассемблерный код читать непросто, поэтому давайте я приведу питоновский эквивалент нашей функции:

def print_int32(n):
    buffer = [ None ]*16 # output string buffer
    ecx = 0              # number of characters stored in the buffer

    eax = abs(n)
    while True:
        edx = eax %  10
        eax = eax // 10
        buffer[ecx] = chr(edx + ord('0'))
        ecx += 1
        if eax == 0: break

    if n<0:
        buffer[ecx] = '-'
        ecx += 1

    print(''.join(buffer[ecx-1::-1]))


print_int32(-729)

Write я буду вызывать только один раз, поэтому нужно подготовить строковый буфер. 32-битное число не потребует больше 11 символов, поэтому я выделяю 16 под буфер (чтобы стек был выровнен по краю машинного слова). Затем конвертирую в строку модуль заданного числа, и в конце приклеиваю минус, если число было отрицательным.

При помощи такой нехитрой гимнастики мы можем выводить на экран строки и числа, и, что характерно, без головной боли линковки с какой-нибудь 32-битной версией libc на 64-битной системе.

А такой вывод на экран необходим не только для поддержки инструкции print нашего языка, но и для отладки. GDB это для слабых духом, вставка вывода на экран где ни попадя — это наше всё ;)

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

Ну, собственно, и всё. Теперь берём шаблон генерации питоновского кода, и вместо вывода питоновского print() достаточно написать call print_int32, вместо eax = eax * ebx написать imull %ebx, %eax. Таким образом планомерно переводим питоновские инструкции в ассемблерные, и дело в шляпе! Никаких тонкостей не осталось, долгожданный компилятор почти готов. Можно взять релиз v0.0.5 и с ним поиграть.

Давайте уже программировать на wend! Побитовые операции.

Если вы внимательно следили за происходившим, то видели, что из численных типов у меня только знаковые 32-битные целые числа, и над ними разрешены только базовые арифметические операции, в то время как более развитые языки позволяют, например, делать побитовые логические операции. А я что, рыжий? Я тоже могу :)

На самом деле, это отличное упражнение, рекомендую в мой код не смотреть и попробовать написать код самостоятельно. Обратите внимание, что я железно знаю, что у меня числа хранятся в дополнительном коде, и нет никаких undefined behavior при знаковых переполнениях :)

AND

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

//            _   _ _____
//      /\   | \ | |  __ \
//     /  \  |  \| | |  | |
//    / /\ \ | . ` | |  | |
//   / ____ \| |\  | |__| |
//  /_/    \_\_| \_|_____/

    fun and(a:int, b:int) : int {
        var result:int;
        var pow:int;

        result = 0;
        if (a<0 && b<0) {
            result = -2147483648;
        }
        if (a<0) {
            a = a + 2147483648;
        }
        if (b<0) {
            b = b + 2147483648;
        }
        pow = 1;
        while a>0 || b>0 {
            if a % 2 == 1 && b % 2 == 1 {
                result = result + pow;
            }
            a = a / 2;
            b = b / 2;
            pow = pow * 2;
        }
        return result;
    }

Код весьма дубовый, если кто-то может предложить более изящный подход, с удовольствием его перейму!

NOT

Побитовое «не» выглядит существенно проще:

//   _   _  ____ _______
//  | \ | |/ __ \__   __|
//  |  \| | |  | | | |
//  | . ` | |  | | | |
//  | |\  | |__| | | |
//  |_| \_|\____/  |_|

    fun not(a:int) : int {
        return -1 - a;
    }

XOR и OR

Ну а поскольку «и» и «не» хватает для моделирования всех остальных операций, то «или» сделать тривиально:

//  __   ______  _____
//  \ \ / / __ \|  __ \
//   \ V / |  | | |__) |
//    > <| |  | |  _  /
//   / . \ |__| | | \ \
//  /_/ \_\____/|_|  \_\

    fun xor(a:int, b:int) : int {
        return a - and(a,b) +  b - and(a,b);
    }

//    ____  _____
//   / __ \|  __ \
//  | |  | | |__) |
//  | |  | |  _  /
//  | |__| | | \ \
//   \____/|_|  \_\

    fun or(a:int, b:int) : int {
        return xor(xor(a,b),and(a,b));
    }

Тест

Запускаем тест на заботливо подготовленных случайных данных, и смотри-ка, работает!

     bitwise and     -1804289383      1681692777      1957747793      -719885386       596516649      1025202362       783368690     -2044897763
     -1804289383     -1804289383        70555657       338728977     -1810617712          268809       336599192        70254736     -2079059431
      1681692777        70555657      1681692777      1680906305      1142163488       537663529       605558824       607125600        68947977
      1957747793       338728977      1680906305      1957747793      1410353168       545266689       873486352       615530576        68178961
      -719885386     -1810617712      1142163488      1410353168      -719885386        17173280       353585330        68239794     -2078981612
       596516649          268809       537663529       545266689        17173280       596516649       554309672       578814240        34346505
      1025202362       336599192       605558824       873486352       353585330       554309672      1025202362       739328178        68767768
       783368690        70254736       607125600       615530576        68239794       578814240       739328178       783368690       101793808
     -2044897763     -2079059431        68947977        68178961     -2078981612        34346505        68767768       101793808     -2044897763

      bitwise or     -1804289383      1681692777      1957747793      -719885386       596516649      1025202362       783368690     -2044897763
     -1804289383     -1804289383      -193152263      -185270567      -713557057     -1208041543     -1115686213     -1091175429     -1770127715
      1681692777      -193152263      1681692777      1958534265      -180356097      1740545897      2101336315      1857935867      -432152963
      1957747793      -185270567      1958534265      1957747793      -172490761      2008997753      2109463803      2125585907      -155328931
      -719885386      -713557057      -180356097      -172490761      -719885386      -140542017       -48268354        -4756490      -685801537
       596516649     -1208041543      1740545897      2008997753      -140542017       596516649      1067409339       801071099     -1482727619
      1025202362     -1115686213      2101336315      2109463803       -48268354      1067409339      1025202362      1069242874     -1088463169
       783368690     -1091175429      1857935867      2125585907        -4756490       801071099      1069242874       783368690     -1363322881
     -2044897763     -1770127715      -432152963      -155328931      -685801537     -1482727619     -1088463169     -1363322881     -2044897763

     bitwise xor     -1804289383      1681692777      1957747793      -719885386       596516649      1025202362       783368690     -2044897763
     -1804289383               0      -263707920      -523999544      1097060655     -1208310352     -1452285405     -1161430165       308931716
      1681692777      -263707920               0       277627960     -1322519585      1202882368      1495777491      1250810267      -501100940
      1957747793      -523999544       277627960               0     -1582843929      1463731064      1235977451      1510055331      -223507892
      -719885386      1097060655     -1322519585     -1582843929               0      -157715297      -401853684       -72996284      1393180075
       596516649     -1208310352      1202882368      1463731064      -157715297               0       513099667       222256859     -1517074124
      1025202362     -1452285405      1495777491      1235977451      -401853684       513099667               0       329914696     -1157230937
       783368690     -1161430165      1250810267      1510055331       -72996284       222256859       329914696               0     -1465116689
     -2044897763       308931716      -501100940      -223507892      1393180075     -1517074124     -1157230937     -1465116689               0

                     -1804289383      1681692777      1957747793      -719885386       596516649      1025202362       783368690     -2044897763
     bitwise not      1804289382     -1681692778     -1957747794       719885385      -596516650     -1025202363      -783368691      2044897762

Подводим итоги

Промежуточная цель достигнута: мы научились компилировать собственный язык в настоящий x86 gnu ассемблер. На данный момент для парсинга я пользуюсь сторонней библиотекой sly, но по пути меня слегка занесло, и выяснилось, что написать свой парсер совсем несложно. А заодно можно будет грамматику языка подправить, чтобы приятнее писать можно было!

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

А вот что будет потом... Есть у меня большое подозрение (я ненастоящий сварщик, я маску нашёл!), что самое интересное начинается именно потом. Я попробую разобраться, и заодно рассказать вам, как работает оптимизирующий компилятор. В качестве промежуточного представления я выберу LLVM IR, чтобы можно было в любой момент запускать код при помощи LLVM, но при этом всё будет сделано вручную без LLVM. Бюджета на «оптимизирующий» (только показывающий принцип) компилятор я себе отведу в районе пятисот дополнительных строк питоновского кода. Посмотрим, что получится :)

Stay tuned, have fun!

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


  1. csl
    17.07.2024 18:37
    +2

    Заждался!