GoLogoThe "Go" tools
The GoAsm manual
GoAsm Assembler and Tools forum (in the MASM forum)

Writing 64-bit programs

GoLogo

by Jeremy Gordon - email

This file is intended for those interested in writing 64-bit programs for the AMD64 and EM64T processors running on x64 (64-bit Windows), using GoAsm (assembler), GoRC (resource compiler) and GoLink (linker). It may also be of interest to those writing 64-bit assembler programs for Windows using other tools.

Contents

Introduction to 64-bit programming
How easy is 64-bit programming?
Differences between 32-bit and 64-bit executables
Differences between Win32 and Win64 (for AMD64/EM64T)
Differences between x86 and x64 processors:
      registers
      instructions
      RIP-relative addressing
      call address sizes
64-bit programming in practice
Changes to Windows data types
Alignment requirements
Windows structures in 64-bit programming
Choice of register
Zero-extension of results into 64-bit registers
Sign-extension of results into qwords
Automatic stack alignment
Using the same source code for both 32 and 64-bits
Converting existing 32-bit code to 64-bit
Using AdaptAsm.exe to help with the conversion
Some pitfalls to avoid when converting existing source code
Switching using /x64 and /x86 in conditional assembly
Assembling and linking to produce the executable
Some code optimisation and refinement done by GoAsm
Some tips to reduce the size of your code
Demonstration files
Hello64World1 (simple 64-bit console program)
Hello64World2 (simple 64-bit windows program)
Hello64World3 (switchable 32-bit or 64-bit windows program)
More information, references and links

Introduction to 64-bit programming

How easy is 64-bit programming?uptop

Despite the differences between the 64-bit processors and their 32-bit counterparts, and between the x64 (Win64) operating system and Win32, using GoAsm to write 64-bit Windows programs is just as easy as it was in Win32.

In fact, you can readily use the same source code to create executables for both platforms if you follow a set of rules.

You can also convert existing 32-bit source code to 64-bits and some of the work required to do this can be done automatically using AdaptAsm.

Differences between 32-bit and 64-bit executablesuptop

Although 32-bit and 64-bit executables are based on the same PE (Portable Executable) format, in fact there are a number of major differences. The extent of those differences means that 32-bit code will only run on Win64 using the Windows on Windows (WOW64) subsystem. This works by intercepting API calls from the executable and converting the parameters to suit Win64. 64-bit code will not work at all on 32-bit platforms.

The executable contains a flag which tells the system at load-time whether it is 32-bit or 64-bit. If the x64 loader sees a 32-bit executable, WOW64 kicks-in automatically. This means that 32-bit and 64-bit code cannot be mixed within the same executable.

The significance of the above is that the programmer has to choose between:-

For those who are interested in PE file internals, here is a summary of the main differences between 32-bit and 64-bit executables:-

You can view the internals of the PE file using Wayne J. Radburn's PEview.

Differences between Win32 and Win64 (for AMD64/EM64T)uptop

Here are the main differences between Win32 and Win64 of relevance to the assembler or Windows programmer:-

Differences between x86 and x64 processorsuptop

The main differences are the expanded register range, some changes to instructions, and the use of RIP-relative addressing. The notes below refer to the AMD64 in 64-bit mode. In this mode the AMD64 can also run 32-bit executables naturally.

Registersuptop

The AMD64 adds several new registers to those available in the 86 series of processors, and also adds new ways to address the existing registers.

Instructionsuptop

RIP-Relative addressinguptop

Some instructions in the AMD64 processor which address data or code, use RIP-Relative addressing to do so. The relative address is contained in a dword which is part of the instruction. When using this type of addressing, the processor adds three values: (a) the contents of the dword containing the relative address (b) the length of the instruction and (c) the value of RIP (the current instruction pointer) at the beginning of the instruction. The resulting value is then regarded as the absolute address of the data and code to be addressed by the instruction. Since the relative address can be a negative value, it is possible to address data or code earlier in the image from RIP as well as later. The range is roughly ±2GB, depending on the instruction size. Since relative addressing cannot address outside this range, this is the practical size limit of 64-bit images.

RIP-relative addressing happens "behind the back" of the user. The processor uses it if the opcodes contain certain values (in the ModRM byte, the Mod field equals 00 binary, and the r/m field equals 101 binary). You cannot control this except by changing the type of instructions you use. Generally here are the rules which govern whether or not an instruction uses RIP-relative addressing:-

