The "Go" tools
     The GoAsm manual

some programming ....

hints and tips

by Jeremy Gordon -

This file is intended for those interested in 32 bit assembler programming, in particular for Windows.

Some programming hints:-
Traditional use of registers
Use of Windows: register and stack
Push registers alphabetically
Protective coding
Code incrementally
Make re-usable functions
Split up your functions if too large
Return values and flags from functions
Provide good comments and descriptions
Best order of source script
Best direction of conditional jumps
Lower or upper case?
Save your work regularly
Some tips of the trade:-
Setting the flags
Checking for zero
Setting to a number
Using INC and DEC instead of ADD and SUB
Using the 16-bit registers
Multiplying using LEA
Using a register more than once
Comment out lines
Check before using MMX, SSE or SSE2
During development assemble and link with debug symbols
Finding errors in your programs

Traditional use of registers

Some instructions use particular registers to perform certain tasks; some instructions are faster if certain registers are used; in early processors not all the registers could do everything as they do now. These three facts, together with established traditional use of registers by assembler programmers over the years, have established expectations about how the registers should be used. If you stick to these it will help with the readability of your code.
  • Use EAX to pass data to a procedure and to return data from a procedure to the calling code. The Windows APIs themselves use EAX to return a value to the caller. AL, AX and EAX should also be used as far as possible to receive data from memory and loading data to memory since they may work slightly quicker than other registers. For example use MOV AL,[ESI] in preference to MOV DL,[ESI]. Also if you need to use ADD, AND, ADC, CMP, MOV, OR, SUB, TEST, XCHG, XOR with an immediate value (ie. a number like MOV AL,23h) use AL, AX or EAX if you can since the instruction uses fewer opcodes than if you use another register.
  • Use the EDX register as a backup for EAX if that is already in use.
  • Use the ECX register as a counter. JECXZ is a special instruction that tells you if ECX is zero and the LOOP, SCAS, MOVS series of instructions all use ECX as a counter.
  • Use EBX to hold data generally or to address memory for example MOV EAX,[EBX] or MOV [EBX],EDX.
  • Use ESI where you need to read from memory, eg. MOV EAX,[ESI] and EDI where you need to write to memory eg. MOV [EDI],EAX. This is consistent with the LODSD, STOSD and MOVSD instructions.
  • Use any of the registers as base or index registers in complex memory instructions eg. MOV EAX,[MemPtr+ESI*4+ECX]
  • Never use ESP for anything other than a pointer to the stack, unless you have a routine which has no stack activity at all. Then you might save the value of ESP in memory and restore it before returning to the caller of the routine.
  • Traditionally EBP is used to address local data on the stack in callback routines. EBP and its 16-bit component BP can be used as a general register in Windows programming, but you will have to be very careful if you are using stack frames (FRAME in GoAsm) or local data (LOCAL in GoAsm). This is because stack frame parameters and local data are addressed using EBP plus or minus value. After EBP is changed the parameters and local data cannot be accessed until EBP is restored to its previous value. Note that in 16-bit code BP used to address the stack segment unless used with a segment override, but in 32 bit windows EBP can be used to address any part of the 4GB "flat" memory area.
  • CS, DS and SS are still used by Windows even in 32 bit code, so you should not use these. And these days you can't use ES,FS or GS either. In Windows 98 and upwards this causes an exception.
  • If you need to use the ordinary registers to hold 64 bit information use EDX:EAX, where EDX holds the most significant bits. This accords with the 64 bit shift instructions SHLD and SHRD and also with CDQ.

Use of Windows: registers and stacktop

You can rely on the fact that all Windows APIs save and restore the registers EBP,EBX,EDI and ESI. Therefore use these registers to keep handles and pointers which need to be used more than once through sequences of API calls.
Just as Windows maintains the value of these registers in the APIs you too must ensure that they are maintained in your callback procedures. I recommend that in each your callback and window procedures you save these registers at the beginning of the procedure and restore them at the end.
You can also rely on Windows APIs restoring the stack, given the correct number of parameters. However I have come across one exception to this (wsprintf). This is documented in the Windows SDK.

