The "Go" tools
The GoAsm manual
some programming ....
by Jeremy Gordon -
This file is intended for those interested in 32 bit assembler programming, in particular for Windows.
Traditional use of registersSome 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 of Windows: registers and stacktopYou 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 alphabeticallytopIf 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,EBPIf you prefer, in GoAsm you can use the USES statement which will preserve and restore the registers automatically for you.
Protective codingtopIt 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.
Code incrementallytopWhen 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 functionstopTake 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 largetopThe 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 functionstopTraditionally 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 descriptionstopRemember 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 scripttopAlthough 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 jumpstopOn 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:-
Lower or upper case?topLargely 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:-
Save your work regularlytopThis 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 flagstopThe 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 zerotopHere 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 numbertopHere 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 opcodesand 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 registerstopIn 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 LEAtopHere 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 oncetopTo save using another register it is legitimate to load into a register the data pointed to by itself, for example:-
Comment out linestopWhen 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 SSE2topBefore 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 symbolstopTo 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 programstopHave a look at the help file in my program GoBug - What when and how, Other techniques to try.
Copyright © Jeremy Gordon 2002-2003