Bearing in mind that the image size is limited to 2GB by the above arrangements, it might be thought that the advantages of RIP-relative addressing are somewhat limited. This seems to be the case. It appears that the only advantage is that it lessens the number of relocations which would need to be carried out by the loader if a DLL is loaded at an address which is unexpected. The loader then would need to adjust all absolute addresses to suit the actual image base, but relative addresses would not have to be altered since they refer to other parts of the virtual image of the executable itself. However, it is good practice for the programmer to choose a suitable image base at link-time to avoid the need for relocations in a DLL in the first place. A good example of this is the system DLLs themselves. They all have a different image base which effectively avoids any prospective clashes of the image in memory which would require relocation at load-time.

Call address sizesuptop

In 64-bit assembly, a simple call to a code label eg.

CALL CALCULATE

will be coded as an E8 RIP-relative call, using a dword to provide the offset from RIP. The destination of this call might be an internal code label (ie. a procedure or function within the executable itself). Or it might be to an external code label, such as an API in a system Dll or to a code label exported by another exe or Dll. The first destination of a call to an external code label is to the Import Address Table which is part of the executable itself. This table is written over by the loader when the executable starts. Therefore during run-time the table contains the absolute addresses in virtual memory of the eventual destination of the call. In a 64-bit executable, the table contains 64-bit values, so the E8 RIP-relative call is capable of calling a procedure or function anywhere in memory.

Calls to memory addresses either held in a label, or in registers, or in memory pointed to by registers, however, are dealt with in a different way. They are not channelled through the Import Address Table. These calls must also permit the destination of the call to be anywhere in memory. In order to achieve this they must themselves use 64-bit absolute addresses. Examples of these types of calls are:-

CALL RAX
CALL EAX           ;codes the same as CALL RAX
CALL [Table+8h]
CALL [RSI]
CALL [ESI]         ;codes the same as CALL [RSI]

Here you need to be careful that you are in fact giving a qword to the call, and not just a dword.
See some pitfalls to avoid when converting existing source code.

Changes to Windows data types

Here is a list of the changes to data types between 32 and 64-bits:-

All handles now qwords not dwords

eg.
HACCEL, HINSTANCE, HBRUSH, HBITMAP
HCOLORSPACE, HCURSOR, HDC, HFONT
HICON, HINSTANCE, HKEY, HLOCAL
HMENU, HMODULE, HPEN, HPALETTE, HWND
(and others starting with H)

exceptions:- HRESULT, HFILE which remain dwords, and HALF_PTR (see below)

All pointers now qwords not dwords

eg.
LPCSTR, LPCTSTR, LPLONG, LPSTR
(and others starting with LP)
PBOOL, PHANDLE, PHKEY, PVOID
(and others starting with P)
DWORD_PTR, ULONG_PTR, UINT_PTR
(and others ending with _PTR)
and LRESULT

exceptions:- HALF_PTR, and UHALF_PTR which are now dwords instead of a word and POINTER_32 which remains a 32-bit pointer

WPARAM and LPARAM now qwords not dwords

Here is a list of the data types which remain the same:-

ATOM         remains a word
BOOL         remains a dword
CHAR         remains a byte
DWORDLONG    remains a qword
COLORREF     remains a dword
INT          remains a dword
INT32        remains a dword
INT64        remains a qword
LANGID       remains a word
LCTYPE       remains a dword
LCID         remains a dword
LGRPID       remains a dword
LONG         remains a dword
LONG32       remains a dword
LONG64       remains a qword
LONGLONG     remains a qword
POINT        remains two dwords
RECT         remains four dwords
SHORT        remains a word
UINT         remains a dword
UINT32       remains a dword
UINT64       remains a qword
ULONG        remains a dword
ULONG32      remains a dword
ULONG64      remains a qword
ULONGLONG    remains a qword
USHORT       remains a word

Using the switched type indicator

The above change of a data type may require a corresponding change to a type indicator. The letter P is reserved as a type indicator in all situations when GoAsm might expect to find one. So you can have this switch:-

#if x64
P = 8
#else
P = 4
#endif

