05/09/2018

Converting C into Assembler on the Amiga

Introduction

This article is basically a cheat-sheet for converting C code into Amiga Assembler. The basic method is not my own. I found a page on it a year or so back, but unfortunately, I forgot to save the link. This is my rendition of the method, and any errors in thought or application should be considered mine.

Most coders, whether on the Amiga or not, are familiar with the C language. Not everyone knows how to work with Assembler, though, and the bar for learning it can be daunting.

For people already conversant with C who want to learn Assembler, there's a solution: translating it yourself, by hand, at speed. That sounds like a tall order, and complex C is indeed hard to translate. But C doesn’t have to be complex in order to be powerful. If it turns out a reduced subset of C code can help to not only dip our toes into Assembler but to write it quickly and accurately (and I aim to demonstrate that it can), then it should be an avenue worth pursuing.

Our goal, then, is to write our program in C, and then render that code into legal 68k Assembler. At first glance that would be intimidating. However, by adding just a few self-imposed constraints, we can restructure the C code into a form less concise but much easier to translate.

This is a deceptively powerful technique. After all, compiled C is just the computer doing its uninspired best to write Assembler in the first place. It just does it much faster than a human, though sometimes less efficiently (particularly on older systems, where memory and CPU speeds impose constraints on the fancy optimizations the compiler can pull). Still, manual translation remains a proven method of producing a working Assembler program in short order. The added benefit is the greater insight into one's own code offered by manual translation.

From C to A in three steps

The procedure itself is unambiguous. It takes a C listing, and the end result is a rough sketch of the final yet-to-be-optimized Assembler code. We will most likely require a few Amiga-specific additions to get something up on the screen, but the resulting binary will otherwise be fully functional.

  1. Write the routine in C. Determine its correctness.
  2. Restructure the routine in accordance with the following constraints:
    1. Calculations must be on the form of [value] = [value] [sign] [number or variable] (ex: "x = x + 2"), nothing more complex.
    2. Comparisons must be on the form of [number or value] [comparator] [number or variable] (ex: "x != 2"), nothing more complex.
    3. Remove complex branching constructs. Replace with goto or function call.
    4. Remove loop constructs. Replace with goto.
    5. The only allowed conditional is a single-line if statement followed by a goto.
    6. Remove references to variable length strings, arrays or lists. Use statically sized arrays.
    7. Replace primitive functions with calls to OS functions on the target platform when convenient (ie. typedef).
    8. Rename variable names to resemble register names, where appropriate.
    9. Test that the code’s output is unchanged (i.e. is still correct).
  3. Translate this modified code into rough Assembler form, add Amiga-specific code (libraries, interrupts, hardware access) and, again, verify correctness.

How the method is used

To illustrate the method, we could try a simple example. Project Euler is a website that offers a series of problems in roughly ascending difficulty. Most of these problems are constructed in such a way as to make a brute-force solution computationally expensive.

I generally try to avoid spoilers. However, the very first Project Euler exercise should be considered reasonably easy for any CS student to solve in a handful of minutes. The Sieve of Eratosthenes provides a fairly good, conceptually simple solution. Better still, it yields a short enough listing to be manageable for us to translate. Note that will not translate the printout part of the routine, as it would just be a distraction.

Running this program yields a sum of 233168, which in hex is 0x38ed0. This is our expected result.

Next is to prepare the C listing for translation. Applying our list gives us the following modified C listing:

This is already pretty close to what we want, but we're still not there. We need to put registers in place of variables:

Except for the Printf() function, this is practically Assembler at this point. The instructions are now ready to be translated. The result:

Assembling the program results in a minor error, easily fixed by changing the addq instruction in the third loop to add.w.

As noted, the program should result in a value of 0x38ED0 in register d0. When we assemble the corrected listing and run it, it yields exactly that: ergo, the program works. The only bug we encountered stemmed from my attempt to optimize instruction size, which was not part of the method. Had I stuck to the script, the first attempt would have assembled and given the correct response.

Two things bear mention about our C implementation. When I wrote this, I declared three iterators (scoped to each for loop). Doing so is good high-level practice, but runs contrary to the Assembler ethos of porous scope. When preparing for translation, consider deliberately compromising scoped constructs or even declaring variables as global.

Regardless, the results speak for themselves. Judging by the above demonstration, the idea is sound.

No comments:

Post a Comment