Working through ‘WACC’ – Chapter 9 – Functions

WACC means this book

And this is my journey through it.

Parsing

This is a complicated chapter, and I haven’t even got to the code generation part yet

  • Significant rewrite of the declaration and definition parsing.
  • Added a new symbol table, I thought this would replace the variable renaming tables but it is an adjunct
  • Deal with the peculiar scoping rules of variables and functions

Anyway that is now working. On to

Code Generation

This is where I expected the first major problem of Windows vs Linux / Mac to be. Linux and Mac use the Unix System V calling convention, Windows has its own one, detailed here x64 calling convention | Microsoft Learn

It spells out stuff that’s not too bad, just different. But then it starts going on about Structured Exception Handling. Reading around this it look quite fiddly, so do I have to do it?

Note: This is nothing to do with C++ exception handling

SEH allows you to catch C runtime errors, like this

int main()
{
    __try
    {
        TestExceptions();
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        printf("Executing SEH __except block\n");
    }

    return 0;
}

its for catching hard errors, like stack overflow, divide by zero, invalid pointer….. . If you don’t have the necessary SEH parts of your stack set up then your app just dies like it would normally, so I am not going to implement them (I can always do it later)

Oh My Goodness!

3 days of hard slog and frustration later (I am retired so I don’t have a job to distract me from this!)

Well that was a tough journey. I could actually see myself giving up on getting the x64 windows version working. Simple stuff first

X64 calling convention

I provided a link above but here is a summary. It is similar to the System V convention described in the book (only worrying about integer arguments here)

  • The first 4 arguments are passed in RCX, RDX, R8 and R9
  • The rest are pushed onto the stack in reverse order
  • Space for those 4 registers is reserved on the stack by the caller because its such a common model to want to save them (thats what the book does in the called function). The caller doesn’t put the values into the register, merely adjusts the stack size for the callee. Net effect is that the stack looks like the 4 register arguments were pushed (even if fewer arguments were passed) but their values were not stored
  • Return values is in RAX

I found that all the stuff about SEH is optional, if your program dies, it dies, it cant be recovered. Fine.

Earlier on I said what a good debugger Visual Studio has. Well I now have a new BFF, IDA Free the disassembler (I mentioned this before). It also has a debugger in it and stack display is wonderful and made debugging this really easy

Of course in the initial tests I was calling myself, so as long as I was consistent on both sides of the call (and didnt smash the stack too hard) tests would work. Then I started running the test suite….

Dropping MASM

I discovered that MASM does not like you creating and / or calling functions with reserved words as names, for example “add” and “sub”. And the test suite has some of those, I do not think they are deliberate tests for reserved words because gas, the gcc assembler, doesn’t care at all.

Well this is a well known issue in many languages, for example in rust if you must call something return you can use r#return, in c# its @return, etc. So I thought a quick google would solve this.

No, it is impossible to define or call a function called ‘add’ in MASM. This used not to be a problem with the x86 (32 bit) calling convention, all extern symbols get renamed by prepending ‘_’. So ‘add’ becomes ‘_add’ – simple. Not so for x64, names are preserved

Well OK, what does MSVC do in this case? You can ask it to output assembler code with the /FA option. Lets compile this

int add (int x, int y) {
    return x + y;
} 

and see what it produces

; Listing generated by Microsoft (R) Optimizing Compiler Version 19.38.33135.0

include listing.inc

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC  add
; Function compile flags: /Odtp
_TEXT   SEGMENT
x$ = 8
y$ = 16
add     PROC
; File C:\work\mycc\test.c
; Line 1
        mov     DWORD PTR [rsp+16], edx
        mov     DWORD PTR [rsp+8], ecx
; Line 2
        mov     eax, DWORD PTR y$[rsp]
        mov     ecx, DWORD PTR x$[rsp]
        add     ecx, eax
        mov     eax, ecx
; Line 3
        ret     0
add     ENDP
_TEXT   ENDS
END

Um, lets assemble that

PS C:\work\mycc> ml64 test.asm
Microsoft (R) Macro Assembler (x64) Version 14.38.33135.0
Copyright (C) Microsoft Corporation.  All rights reserved.

 Assembling: test.asm
test.asm(8) : error A2008:syntax error : add
test.asm(13) : error A2008:syntax error : in instruction
test.asm(25) : error A2008:syntax error : ENDP
PS C:\work\mycc>

And there we have it, MSVC generates assembler code that the assembler cannot assemble. This is apparently well known, Microsoft stated in a support post that the generated MASM is for “illustrative purposes only”.

I tried all sorts of hacks some worked to allow me to create a function called ‘add’ but nothing worked for calling one. Posted on StackOverflow and godbolt forum – no help.

So I decided to switch assemblers, but which one to use. I looked at several

  • NASM, this seems to be the most popular and well maintained independent assembler
  • YASM, Yasm User Manual, another fairly common assembler. Seems to have started as a ‘better NASM’. Uses NASM-ish syntax
  • gcc / gas which is the assembler the book uses

I was looking for

  • Native Windows supports, i.e. runs on Windows and produces Windows code
  • Allows reserved words as function names (duh)
  • Supports windows SEH primitives
AssemblerNative Windows‘add’SEH
NASMYYN
YASMYYY
gasN*YY
MASMYNY

    *As far as I can see gas requires mingw to be installed, I didnt try though

    So it seems like YASM is the natural choice, but I decided to use NASM because it’s so widely used (I already had it installed from another project). Everything works fine without SEH support, so I won’t worry about it

    Switching to NASM

    This was remarkable easy. I had to update my driver code to run NASM when needed, and I replaced one file, x64gen.rs with nasmgen.rs. I will keep the original just in case. Its just no longer built.

    I was surprised that it took only an hour or so.

    Test Suite updates

    First there are some tests (only 1 for chapter 9) that have assembler components, so I updated the driver to call NASM for the .s file and then run MSVC for the other parts.

    Then added a Windows version of the assembler program: stack_alignment_check_windows.s

    I also found that the junk removal at the end of a test failed sometimes. This is because when a program finishes its resources have not necessarily been released yet, the OS is still cleaning things up, in particular the .exe is still locked. Many toolchains, test suites etc have found this. They typically retry the delete after a small delay. The WACC test suite was declaring this a failure, I modified the driver code to silently ignore the unlink failure

    
    Ran 446 tests in 65.583s
    
    OK
    PS C:\work\forks\writing-a-c-compiler-tests>

    Woohoo!


    On to the last chapter in part 1


    Comments

    Leave a comment