P can be switched to the equivalent of any of the pre-defined type indicators that is B, W, D, Q or T. In this case it is switched either to Q (value 8) or to D (value 4). Therefore you can control the size of the instruction with it, for example:-

MOV P[RDI],0          ;zero a qword at RDI if 64-bit, dword at EDI if 32-bit
LOCAL POINTERS[10]:P  ;make 80 byte local pointer buffer if 64-bit, 40 byte if 32-bit

Alignment requirements

The requirements of the system in Win64 for correct alignment of the stack pointer, data, and structure members are much stricter than in Win32. Wrong alignment can cause at best a loss of performance and at worst, an exception or program exit.

Stack alignment

The stack pointer (RSP) must be 16-byte aligned when making a call to an API. However, this is organised automatically by GoAsm if you use INVOKE, see automatic stack alignment.

Data alignment

All data must be aligned on a "natural boundary". So a byte can be byte-aligned, a word should be 2-byte aligned, a dword should be 4-byte aligned, and a qword should be 8-byte aligned. A tword should also be qword aligned. GoAsm deals with alignment automatically for you when you declare local data (within a FRAME or USEDATA area). But you will need to organise your own data declarations to ensure that the data is properly aligned. The easiest way to do this is to declare all qwords first, then all dwords, then all words and finally all bytes. Twords (being 10 bytes) would put out the alignment for later declarations, so you could declare all those first and then put the data back into alignment ready for the qwords by using ALIGN 8.

As for strings, in accordance with the above rules, Unicode strings must be 2-byte aligned, whereas ANSI strings can be byte aligned.

When structures are used they need to be aligned on the natural boundary of the largest member. All structure members must also be aligned properly, and the structure itself needs to be padded to end on a natural boundary (the system can write in this area). Because of the importance of this, from Version 0.56 (beta), GoAsm aligns structures automatically for you. See automatic alignment and padding of structures and structure members for more.

Windows structures in 64-bit programminguptop

Windows often uses structures to send and receive information using the APIs. In 64-bits these structures are likely to be significantly different from their 32-bit counterparts because of the enlargement of many data types to 64-bits. See changes to Windows data types. Take for example the WNDCLASS structure which is used when you want to register a window class:-

WNDCLASS STRUCT
        style DD 0    ;+0 window class style
              DD 0    ;+4 padding for next
  lpfnWndProc DQ 0    ;+8 pointer to Window Procedure
              DD 0    ;+10 no. of extra bytes to allocate after structure
              DD 0    ;+14 no. of extra bytes to allocate after window instance
    hInstance DQ 0    ;+18 handle to instance containing window procedure
        hIcon DQ 0    ;+20 handle to the class icon
      hCursor DQ 0    ;+28 handle to the class cursor
hbrBackground DQ 0    ;+30 identifies the class background brush
 lpszMenuName DQ 0    ;+38 pointer to resource name for class menu
lpszClassName DQ 0    ;+40 pointer to string for window class name
ENDS

A number of the members are now qwords, whereas previously they were dwords as you can see from the 32-bit version below. The class style at offset +0h remains a dword, but then in the 64-bit version, padding of four bytes is required because the next member is a qword. This complies with the requirement that structure members are aligned on their natural boundary. A qword is used to provide space for the pointers firstly to the window procedure itself at +8h, to menu name at +38h and to the window class name at +40h. This is despite the fact that 64-programming as implemented by Win64 for the AMD64 processor only uses 32-bit pointers where those pointers give the addresses of internal data. Presumably the reason for this is that the same structures as being used here as are used for the IA64 family of processors (which use 64-bit pointers to internal data). Handles in the structure are also enlarged to 64-bits.

WNDCLASS STRUCT
        style DD 0    ;+0 window class style
  lpfnWndProc DD 0    ;+4 pointer to Window Procedure
              DD 0    ;+8 no. of extra bytes to allocate after structure
              DD 0    ;+C no. of extra bytes to allocate after window instance
    hInstance DD 0    ;+10 handle to instance containing window procedure
        hIcon DD 0    ;+14 handle to the class icon
      hCursor DD 0    ;+18 handle to the class cursor
hbrBackground DD 0    ;+1C identifies the class background brush
 lpszMenuName DD 0    ;+20 pointer to resource name for class menu
lpszClassName DD 0    ;+24 pointer to string for window class name
ENDS

