I do not fear computers. I fear the lack of them.
-- Isaac Asimov
Ever since I took the leap from Basic V2 to 6510 assembly on my old Commodore 64, I've had a hate/love relationship with assembly. It allowed me back then to do things that were not possibly through any other means (vblank handler, raster coppers etc). My second foray into assembly was through Turbo Pascal funny enough. Turbo Pascal had this really easy mechanism to escape into inline assembly so that you could write your high level constructs in Pascal and then write the guts in assembly. This worked really well for me for a long while, even though real mode x86 assembly is kind of a pain. I had a brief excursion into protected mode 32 bit intel assembly programming and then I virtually stopped. What happened? I started to write more and more code in C++. Horribly slow code, compared to the assembly, but the compilers were not too bad and they were getting better. For the majority of the code, it made so much sense to write it in C++. Much of the programs I now wrote had to run on SPARC as well, which made assembly unsuitable.
You would think that working on console games where the platform doesn't change and it actually makes sense to write assembly that I write more today? Ironically, that's not true. For the duration of my time in the industry, I've written very little assembly, at least compared to the amount of C++ I've written. That doesn't mean that assembly is a dead art, far from it. I consult the instruction manuals at least a couple of times every week, since I can't keep all the instructions and their little quirks in memory. The reason why I consult them is because I read assembly almost every day.
Reading compiler generated assembly is something that's incredibly useful to figure out exactly what happens and also to learn the C++ language. C and C++ are really close to the hardware, C is little more than portable assembly. Knowing how the constructs in C/C++ are translated into assembly can help you debug bad code. It can also help you track down compiler bugs (even though in 90% of the cases, the alleged compiler bug turns out to be just bad code, which the compiler tries really really hard to transform to asm).
Environment setup for Microsoft C++
The first part is to setup your environment to easily transform C++ code into assembly and inspect the output. Experimentation is really important, the simple examples I will show here are not enough in the long run and there is no substitute for experience. I will discuss both the Microsoft Visual C++ setup and a typical GCC setup.
The first thing I do is to create a batch script for each set of compiler I want to support. I've found that it is much easier to handle everything from the command line for experimentation. First, let's start with MSVC. Put the following in a file called vc.bat and place it in your path somewhere:
That should allow you to type vc anywhere and instantly get access to the command line tools if you have visual studio installed. You can also of course install the Platform SDK, which includes the command line installer and tie the environment variables to that compiler.
Some of the command lines we're going to use are:
- /Fa: generate assembly listing
- /nologo: don't print the logo
- /c: only compiler
- /Ox: max optimizations
- /Oy-: disable frame pointer omission
- /Za: disable language extensions
- /fp:fast: generate possibly non IEEE 754 compliant code
So for example, if we want to compile a foobar.cpp file into a foobar.asm file, we could issue:
cl /nologo /c /Fa /Ox /Oy- /Za foobar.cpp
After issuing this command, we should have a foobar.asm in the same directory ready to load up in any text editor.
Environment setup for GCC
Now for GCC, it's a little bit trickier. There are several distributions for GCC out there, so I'm going to talk about this in a very generic way. For an example I'm going to use the GameCube compiler in devkitpro (this is a PowerPC 750 compiler). After installing the devkit, you need to remember the root of the devkitpro and then place the following in a batchfile, e.g. gc.bat:
This will now allow you to invoke the gcc compiler for an PPC750 processor after calling the batchfile gc.bat. The full command line manual for GCC is available here, but I'll handle the flags we're interested in for this article here:
- -c : compile only
- -S : output assembly file
- -O3 : full optimizations
- --verbose-asm: insert extra comments in the assembly
- -ffast-math: don't bother with IEEE 754 compilant math.
- -ansi : check for ansi compliant code
- -pedantic : be very picky about the code you compile
So for example, say we have a C++ file foobar.cpp and want to see how the file looks like in assembly. Then we could invoke the following:
powerpc-gekko-gcc -S -c -O2 --verbose-asm -ffast-math foobar.cpp
This would produce the file foobar.s which can contain something like this:
_Z6foobarfi: stwu r1,-16(r1) fctiwz f0,f1 stfd f0,8(r1) lwz r0,12(r1) addi r1,r1,16 mullw r3,r0,3 blr
You might wonder how to get a GCC compiler onto your windows machine. There is a homebrew kit with an easy windows installer for both the PSP, GameCube and Nintendo DS consoles that you can download here.
Optimized builds v/s debug builds
The settings we've seen previously includes compiler options for optimizations. From experiences with debug builds v/s optimized builds and the debugger, it might seem odd that we want to generate optimized builds for inspection. The reason though is that the debug builds generate extra precarious code that does a lot of extra housekeeping tasks that at first glance doesn't make sense for a human, but in the context for a debugger it makes sense. When you turn on optimizations most of these retarded repeated stores and loads go away and you are left with relatively clean code that in some cases are not that far from what you might have produced yourself. We've taken it one step further and included some flags that can reduce weird code some more, e.g. the floating point code.
If you are writing C only, the symbol names in the assembly file should not be that bad to read, but if you're writing C++ code you might notice that the names are barley readable. That's because most compilers are embedding information about the calling convention and types into the name. There are usually tools to demangle these names into something that you. For C programs, there is some rhyme and reason to the names, they are also standardized by the ABI. The C++ mangling is very much compiler dependant. For Visual Studio, there is a special program (undname.exe) that can decode, or undecorate, the name into something that looks like a regular prototype. Most of the gnu binutils have this built in, for example nm and objdump.
I was originally going to write a really long article about this topic, but I just could not organize it good enough. So I'm just breaking it up in several parts, hence the Part I in the title. Basically the things I wanted to write a little bit about was an update of Pietrek's Assembly Survival Guides, retargeted a little bit towards games programmers. I've got links to them below in the resource section, they are still very good reads.
- Pietrek, Matt Under The Hood, Feb. 1998 http://www.microsoft.com/msj/0298/hood0298.aspx
- Pietrek, Matt Under The Hood, Jun. 1998 http://www.microsoft.com/msj/0698/hood0698.aspx
- GCC Manual http://gcc.gnu.org/onlinedocs/gcc-4.2.2/gcc/