Push registers alphabeticallytop

If you need to preserve a series of registers in a particular procedure it is a good idea to PUSH them in alphabetical order. That way you can easily check that the POPs are in reverse alphabetical order so that the original values for the registers are correctly restored. For example:-
PUSH EBP,EBX,EDI,ESI
.
.                       ;your code goes here
.
POP ESI,EDI,EBX,EBP
If you prefer, in GoAsm you can use the USES statement which will preserve and restore the registers automatically for you.

Protective codingtop

It is important to make your programs as robust as possible by reducing to a minimum the chance of a program crash or infinite loop. Here are some ways to do this.
  • Before using the REP, REPZ or REPNZ (repeat) mnemonics which use ECX as a countdown register, and before using LOOP, LOOPZ or LOOPNZ always check ECX for zero. If the instruction is carried out with ECX=0 it will do 4,294,967,296 operations! If you want to be doubly cautious, you could also check ECX for high bit set using OR ECX,ECX then JS >L2
  • Avoid dividing by zero by always checking the register in a DIV ECX or similar instruction.
  • Whenever using DIV ECX or DIV CX (or similar) always set EDX to zero if it is unused (these instructions will try to divide the number in EDX:EAX or DX:AX respectively).
  • MUL can cause an overflow exception if the values given to it are too high. If these values are in registers check they are not too high before calling MUL.
  • Before trying to read from, or write to memory using a register, check that the register is not zero. As an extra precaution you could call the Windows API IsBadReadPtr.
  • Protect you code using exception handling. See my article on this available from www.GoDevTool.com.

Code incrementallytop

When writing new code as soon as you have completed a discrete part of it, test it in real time under all possible conditions and also if necessary run through it in single-step with the debugger. This way, if your program does something unexpected you can be reasonably sure the fault lies in the new code you have just written. If you leave the testing until you have written some other code, the fault will be more difficult to find.

Make re-usable functionstop

Take into account that some functions you write for your program may be of use in other parts of the program or in future programs you write. You will then end up with a number of re-usable modules which do specific jobs, like loading a string to memory, writing a decimal value to memory, dividing by ten, loading a dialog font, or loading a new window title. Give the functions obvious names like LOAD_STRINGEDI or DECIMAL_WRITE or DIVIDE_BY_TEN and so on. You could ensure that these modules themselves also declare data they use to make them even more independent of their callers. In object orientated programming these modules would be called "objects". So you are now programming in OOP! To facilitate such modular approach stick as far as possible to the traditional use of registers, and save and restore all registers used by the module.

Split up your functions if too largetop

The modular approach to programming will also assist in the readability of your code since the name of such small functions assist you to see what larger sections of code are doing. If your functions are getting too large to be readily understandable, split them up into smaller functions to call, giving each function a name describing what it does. I would recommend this even if the function is only called once. There is very small speed overhead in making a call. As a general rule of thumb if any of your jumps within a function (other than jumps to exit the function) cannot be coded using the short form (in the range +127/-128 bytes) then your function should be split up into smaller sections.

Return values and flags from functionstop

Traditionally EAX is used to return a value from a function and the Windows APIs also do this. In assembler it would also be usual if a function's job is to find a memory address, for this to be returned in ESI, EDI or EBX.
Functions often need to return with the flags set to indicate results. Traditionally the return would be as follows:-
Return c (carry) to show an error occurred.
Return nc (not carry) to show success.
Return z or nz (zero or not zero) to show the result of an action.

Flags can also be used to instruct the caller whether or not to take further action. Here is an example from the function GENERAL_WNDPROC in the HelloWorld2 sample program:-