Here is another example, this time the structure DRAWITEMSTRUCT. First, lets have a look at the 32-bit version in the form you would find it in the SDK:-

    UINT CtlType      ;+0
    UINT CtlID        ;+4
    UINT itemID       ;+8
    UINT itemAction   ;+C
    UINT itemState    ;+10
    HWND hwndItem     ;+14
    HDC hDC           ;+18
    RECT rcItem       ;+1C
    ULONG_PTR itemData;+2C
(total size of structure is 30h bytes)

In 64-bits this structure becomes:-

    UINT CtlType      ;+0
    UINT CtlID        ;+4
    UINT itemID       ;+8
    UINT itemAction   ;+C
    UINT itemState    ;+10
    padding dword    HWND hwndItem     ;+18    HDC hDC           ;+20
    RECT rcItem       ;+28
    ULONG_PTR itemData;+38
(total size of structure is 40h bytes)

It is also a requirement that the structure is enlarged so that it ends on the natural boundary of its largest member. This is achieved by adding the necessary padding at the end of the structure. So PAINTSTRUCT becomes:-

PAINTSTRUCT STRUCT
            DQ 0      ;+0 hDC
            DD 0      ;+8 fErase
      left  DD 0      ;+C  left   )
       top  DD 0      ;+10 top    ) RECT
     right  DD 0      ;+14 right  )
    bottom  DD 0      ;+18 bottom )
            DD 0      ;+1C fRestore
            DD 0      ;+20 fIncUpdate
            DB 32 DUP 0   ;+24 rgbReserved
            DD 0      ;+44 padding to being total size to 72 bytes
ENDS

In practice it was found that the system wrote to the area of padding at +44h when using PAINTSTRUCT in certain circumstances. This shows the importance of complying with these rules (otherwise you could find that data after the structure could be written over).

Note that the beginning of structures must be aligned on the natural boundary of the largest member as well. All the above rules ensure, therefore, that qwords in the structure are always qword aligned.

Automatic alignment and padding of structures and structure members

As we have seen correct alignment of structures and structure members is crucial for proper operation of 64-bit code. Unfortunately the Windows header files containing the structure definitions do not necessarily contain the necessary padding to achieve such alignment.

So from Version 0.56, GoAsm does this work automatically for you as follows:-

  1. GoAsm always aligns the structure itself to the correct data boundary.
  2. GoAsm always pads if necessary to ensure that structure members are on their natural boundary. So in the MSG structure example below, the padding at +0Ch could be left out. It would be inserted automatically.
  3. GoAsm always adds padding at the end of a structure so that the structure ends on a natural boundary. So in the example below the padding at +2Ch could be left out. It would be inserted automatically.
  4. The symbols created when using a structure are automatically adjusted to suit the alignment and padding which is applied.
MSG      DQ 0         ;+0h hWnd
         DD 0         ;+8h message
         DD 0         ;padding for next
         DQ 0         ;+10h wParam
         DQ 0         ;+18h lParam
         DD 0         ;+20h time
         DD 0         ;+24h 1st part of point structure
         DD 0         ;+28h 2nd part of point structure
         DD 0         ;+2Ch padding to bring the overall size to 48 bytes

You can see what alignment and padding GoAsm has added to your source code if you specify /l in GoAsm's command line. This will create a list file. Also you can view the effect in a debugger.

Structures - the overall picture

Choice of registeruptop

See also some tips to reduce the size of your code which has some additional implications for your choice of registers
and also some pitfalls to avoid when converting existing source code.

Zero-extension of results into 64-bit registersuptop

Take care when mixing the 64-bit registers and their 32-bit counterparts because the processor can change the contents of the whole 64-bit register when this is not obvious. This is because when writing results to a 32-bit register the processor will zero-extend the result into the whole 64-bits of the register. So, for example:-

MOV RAX,-1              ;fill RAX with 0FFFFFFFF FFFFFFFFh
AND EAX,0F0F0F0Fh       ;(apparently) work only on EAX

but the processor will zero extend the result into RAX, in other words it will zero the whole of the high dword of RAX. The result in RAX is 00000000 0F0F0F0Fh not 0FFFFFFFF 0F0F0F0Fh as expected. This happens irrespective of the value of bit 31 of RAX (this is not the same as sign-extension).

