Here's a 32 bit Windows hello world:
.global main
.section .text
main:
leal message, %eax
pushl %eax // the C convention is params are pushed onto the stack
call printf
popl %eax // and the caller is responsible for cleanup
pushl $0
call exit
hlt // never reached
.data
message:
.string "Hello, world!\n\0"
Build with this: tcc test.s -lmsvcrt
(the filename has to end with .s for tcc to recognize it as an asm file)
That calls C functions to do the work. You can also use just Windows functions:
.global main
.section .text
main:
pushl $-11 // STD_OUTPUT_HANDLE
call GetStdHandle // the return value will be in eax
// now we'll call WriteFile. Args are pushed from right to left
pushl $0 // lpOverlapped == null, don't want async
pushl $0 // lpNumberOfBytesWritten == null, we don't care
pushl $14 // nNumberOfBytesToWrite, our hello world has 14 chars.
leal message, %ebx // load address for lpBuffer, pointer to the string to write
pushl %ebx // and push it to the stack
pushl %eax // the return value we got from GetStdHandle
call WriteFile // and write the file
// unlike the C convention, the Windows functions clean up their own stack
// so we don't have to pop that stuff
// and now we'll call ExitProcess to get out
pushl $0 // our main's return value of 0 == success
call ExitProcess
hlt // never reached
.data
message:
.string "Hello, world!\n\0"// so we don't have to pop that stuff
no need for the C lib here, so build with a simple tcc test.s
, and when you run it from a console, you should see the message in there.
Fun fact: the Windows functions are easy enough to use that you could pop up a message box simply enough:
.global main
.section .text
main:
pushl $0 // MB_OK
pushl $0 // title = null (will use the default of "Error")
leal message, %eax // load our message
pushl %eax // and push the string pointer to the argument list
pushl $0 // hwnd == null, no owning window
call MessageBoxA // and pop up the message box
pushl $0
call ExitProcess
hlt // never reached
.data
message:
.string "Hello, world!\n\0" // don't forget the zero terminator!
MessageBoxA is found in user32, so build with tcc test.s -luser32
, run and you'll get that.
This is all win32 because I don't know a lot of win64, but the 32 bit program should still work just as well and you can play around with it. tcc uses the AT&T assembler syntax, which isn't as common, but to convert nasm or most other assembler's Intel syntax to it, remember that:
it needs the length suffixes on the instruction. so pushl
instead of push
when pushing a long, aka 32 bit value.
immediates, number literals, are prefixed with the $. Actually, now that I write this, I realize I could have written pushl $message
above instead of the two-step leal message, %eax pushl %eax
. Oh well, both ways work. But if you forget the $, it tries to use it as a pointer and you're likely to see illegal read/write at small looking memory addresses when loading things.
register names need the % prefix
It goes source, destination, which is the opposite of Intel syntax's destination, source. (I learned the intel way as a newb, so AT&T syntax feels backward to me!)
But if you remember those four differences, you can translate Intel syntax code to AT&T syntax code for tcc fairly easily. A minor change is that tcc uses C style comments ( // comment
), whereas most other assemblers use ; command
Another thing with tcc is you don't have to declare extern functions, nor use the argument length suffix or underscore prefix. You write it almost like in C, though the ASCII or Wide suffix needs to be there (those are macros in C; #define MessageBox MessageBoxA
and so in in the Windows headers, changing out the A for W if you are compiling with unicode support. The difference is the A versions take ascii strings - 8 bits per character, can't access all characters, and the W versions take 16 bit unicode strings.).
Side note: the actual mangled name of Win32 functions is something like _MessageBoxA@16. You can see we did use the 'A' in the example code, as I just mentioned, but not the _ nor the @16. Those bits of the name are hidden from C and are used to help catch wrong number of arguments - @16 means it expects 16 bytes of arguments, for Message Box in 32 bit, that's the hwnd (4, HWND), pointer to message (4, char*), pointer to title (4, char*), and the flags (4, an int). If you see example code on the internet calling functions with these changes, that's why it is there, and you do not need it with tcc.
That should get you started!