CALL [EDX+ECX*8+4]      ;call the correct procedure for the message
JNC >L4                 ;nc=don't call DefWindowProc
PUSH [ESP+18h],[ESP+18h],[ESP+18h],[ESP+18h]
CALL DefWindowProcA
L4:

Provide good comments and descriptionstop

Remember that you may wish to refer back to your code years after you have written it. At the top of the source script explain what the program does and how it works, how it should be assembled and linked. Describe what each function is for and how it works if this is not obvious. Add comments to any line in the source script which does not do something obvious. Add comments and descriptions to data declarations and structure templates.

Best order of source scripttop

Although GoAsm is a one-pass assembler, it does not require that data declarations should be in any particular place in the source script. Generally, however, it would be usual to have data declarations before the code which addresses that data. GoAsm does require that definitions and structure templates should be declared before they are used, otherwise GoAsm will be unaware what they are at the time of their use.
Data is best aligned on a boundary to match the size of data operations. This can be achieved using the ALIGN directive. However good alignment will be automatically obtained if at the following order is followed in data declarations, starting with the opening of the data section:-
Qwords, dwords, words, bytes, strings. Twords are best aligned on an 8-byte boundary, but an odd number of twords are declared at the beginning of the data section this will upset the alignment for the rest of data, since they are 10 bytes each.
A declaration of uninitialised data will not affect alignment since no bytes are in fact taken up, being reserved only for the .bss section.

Best direction of conditional jumpstop

On the Pentium III and upwards, the processor will decide whether to cache the destination of a conditional jump in your code depending on its direction. The rule used by the processor is that destinations of forwards jumps are not cached, whilst the destination of backward jumps are cached. So your code will run faster if you follow the following rules:-
  • When error checking, eg. testing the value in eax after a call to an API, always jump forwards to exit in the event of a failure.
  • In loops, always loop backwards to the beginning of the loop.
  • In loops, when testing to end the loop always jump forwards to exit the loop.

Lower or upper case?top

Largely this is a matter of personal preference but when programming in Windows I have found the following to make the source script as readable as possible:-
  • Mnemonics and registers always in upper case or lower case (must be consistent throughout the file).
  • Code labels always in upper case only. This distinguishes these labels when called from a Windows API which will be in mixed case. For example if you follow this rule, you will know CALL COMPARESTRING is a call to one of your own procedures and that CALL CompareStringA is a call to a Windows API.
  • Data labels and pointer names which are described in the Windows SDK to be used and their case retained, while other labels to be in upper case only. Again this helps readability of the source script because the Windows data labels and pointer names are well known to all Windows programmers. So you know that hwnd, hAccel or szWindowName have some special meaning to Windows and will be described in the SDK but MBTITLE and MBMESSAGE are specific to your program.

Save your work regularlytop

This is not only prudent in case of disk failure, but there is also another reason. In programming sometimes you have to make a decision radically to change the way part of your program works. You may make major changes to your code. However, at the end of that process you may decide to revert back to the original coding. So keep a copy of all coding until you are sure you are happy with your radical change! It is a good idea to have several diskettes holding copies of your source script going back different periods of time.

Setting the flagstop

The state of the flags normally reflect the result of a particular instruction but it is often necessary to set them manually. Apart from CLC, CMC and STC to set or reset the carry flag you can use the following instructions which do not change the registers concerned and which are two opcodes each:-
CMP EAX,EAX            ;set the zero flag (no change to eax)
CMP EAX,EDX            ;when they must be different reset the zero flag
OR EAX,EAX             ;when eax cannot be zero reset the zero flag
TEST EAX,EAX           ;same effect as OR EAX,EAX

Checking for zerotop

Here are some ways of checking for zero:-
JECXZ >L1              ;two opcodes
OR ECX,ECX             ;two opcodes
JZ >L1                 ;and two more opcodes
;
TEST ECX,ECX           ;two opcodes
CMP ECX,0              ;three opcodes

Setting to a numbertop

Here are some ways of setting a register to a number:-
XOR EAX,EAX            ;set eax=0 with two opcodes
SUB EAX,EAX            ;set eax=0 with two opcodes
AND EAX,0              ;set eax=0 with three opcodes
MOV EAX,0              ;set eax=0 with five opcodes
;
XOR EAX,EAX
INC EAX                ;set eax=1 with total of three opcodes
MOV EAX,1              ;set eax=1 with five opcodes
;
OR EAX,-1              ;set eax=-1 with three opcodes
XOR EAX,EAX
DEC EAX                ;set eax=-1 with total of three opcodes
MOV EAX,-1             ;set eax=-1 with five opcodes
;
XOR EAX,EAX
MOV AL,66h             ;set eax=66h with four opcodes
MOV EAX,66h            ;set eax=66h with five opcodes
and when using memory:-
XOR EAX,EAX            ;
MOV [ESI],EAX          ;set memory at esi to zero using four opcodes
MOV D[ESI],0           ;set memory at esi to zero using six opcodes
;
XOR EAX,EAX
MOV [HELLO],EAX        ;set HELLO to zero using seven opcodes
MOV D[HELLO],0         ;set HELLO to zero using ten opcodes

Using INC and DEC instead of ADD and SUBtop

INC ESI,ESI            ;increment esi twice with two opcodes
ADD ESI,2              ;increment esi twice with three opcodes
;
DEC ESI,ESI            ;decrement esi twice with two opcodes
SUB ESI,2              ;decrement esi twice with three opcodes

Using the 16-bit registerstop

In most cases, using a 16-bit register instead of an 8-bit or 32-bit register will add one extra opcode to your code. This is because the assembler needs to generate the size override byte (66h) before the instruction.

Multiplying using LEAtop

Here are some examples of how to do quick arithmetic using LEA:-
LEA EAX,[EAX+EAX*2]    ;multiply eax by 3 using 3 opcodes, 1 clock
LEA EAX,[EAX+EAX*4]    ;multiply eax by 5 using 3 opcodes, 1 clock
LEA EAX,[EAX+EAX*8]    ;multiply eax by 9 using 3 opcodes, 1 clock
;
LEA EAX,[EAX+EAX*4]    ;multiply eax by 5
LEA EAX,[EAX*2]        ;final result is multiply by 10 (total 6 opcodes)
;
LEA EAX,[EAX+EAX*4]    ;multiply eax by 5
SHL EAX,1              ;final result is multiply by 10 (total 5 opcodes)
;
LEA EDX,[EAX*2]        ;get twice eax in edx
LEA EAX,[EAX+EAX*8]    ;multiply eax by 9
LEA EAX,[EAX*8]        ;now result in eax is eax*72
SUB EAX,EDX            ;now result in eax is eax*70

Using a register more than oncetop

To save using another register it is legitimate to load into a register the data pointed to by itself, for example:-
MOV EAX,[EAX]

Comment out linestop

When amending your code remove a line from assembly by commenting it out in case you need to restore it later:-
MOV EBX,ADDR WORTHYNESS
;MOV EDX,[EBX+14h]     ;line commented out
MOV EDX,[EBX+10h]      ;line replacing line commented out to check result

Check before using MMX, SSE or SSE2top

Before using the MMX, SSE or SSE2 mnemonics always check that the processor accepts them using the CPUID mnemonic. If not, provide alternative code using ordinary registers.

During development assemble and link with debug symbolstop

To facilitate debugging during development switch the linker to produce a debug output. See your linker's manual how to do this. You can use my linker GoLink to do this and you can then use my debugger GoBug. GoAsm automatically passes all symbols to the linker for inclusion in the debug output. Then when your program is finished you can produce a final version of the executable without debug output.

Finding errors in your programstop

Have a look at the help file in my program GoBug - What when and how, Other techniques to try.


Copyright © Jeremy Gordon 2002-2003
Back to top