A similar thing happens when using other instructions. Here is an example with XOR:-

MOV RAX,-1              ;fill RAX with 0FFFFFFFF FFFFFFFFh
XOR EAX,EAX             ;(apparently) zero EAX

The actual result in RAX is zero.

It also happens with the mov instruction, for example:-

MOV RCX,1111111111111111h
MOV ECX,88888888h

The result is RCX=88888888h

You can take advantage of zero-extension in various ways. Some examples are given in some tips to reduce the size of your code. Take also this example, where the structure RECT (which is four dwords) contains values which must be passed to the API MoveWindow as qwords:-

MOV RBX,ADDR RECT
MOV EAX,[EBX]      ;get x-pos
MOV ECX,[EBX+4]    ;get y-pos
MOV EDX,[EBX+8]    ;get right
SUB EDX,EAX        ;get width
MOV R8D,[EBX+0Ch]  ;get bottom
SUB R8D,ECX        ;get height
INVOKE MoveWindow,[hWnd],RAX,RCX,RDX,R8,0

Here only 32-bit registers are used to extract the information from the RECT structure, but we know that the high part of the 64-bit versions of those registers are set to zero.

It is possible that there is a performance loss in relying on zero-extension. Some of the documentation suggests that the processor has to carry out an additional operation to zero the high bits of the register.

Sign-extension of results into qwordsuptop

You may wonder about the difference between the following instructions:-

MOV D[THING],12345678h
MOV Q[THING],12345678h

These code differently and do different things. The dword version places the value 12345678h into the dword at the label THING as you would expect. The qword version does the same, but also zeroes the dword at THING+4. This is because it sign-extends the result into the qword at the label THING. So if the high bit is set, the qword version will fill THING+4 with 0FFFFFFFFh. In other words, the 32-bit value in these instructions are regarded as signed numbers, and written to memory accordingly.

MOV D[THING],12345678h   ;THING is now 12345678h (as dword)
MOV Q[THING],12345678h   ;THING is now 12345678h (as qword)
MOV D[THING],87654321h   ;THING is now 87654321h ie. -789ABCDFh (as dword)
MOV Q[THING],87654321h   ;THING is now 0FFFFFFFF 87654321h ie. -789ABCDFh (as qword)

The same happens if you use a register to address the data area for example:-

MOV RSI,ADDR THING
MOV D[RSI],12345678h     ;THING is now 12345678h (as dword)
MOV Q[RSI],12345678h     ;THING is now 12345678h (as qword)
MOV Q[RSI],87654321h     ;THING is now 0FFFFFFFF 87654321h ie. -789ABCDFh (as qword)

Note that you can't put more than 4 bytes into memory directly using the MOV instruction even though you are using 64-bit code, so this shows an error:-

MOV Q[THING],123456789ABCDEFh

Instead, to achieve this result you would use the following code:-

MOV RAX,123456789ABCDEFh
MOV [THING],RAX

You may need to use the C style LL suffix to avoid sign extension if that is not intended:-

MOV RAX,87654321h        ;32-bit sign extended to 0FFFFFFFF 87654321
MOV RAX,0x87654321LL     ;encodes 64-bit 00000000 87654321

Automatic stack alignmentuptop

The stack pointer (RSP) must be 16-byte aligned when making a call to an API. With some APIs this does not matter, but with other APIs wrong stack alignment will cause an exception. Some APIs will handle the exception themselves and align the stack as required (this will, however, cause performance to suffer). Other APIs (at least on early builds of x64) cannot handle the exception and unless you are running the application under debug control, it will exit.

Because of this requirement, the Win64 documentation states that you can only call an API within a stack frame. This is because it is assumed that only within a stack frame can the stack be guaranteed to be aligned properly. A call out of the stack frame will misalign the stack by 8 bytes.

This requirement is very restrictive to assembler programmers, and causes compilers a big headache. GoAsm's solution to this problem is to insert special coding before and after each API call (when INVOKE is used) to ensure that the stack is always properly aligned at the time of the call. This liberates the assembler programmer, and means that:-

The overhead for aligning the stack at the time of each API call is an additional nine bytes per API, which seems a small price to pay for the advantages gained. To keep down the size of the code as much as possible, GoAsm takes a number of opportunities to optimise the code particularly when inserting the parameters. See some optimisation done by GoAsm for details. See also coding to achieve automatic stack alignment.

