Compiler Over the Weekend: Finally, Assembler

We continue our conversation about the toy compiler of the simplest language I invented, wend. This time we have reached a certain milestone: we will finally generate not Python code, but assembler code.

Well, when it works, I suggest solving the problem: how to emulate bitwise and-not-xor-or operations using four arithmetic ones.

This is already the sixth article in the series; to understand what is happening, you need to read, if not all of them, then at least the previous one.

Table of contents of the cycle

  1. Syntax trees and a naive Python translator

  2. Lexer/parser

    2'. The Cursed Fire, or the Magic of the C Preprocessor

  3. Symbol Tables: Variable Scopes and Type Checking

  4. Stack and translator to python without using python variables

  5. Translator to assembler (This article)

  6. Getting rid of dependencies: writing a lexer and parser ourselves

  7. Optimizing compiler?

  8. Garbage collector?

We covered stack and register handling, which are the biggest problems for assembler beginners, in the previous article, so this time there's very little left to do. At the moment, our compiler generates Python code, the structure of which fully corresponds to the desired assembler code. The only thing left to figure out is output to the screen, everything else is already decidedly ready.

Hello world, or displaying strings on the screen

Most often, the MIPS assembler is chosen in educational compilers, but I don’t like running code in an emulator, so I didn’t think long and chose the x86 GNU assembler, fortunately, it comes with gcc. I can’t say exactly why, but I wanted a 32-bit version. For our purposes, there is absolutely no need to be an assembler guru, but you need to be able to write programs at the level of Halloween.

Let's make a template from which we will later push off. Let's imagine that we have a file helloworld.s with the following contents:

.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

Then we can compile it using the as and ld commands as follows:

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

If everything went well, then a proud greeting should appear on the screen. Now let's figure out what's going on there. And there are only two system calls – sys_write and sys_exit. In sys, the same could be written as follows:

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

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

If the stars align correctly, gcc will generate roughly the same assembler code. For our purposes, no other system calls are needed, write and exit are more than enough, since the only interaction with the outside world in wend is output to the screen.

Wend can't do any string operations, only output constant strings to the screen, so my compiler simply creates a unique identifier in the header for each string, just like for our hello world. To output Boolean values ​​to the screen, there are two constant strings, true and false. And what about numbers? Well, that's where I'll have to work a little. I'm lazy, and I didn't want to deal with glibc linking and the like, so the luxury of printf is not available to me. Oh well, we'll manage with sys_write 🙂

Displaying decimal numbers on the screen

sys_write can print strings to the screen, so we need to learn how to convert numbers (I only have signed 32-bit ones) to string representation. To do this, I rolled up my sleeves and wrote the print_int32 function:

.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

It's hard to read someone else's assembly code, so let me give you the Python equivalent of our function:

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)

I will only call Write once, so I need to prepare a string buffer. A 32-bit number will not require more than 11 characters, so I allocate 16 for the buffer (so that the stack is aligned to the edge of the machine word). Then I convert the absolute value of the given number to a string, and at the end I stick a minus if the number was negative.

With the help of such simple gymnastics we can display strings and numbers on the screen, and, what is typical, without the headache of linking with some 32-bit version of libc on a 64-bit system.

And such output to the screen is necessary not only to support the print instruction of our language, but also for debugging. GDB is for the faint of heart, inserting output to the screen anywhere is our everything 😉

Putting it all together

Well, that’s all, actually. Now we take a template for generating Python code, and instead of outputting Python print() it's enough to write call print_int32instead of eax = eax * ebx write imull %ebx, %eax. Thus, we systematically translate Python instructions into assembler, and the job is done! There are no subtleties left, the long-awaited compiler is almost ready. You can take release v0.0.5 and play with it.

Let's program in wend already! Bitwise operations.

If you were paying attention to what was happening, you saw that the only numeric types I have are signed 32-bit integers, and only basic arithmetic operations are allowed on them, while more advanced languages ​​allow, for example, bitwise logical operations. Am I a ginger? I can do that too 🙂

Actually, this is a great exercise, I recommend not looking at my code and trying to write the code yourself. Note that I know for sure that my numbers are stored in two's complement, and there is no undefined behavior with signed overflows 🙂

AND

Let's try to emulate a bitwise “and” using standard arithmetic. Without thinking twice, I process the most significant bit, and then simply run a trivial loop over the remaining 31 bits:

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

    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;
    }

The code is quite oaky, if someone can suggest a more elegant approach, I will gladly adopt it!

NOT

The bitwise “not” looks much simpler:

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

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

XOR and OR

Well, since “and” and “not” are not enough to model all other operations, then “or” is trivial to do:

//  __   ______  _____
//  \ \ / / __ \|  __ \
//   \ 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));
    }

Test

Let's launch test on carefully prepared random data, and look, it works!

     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

Let's sum it up

The intermediate goal has been achieved: we have learned to compile our own language into a real x86 gnu assembler. At the moment, I use a third-party library sly for parsing, but along the way I got a little carried away, and it turned out that writing your own parser is not at all difficult. And at the same time, it will be possible to correct the grammar of the language, so that it would be more pleasant to write!

So the next two articles will be about how to make a lexer and parser yourself, the finished code is already in the repository.

And here's what happens next… I have a strong suspicion (I'm not a real welder, I found a mask!) that the most interesting things start right after that. I'll try to figure it out, and at the same time tell you how the optimizing compiler works. As an intermediate representation, I'll choose LLVM IR, so that you can run the code using LLVM at any time, but everything will be done manually without LLVM. I will allocate myself about five hundred additional lines of Python code for the “optimizing” (only showing the principle) compiler budget. Let's see what happens 🙂

Stay tuned, have fun!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *