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.
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 stack top
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 alphabetically top
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 coding top
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 incrementally top
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 functions top
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 large top
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 functions top
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 descriptions top
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 script top
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 jumps top
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 regularly top
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 flags top
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 zero top
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 number top
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 SUB top
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 registers top
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 LEA top
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 once top
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 lines top
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 SSE2 top
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 symbols top
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 programs top
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
|