Using the same source code for both 32 and 64-bitsuptop

The GoAsm manual describes the use of ARG and INVOKE in the section dealing with calls to Windows APIs in 32-bits and 64-bits and the use of FRAME...ENDF in the section dealing with callback stack frames in 32-bits and 64-bits. GoAsm's ARG and INVOKE and FRAME...ENDF constructs effectively deal with the changes in the calling convention in 64-bit programming.

Bringing together all those considerations and also those set out above, it is perfectly possible to use the same source code to create executables for both 32-bit and 64-bit platforms.

To recap, here are the rules which must be followed to do this:-

The "Go" tools will do the rest of the work.

Note that x86 should not be used in the command line for Win32 source code (use it only for 32/64-bit switchable source code).

See the file Hello64World3 for example source code which can make either a simple Win32 "Hello World" Window program or a Win64 one.

Converting existing 32-bit code to 64-bituptop

Bringing together all the above considerations, this is what you need to do to convert existing 32-bit source code to 64-bit source.

AdaptAsm can do some of the above work for you.

Using AdaptAsm.exe to help with the conversionuptop

AdaptAsm comes packaged with GoAsm and I originally wrote it to help to convert source code used for other assemblers to GoAsm syntax. I have now extended it to help towards the conversion of 32-bit source code to 64-bit source code. This works both on GoAsm source code and also source code for other assemblers.

For full details of AdaptAsm's other rôles see the GoAsm manual.

You use AdaptAsm from the command line using the following:-

AdaptAsm [command line switches] inputfile[.ext]

If no input extension is specified, .asm is assumed.
If no output extension is specified, .adt is assumed
The command line switches are:-

/h=this help
/a=adapt a386 file
/m=adapt masm file
/n=adapt nasm file
/fo=specify output path/file eg. /fo GoAsm\adapted.asm
/l=create log output file
/o=don't ask before overwriting input file
/x64=adapt file for 64-bits
What AdaptAsm does when helping to adapt a file to 64-bits using the /x64 switch
CALLs to APIs are changed to INVOKE (CALLs to non-APIs are not affected).
AdaptAsm does this by looking at lists of APIs in ".h.txt" files in the same folder as AdaptAsm.exe. See the ".h.txt" files for more information about these files.
This works with all types of calls even if enclosed in square brackets and even if dependent on a define (equate) or a switch, for example:-
CALL ExitProcess        ;changed to INVOKE
CALL [ExitProcess]      ;changed to INVOKE
CALL INTERNAL_PROC      ;not changed
CALL SendMessage        ;changed to INVOKE
CALL SendMessageA       ;changed to INVOKE
CALL SendMessageW       ;changed to INVOKE
CALL SendMessage##AW    ;changed to INVOKE
Changing PUSH to ARG for the parameters sent to the API. AdaptAsm does this by counting the correct number of parameters back from the CALL and comparing this with the correct number of parameters in the lists of APIs in ".h.txt" files in the same folder as AdaptAsm.exe. See the ".h.txt" files for more information about these files.
Here are some simple examples:-
PUSH EBX,0,1100h,[hMessTV]      ;PUSH is changed to ARG (and EBX changed to RBX)
CALL SendMessageA               ;CALL is changed to INVOKE
PUSH EBX,0                      ;PUSH is changed to ARG (and EBX changed to RBX)
PUSH 1100h                      ;PUSH is changed to ARG
PUSH [hMessTV]                  ;PUSH is changed to ARG
CALL SendMessageA               ;CALL is changed to INVOKE
You may have preserved registers across API calls and these are unaffected, for example:-
PUSH EAX                        ;PUSH not changed (but EAX changed to RAX)
PUSH EBX,0,1100h,[hMessTV]      ;PUSH is changed to ARG (and EBX changed to RBX)
CALL SendMessageA               ;CALL is changed to INVOKE
POP EAX                         ;POP not changed (but EAX changed to RAX)
However, if you have mixed these two uses of PUSH AdaptAsm will show an error by changing the PUSH to ARG and noting the problem in the log file:-
PUSH EAX,EBX,0,1100h,[hMessTV]  ;PUSH is changed to ARG (too many parameters)
CALL SendMessageA               ;CALL is changed to INVOKE
POP EAX                         ;restore eax register
If AdaptAsm cannot find all the expected parameters it shows an error by changing the CALL to INVOKE and noting the problem in the log file, for example:-
CALL INTERNAL_PROC              ;not changed
PUSH 0,1100h,[hMessTV]          ;PUSH is changed to ARG
CALL SendMessageA               ;CALL is changed to INVOKE (too few parameters)
This means that this type of thing which could be done in 32-bits, will show up as as error by AdaptAsm (and rightly so, since in 64-bit assembler each CALL must immediately follow the parameters):-
PUSH 0,EAX,14Eh,[hComboSev]     ;14Eh=CB_SETCURSEL
PUSH 0,EAX,151h,[hComboSev]     ;151h=CB_SETITEMDATA
CALL SendMessageA
CALL SendMessageA
32-bit general purpose registers in square brackets are changed to their 64-bit counterparts so that they can be used for both 32-bit and 64-bit assembly, for example:-
MOV EAX,[EAX+EBX]         ;changed to MOV EAX,[RAX+RBX]
MOV D[EBX*8+EBP],8h       ;changed to MOV D[RBX*8+RBP],8h
CALL [EBX]                ;changed to CALL [RBX]
INVOKE ExitProcess,[EBX]  ;changed to INVOKE ExitProcess,[RBX]
PUSH [EBX]                ;changed to PUSH [RBX] or ARG [RBX]
POP [EBX]                 ;changed to POP [RBX]
Where a pointer is used with a 32-bit general purpose register, the register is changed to its 64-bit counterpart, for example:-
MOV EAX,ADDR THING        ;changed to MOV RAX,ADDR THING
CMP ESI,ADDR THING        ;changed to CMP RSI,ADDR THING
MOV EBP,OFFSET THING      ;changed to MOV RBP,OFFSET THING
LEA EAX,THING             ;changed to LEA RAX,THING
Although not strictly necessary, for good measure 32-bit general purpose registers after PUSH, POP and INVOKE are changed to their 64-bit counterparts, for example:-
PUSH EAX,EBX              ;changed to PUSH RAX,RBX
POP EBX,EAX               ;changed to POP RBX,RAX
INVOKE ExitProcess,EBX    ;changed to INVOKE ExitProcess,RBX
What AdaptAsm does not do (and you need to do by hand)
AdaptAsm cannot decide for you which register to use in other circumstances. You will have to decide this on a case-by-case basis see choice of registers for some guidance on this.
AdaptAsm does not ensure that structures and data sizes are correct for 64-bit use, nor that the pointers to structures and strings are properly aligned.

The "h.txt" files used by AdaptAsm with the /x64 switchuptop

These files are text files containing lists of APIs and the number of parameters required by each API. AdaptAsm looks inside its own folder for such h.txt files. The "h.txt" files are created from Microsoft header files using a clever javascript file ApiParamCount.js, written by Leland M George of West Virginia, who has kindly donated it to the public domain. This js file is shipped with AdaptAsm together with some ready-made h.txt files containing the most commonly used APIs. If your program uses APIs declared in other header files you can make your own "h.txt" files using the js file. There are two ways to use the js file:-

Alternatively you can make your own h.txt file or edit the existing ones. The format is as follows:-

Switching using x64 and x86 in conditional assemblyuptop

As well as switching to 64-bit or 32-bit assembly, specifying /x64 or /x86 in GoAsm's command line also permits these words to be tested in conditional assembly. So, for example, you can switch two different generalised window procedures in this way:-

WndProcTable:
#if X64
MOV EAX,ADDR MESSAGES   ;give eax the list of messages to deal with
CALL GENERAL_WNDPROC64  ;call the generic message handler (64-bit version)
#else
MOV EDX,ADDR MESSAGES   ;give edx the list of messages to deal with
CALL GENERAL_WNDPROC    ;call the generic message handler (32-bit version)
#endif
RET

Note that the words "x64" and "x86" are not case sensitive.

Here is another example to switch include files including structures:-

#if X64
#include structures64.inc
#else
#include structures32.inc
#endif

Some pitfalls to avoid when converting existing source codeuptop

Assembling and linking to produce the executableuptop

To make a 64-bit object file with GoAsm use this command line:-

GoAsm /x64 filename

where filename is the name of your asm file written either as a 64-bit source file or a 32/64 switchable source file. Use /x86 instead of /x64 when assembling a 32/64 switchable source file to make a 32-bit version. The object file created by GoAsm can be sent to GoLink or another linker in the usual way. GoLink automatically senses whether the object file is 32 or 64-bit and creates the correct type of executable to suit. You cannot mix 32-bit and 64-bit object files. GoLink will show an error if you try to do this.

You do not necessarily need to make 64-bit executables on a 64-bit machine. This is because the DLL names given to GoLink simply tell the linker that the DLL contains the APIs used by the application and these tend to be the same between the two platforms. If your application calls APIs specific to the 64-bit system however, this does not work.

Some optimisation and refinement done by GoAsmuptop

GoAsm always aims to produce the tightest possible code from your source. In the case of x64, GoAsm has not yet taken up all opportunities to optimise the code. This is because there are still some unknowns, such as effects on performance of optimised code on x64.

The optimisations and refinements are listed here to help you when you look at the code produced by GoAsm in the debugger.

GoAsm optimisations and refinements in all code

None of these affect the flags or adversely affect performance.
  • MOV 64-bit register,ADDR label changed to LEA 64-bit register,label. This saves 5 opcodes. One important difference between the two instructions is that the MOV version uses an absolute relocation (hence in theory it needs to leave space for a 64-bit value to be inserted by the linker). The LEA instruction uses RIP-relative addressing and so it can do the same job but requires only a 32-bit space for the relative address.
  • PUSH or ARG ADDR Non_Local_Label also uses LEA as well as the R11 register as follows:-
    LEA R11,ADDR Non_Local_Label
    PUSH R11
    
    See explanation for this. Note that this will also take place with INVOKE when pushing arguments with ADDR, which also includes use of pointers to a string or raw data (ex. 'Hello' or <'H','i',0>).

This affects the flags.
  • PUSH or ARG ADDR Local_Label is coded as follows:-
    PUSH RBP
    ADD D[RSP],+/-Displacement
    


Additional optimisations and refinements only when INVOKE is used

These may affect the flags which does not matter when calling an API. Those that rely on zero-extension may require another operation from the processor, but it is assumed that this does not matter when calling an API. It is more important to keep the code size down.
  • A register parameter containing zero is optimised using XOR 32-bit register. This is a saving of between 7 and 8 bytes over the MOV equivalent.
  • A register parameter containing a number (an "immediate") which can fit into 32-bits is changed to use a 32-bit register, saving between 1 and 5 bytes depending on the register and the number.
  • A register parameter containing -1 is achieved by using OR 64-bit register,-1 saving 6 bytes.
  • If the parameter is already in the correct register no further code is emitted because it is not required.
  • The coding to achieve automatic stack alignment and to adjust the stack for the FASTCALL calling convention is as follows (which one is used depends on the number of parameters):-
    PUSH RSP             ;save current RSP position on the stack
    PUSH [RSP]           ;keep another copy of that on the stack
    AND SPL,0F0h         ;adjust RSP to align the stack if not already there
                         ;
                         ;  parameters dealt with here
                         ;
    SUB RSP,20h          ;adjust RSP to provide placeholders
    CALL TheAPI
    LEA RSP,[RSP+xxh]    ;get RSP back to correct place for next
    POP RSP              ;restore RSP to its original value
    
    or
    PUSH RSP             ;save current RSP position on the stack
    PUSH [RSP]           ;keep another copy of that on the stack
    OR SPL,8h            ;adjust RSP to align the stack if not already there
                         ;
                         ;  parameters dealt with here
                         ;
    SUB RSP,20h          ;adjust RSP to provide placeholders
    CALL TheAPI
    LEA RSP,[RSP+xxh]    ;get RSP back to correct place for next
    POP RSP              ;restore RSP to its original value
    

Some tips to reduce the size of your codeuptop

Note it is possible some of these optimisations may adversely affect performance..

See also general tips for programming in GoAsm help.


More information, references and links uptop

Information about the AMD64
AMD information for developers
Intel 64 Technology site

Newsgroups and forums:-
64-bit Assembler forum
AMD Developer Community
Start64 forum


Copyright © Jeremy Gordon 2006-2022
Back to top