| Version 0.56.4e |
by Jeremy Gordon -
GoAsm Assembler and Tools forum (in the MASM forum) GoAsm Assembler and Tools forum (in the Windows Programming forum) Old forum messages |
| Go to Alphabetical Index |
Some assemblers, like NASM, do no type checking at all. Others, like
A386, do only basic type checking based on the byte, word, dword, qword and
tword types. MASM and TASM, like "C", allow you to specify your own types
using TYPEDEF and then type-check based on those specified types.
Parameter checking checks that the correct number of parameters are
passed to an API and also type checks the parameters. Most assemblers do
not parameter check but MASM permits
parameter checking when the INVOKE pseudo mnemonic is used.
The overheads required to achieve full type and parameter checking
like a "C" compiler are enormous. Just
look through a Windows header file and see the long lists of various types
allocated to various structures and to the
parameters of APIs. Then look at the efforts of the programmer which
are required in the source script to ensure that no error is thrown up
by the assembler or compiler.
I decided to follow the NASM example and not even offer basic type checking as A386 provides. I have used A386 over many years and have enjoyed its clean syntax, but I have only found its basic type checking a hindrance when programming for Windows. This is because there are often occasions when you want to write to data, or read from data using a different size of data than used to declare the data in the first place.
As for parameter checking, again I have not even tried to offer this since in my view it unnecessarily complicates things. It again requires enormous lists of APIs and parameters to be provided to the assembler or compiler so that it can check that these match what you are giving the API. Miss one and your program does not compile. Take the example of
PUSH 40h,EDX,EAX,[hwnd] CALL MessageBoxAHere is a call to an API which takes 4 parameters. Now it is said that you would like the assembler to tell you if you send the wrong number of parameters. But you don't need this warning. Your program would simply crash if you sent the wrong number of parameters, and you are going to test this call aren't you? Yes! There is no hidden, latent, fault here which will not be noticed at testing stage. Then, it is said that you need type checking in case you send the wrong data size to an API as a parameter. I just can't see this. All parameters to APIs are dwords (with one or two exceptions out of thousands). So you won't be sending the wrong size of data to an API.
Abolishing parameter and type checking not only frees the assembler from a great deal of work, making it faster in operation, but it also frees the programmer from the headache of manipulating header and include files. It provides greater fluidity in memory addressing, since errors will not be thrown up if you want to use data of a size which does not match the size of the data declaration. So in GoAsm even if lParam has been declared as a dword,
MOV [lParam],ALis still allowed. And if LOGFONT is a simple structure of dwords, GoAsm is quite happy for example with
MOV B[LOGFONT+14h],1which you might want to use to set a font to italic.
By not type and parameter checking I have been able to abolish
EXTRN. GoAsm does not need to know the type of symbols which are declared
outside the source file (ie. to be found during linking). I hope
you will agree this relieves you from a lot of hard work and anguish in
having to add those EXTRNs in your larger programs.
The corollary to the abolition of type and parameter checking is that
you must tell GoAsm the size of the
data to be worked on, if this is not obvious.
So, for example,
MOV [MemThing],23h is an error. To load 23h as a byte into MemThing you
need to code MOV B[MemThing],23h. This is because GoAsm will not know at
assembly time whether the 23h should be loaded as a byte, word, or dword,
all of which are permitted by the MOV instruction.
In some ways the requirement for a type indicator (when the type is not obvious) is helpful. This is because you can see from the instruction itself how much memory is affected by the instruction. You don't have to look up a particular data declaration to see its type in order to see what the instruction will do. So, for example:-
MOV B[MemByte],23h ;comforting to see this is limited to a byte operation FLD Q[NUMBER] ;useful to know real number loaded with double precision INC B[COUNT] ;essential to know this can count only up to 256Another advantage arising from no parameter checking is that there is no need to decorate the names of imports, and in turn there is no need for LIB files at the linking stage when using the companion program GoLink.
MOV EAX,lParam MOV EAX,[lParam]However, A386 differentiated between labels with and without colons so that the above was only true if lParam was declared as follows
lParam DD 0but not if it was declared as:-
lParam: DD 0In that case MOV EAX,lParam in A386 would act the same was as MOV EAX,OFFSET lParam. Very confusing!
MOV EAX,lParamIn NASM this is the same as MOV EAX,OFFSET lParam for other assemblers.
MOV EAX,[lParam]or
MOV EAX,OFFSET lParamI tend to agree with this approach. The main aim here is to ensure that coding is unambiguous.
MOV EBX,wParamis completely outlawed, unless wParam is a defined word. In order to get the offset in GoAsm you must use
MOV EBX,ADDR wParam or if you prefer MOV EBX,OFFSET wParam which means the same thingIn order to address memory in GoAsm you must use
MOV EBX,[wParam]
CMPS - use CMPSB or CMPSD INS - use INSB or INSD LODS - use LODSB or LODSD MOVS - use MOVSB or MOVSD OUTS - use OUTSB or OUTSD SCAS - use SCASB or SCASD STOS - use STOSB or STOSD XLAT - use XLATB
The asm file is a file which you make and edit using an ordinary text editor,
such as Paws which you can download from my web site,
www.GoDevTool.com, or a program
like Notepad or Wordpad which comes with Windows. If you use Notepad
or Wordpad you should make sure you save the file in a format which
adds no control or formatting characters, other than the usual end of
line characters (carriage return and line-feed). This is because GoAsm
only looks for plain text. You can achieve this by
saving the file as a "text" document. If you don't use an extension
for the file (the extension is the characters after the "dot") then
the editor may give the file a ".txt" extension
but you can change this by renaming the file (you can rename the file
by right-clicking on the name using Windows Explorer or My Computer).
It may be that you cannot see the extension on your computer, because
it may be set that way. To see the extensions of your files
from Windows Explorer, choose the menu item "View", "Folder options",
then the "View" tab and ensure that the "Hide file extensions for known
file types" is not checked. The procedure may differ slightly in different
versions of Windows.
It is traditional amongst programmers to give their source scripts
an extension which matches the language in which the source code
is written. For example you might have an assembler file called
"myprog.asm". Similarly you will usually find source code written
in the "C" language with the extension ".c" or ".cpp" (for "C++"),
".pas" for pascal and so on. However, there is no magic in these
extensions. GoAsm will accept files of any extension or
files which do not have an extension.
The .asm file contains your instructions
to the processor in words and numbers. These are executed by the processor
when the program is run. It is said therefore, that the .asm file
contains your "source code" or your "source script".
As an example let's look at the code and data in a simple 32-bit Windows program which writes "Hello World" to the MS-DOS (command prompt) window (the "console"). This is what you would put into your asm file:-
DATA SECTION ; KEEP DD 0 ;temporary place to keep things ; CODE SECTION ; START: PUSH -11 ;STD_OUTPUT_HANDLE CALL GetStdHandle ;get, in eax, handle to active screen buffer PUSH 0,ADDR KEEP ;KEEP receives output from API PUSH 24,'Hello World (from GoAsm)' ;24=length of string PUSH EAX ;handle to active screen buffer CALL WriteFile XOR EAX,EAX ;return eax=0 as preferred by Windows RETNote that anything after a semi-colon is ignored, so you can insert comments. See operatives for other comment forms. See provide good comments and descriptions for the importance of comments.
Having put in the code and data to your file you are ready to make your program. This is done in two steps. First you need to assemble your file and then you need to link it. In order to do this you need to open an MS-DOS (command prompt) window. See how to do this. In this case you use the command line:-
GoAsm /fo HelloWorld.obj filenamewhere filename is the name of your asm file. See starting GoAsm for how to use the command line for GoAsm.
GoLink /console helloworld.obj kernel32.dll(add "-debug coff" if you want to watch the program in the debugger).
Note that the GetStdHandle and WriteFile calls are to kernel32.dll which is why the name of that Dll appears in the GoLink command line. See for more information about Dlls. See using GoAsm with various linkers for more information about using GoLink and other linkers if you prefer. See the GoLink help file for other GoLink options.
GoLink creates the file HelloWorld.exe. You can then run this program from the MS-DOS (command prompt) window. Type in HelloWorld and press enter. You will see the string you sent to WriteFile is written in the console.
So let's recap by looking back at the lines in your source script.
See that first you asked Windows for a handle to the console window. This
was returned by the API GetStdHandle and held in the EAX register. This
handle and the string to write were
passed to WriteFile. In other words you told Windows to write the specified
string to the console. Information about exactly how to use the APIs and
the parameters which need to be passed to them is available from Microsoft
from the MSDN site (look for the
"Platform SDK").
Finally see suggestions how to
organise your programming work.
The command line syntax is:-
GoAsm [command line switches] filename[.ext]
Where,
filename is the name of the source file
Command-line Switches
/b beep on error
/c always put output file in current directory
/d define a word (eg. /d WINVER=0x400)
/e=empty output file allowed
/fo=specify output path/file eg. /fo asm\myprog.obj
/gl retain leading underscore in external "C" library calls
/h or /? help
/l=create listing output file
/ms decorate for mslinker
/ne no error messages
/ni no information messages
/nw no warning messages
/no no output messages at all
/x64=assemble for AMD64 or EM64T
/x86=assemble 64-bit source in 32-bit compatibility mode
If no extension is given for the inputfile, GoAsm looks for the
file without any extension. If that file is not found than GoAsm looks
for the file with an assumed .asm extension.
If no path is given for the input file it is assumed to be in the
current directory.
If no filename is given for the output file an object file with the same
name as the inputfile is created. For example MyAsm.asm will create a
file called MyAsm.obj.
The directory which receives the output file is as follows:-
CODE SECTION
DATA SECTION
CONST SECTION or CONSTANT SECTION
The words "code", "data", "const" and "constant" are reserved to section declarations and an error will be signalled if these words are used elsewhere in your source.
GoAsm also allows shortened forms to declare a section as follows:-
CODE
DATA
CONST
You can also use .CODE, .DATA and .CONST if you wish.
GoAsm automatically adds the attributes to suit the processor and
Windows. A code section is given the attributes read, execute, code.
A data section is given the attributes read, write, initialised data.
A const section is given the attributes read, initialised data (you won't
be able to write to a const section). Uninitialised data has the attributes
read, write, uninitialised data.
Except to add the shared attribute, you
can't override these attributes yourself. This is because to do
so is pointless in the Windows system which has control over the attributes
of the section as loaded and running. For example even if you give a code
section the write attribute, Windows will not allow you to write to it.
Also Windows will not permit you to execute code in a data section. You can
change this behaviour however, by calling the API VirtualProtect at run-time.
In GoAsm you can use a code section to hold read-only data, although
there may be a reduction in performance if you do this.
Declaring a section also sets certain switches in GoAsm which affect syntax and coding. The rules are as follows:-
See also sections - some advanced use on naming sections, shared sections, and section ordering.
HELLO1 DB 0 ;one byte with label "HELLO1" set to zero
DB 0 ;second byte set to zero
HELLO2 DW 34h ;two bytes (a word) set to 34h
HELLO3 DD 12345678h ;four bytes (a dword) set to 12345678h
HELLO4 DD 12345678D ;four bytes (a dword) set to 12345678 decimal
HELLO5 DD 1.1 ;four bytes (a dword) set to real number 1.1
HELLO6 DQ 0.0 ;8 bytes (a qword) set to real number 0.0
HELLO7 DQ 123456789ABCDEFh ;8 bytes (a qword) set to 123456789ABCDEFh
HELLO8 DQ 1234567890123456 ;8 bytes (a qword) set to 1234567890123456 decimal
HELLO9 DT 1.1E0 ;10 bytes (a tword) set to real number 1.1
HELLOA DT 123456789ABCDEFh ;10 bytes (a tword) set to 123456789ABCDEFh
Note that DB, DW, DD and DQ accept numbers in both decimal and hex; DD, DQ
and DT accept real numbers too.
Label DB 0,0,0,0 ;four bytes set to zero
DW 33h,44h,55h,66h ;four initialised words
DD 33h,44h,55h,66h ;four initialised dwords
DD 1.1,2.2 ;two DD real numbers
DQ 1.1,2.2 ;two DQ real numbers
DQ 3333h,4444h ;two DQ hex numbers
DT 1.1,2.2 ;two DT real numbers
DT 5555h,6666h ;two DT hex numbers
HELLO1 DB ? ;one byte with label "HELLO1" recorded as uninitialised HELLO2 DW ? ;two bytes (a word) HELLO3 DD ? ;four bytes (a dword) HELLO4 DQ ? ;8 bytes (a qword) HELLO5 DT ? ;10 bytes (a tword)Orphaned uninitialised data is not allowed: you cannot mix initialised and uninitialised data so this is an error:-
DATA6 DD 5 DUP 0
DB ?
DB 0
However this is ok:-
DATA6 DD 5 DUP ? ;5 dwords for the customer
DB ? ;a byte to hold the main course
DB ? ;and a byte to hold the sauces
This is to allow you to separate areas of uninitialised data so that
each separate area can have its own comment
HELLO1: DB ? ;one byte with label "HELLO1" recorded as unitialised HELLO2: DW ? ;two bytes (a word)
HELLO1 DB 2 DUP 0 ;two bytes with label "HELLO1" both set to zero HELLO1A DB 800h DUP ? ;2K buffer not initialised HELLO2 DW 2 DUP 0 ;four bytes all set to zero HELLO3 DD 2 DUP ? ;eight bytes in uninitialised section HELLO4 DD 2 DUP 1.1 ;real number 1.1 repeated twice as dwords HELLO5 DQ 2 DUP 1.1 ;real number 1.1 repeated twice as qwords HELLO6 DQ 2 DUP 333h ;qword repeated twice HELLO7 DT 2 DUP 1.1 ;real number 1.1 repeated twice as twords HELLO8 DT 2 DUP 444h ;tword repeated twiceYou can use DUP to declare some data and then initialise each data component individually for example:-
HELLO300 DB 3 DUP <23,24,25> ;declare three bytes and set them to 23,24,25which does the same as:-
HELLO300 DB 23,24,25 ;declare three bytes and set them to 23,24,25Although it may seem pointless to do this, the syntax does make it easier to initialise a member of a structure if it contains DUP. See initialising structure members which have DUP data declarations.
Letters DB 'a'
DW 'xy'
Sample DD 'form'
ZooDay DQ 'Saturday'
Unless inserting Unicode strings GoAsm carries out no conversion to
the character, so that the actual value inserted in the object file will depend on the
current character set at the time of assembly.
MOV EDI ADDR BUFFER MOV EAX,[Sample] STOSDwhich inserts into the buffer the string: form.
DW 'a' ;first byte is a, second is zero
DD 'ab' ;'a' then 'b' then two zero bytes
Repeat character value initialisations are allowed, for example:-
DD 3 DUP "Hi"This inserts H then i then two zeroes and this is done three times.
String1 DB 'This is a string'
DB 'This is a string with "internal" quotes'
String2 DB "A string in double quotes"
DB "I enjoyed the string's contents"
String3 DB '"A string itself in double quotes"'
DB "'A string itself in single quotes'"
DB "'A string's own single quotes'"
String1 DB 'This is a string with null terminator',0
DB 'First string',0,'And another string',0
String2 DB 22h,"A string's own double quotes",22h
The ASCII values you can use here if you wish are 22h for double quotes
and 27h for single quotes.
LongString1 DB 'His first program looked like it would be a great success '
DB 'until he ran it for the first time',0
LongString2 DB 'His fundamental error:',0Dh,0Ah
DB 'he did not test it as he went along',0
The ASCII values 0Dh and 0Ah are carriage return and line feed
respectively, used to start a new line when the string is drawn on
the screen.
DB L'Hello how are you?'
DUS 'I am a Unicode string with new line and null terminator',0Dh,0Ah,0
See also overriding using the STRINGS directive.
The syntax for a DATABLOCK is as follows:-
MyBlockData DATABLOCK_BEGIN ;comment
.
. data is inserted here
.
DATABLOCK_END
Here all the material between DATABLOCK_BEGIN and DATABLOCK_END is inserted in the output
file, and you can then address the data using the label MyBlockData.
MS1 DB 'First string to use',0 MS2 DB 'Second string to use',0 Strings DD MS1,MS2 ;Strings to hold address of the stringsthen to get ready to use the string MS2 instead of coding
MOV ESI,ADDR MS2you can code
MOV ESI,[Strings+4]Whole tables can be created using this method and addressed by taking advantage of the * index register multiplier (scaling) for example
MOV ESI,[Strings+EAX*4]Here eax, which is zero indexed, holds which string to use. When eax is zero the first string will be used, when eax is one the second string and so on if there are more strings.
PROCEDURE_TO_CALL DD FIRSTPROC,SECONDPROC MOV ESI,ADDR PROCEDURE_TO_CALL ;get procedures in esi MOV ESI,[ESI+EAX*4] ;get correct procedure CALL [ESI] ;call the procedure
START:This can be upper or lower case or a mixture.
If you don't want to use START, you can specify the starting address using one of these methods in GoLink's command line or command file:-
-entry STARTINGADDRESS /entry STARTINGADDRESSIf you are using ALINK only the first method works.
If you are using the MS linker you need to make a slight change to your label. It must be preceded by an underline character. So your label is _START: in your source script. Then you would use one of these instructions to the linker (without the underline character):-
-ENTRY START /ENTRY STARTWhat is happening here is that the MS linker is designed to work with a "C" compiler which will decorate global labels with the underline character. So the linker looks for the label _START, rather than START. Assembler programmers have had to put up with such quirks in Windows tools for many years but now we have our independence!
NAMEOFLABEL:This does not output any code, but sets a bookmark called NAMEOFLABEL at the point in data or code where it appears. If you are in a data section, the colon is not obligatory, nor is it obligatory if the label gives the name of an automated stack frame. Therefore the following lines all create unique labels:-
(in data section) HELLO DB 0 ;label HELLO BYE: DB 0 ;label BYE MEAGAIN ;label MEAGAIN (in code section) RICE: ;label RICE PEAS: FRAME ;label PEAS BEANS FRAME ;label BEANSYou can see from this that a single word which is not known to GoAsm to be a directive, mnemonic, data declaration, initialisation of data, or a defined word will be regarded as label. GoAsm expects a colon after a code section label. This is because there are numerous words which must be used in a code section and if they are misspelt, it is important that an error is declared rather than the word being misconstrued as a label.
The scope of a label defines from where it can be accessed using it own unmodified name. Lets look at these two types of re-usable labels in turn.
.looptop ;label looptop .fin ;label finThe boundary of the scope of these labels is defined by the unique code labels in the source script. In other words the label can be jumped to provided there is no unique label in the way. So for example:-
JZ >.fin CALCULATE: .fin REThere the jump instruction will not find .fin because the label CALCULATE is a unique code label in the way.
If you want to jump past a unique code label to a locally scoped re-usable
label, you can either use another unique code label as the destination of the
jump, or you can use an unscoped re-usable label.
Or for advanced use, you can use the locally scoped label within
an automated stack frame see re-usable label scope in automated
stack frames.
Locally scoped re-usable labels are sent to the debugger as symbols
together with their "owner". Therefore the symbol sent to the debugger
in the above example is CALCULATE.fin.
L1: 24: 24.6:You can even use a single stand-alone colon. You might use this for those extremely insignicant jump destinations in your code.
JZ >.fin ;jump forward to .fin JMP >.exit ;jump forward to .exit LOOP .looptop ;loop backwards to .looptop LOOP <.looptop ;loop backwards to .looptop (alternative form)Here is an example using unscoped labels:-
JZ >L10 ;jump forward to L10 JNC L3 ;jump backwards to L3 JNC <L3 ;jump backwards to L3 (alternative form) JMP 100 ;jump backwards to 100
JZ EXTERNALLABELyou should code
JNZ > JMP EXTERNALLABEL :This is to help with error checking. GoAsm assumes a conditional jump was meant to be to a place inside the existing source script.
JMP LABEL ;look for label in all source scripts JMP <INTERNALLABEL1 ;only look for label earlier in source script JMP >INTERNALLABEL2 ;only look for label later in source script
: CALL PROCESS LOOPZ <or
CMP EAX,EDX JZ > CALL PROCESS : RET
JZ >>.fin ;long forward jump to .fin JZ LONG >.fin ;long forward jump to .fin (alternative form) JC <<A1 ;long backward jump to A1 JC LONG A1 ;long backward jump to A1 (alternative form) JC LONG <A1 ;long backward jump to A1 (alternative form)Note that there is no long form of LOOP and its variations, nor of JECXZ. If you need a long jump for these instructions use this instead:-
DEC ECX JNZ LONG L2 ;long jump replacing LOOP OR ECX,ECX ;test for ecx=0 JZ LONG >L44 ;long jump replacing JECXZ
Here are examples using unique labels:-
MOV ESI,ADDR Process_dabs ;get in esi the address of the code label Process_dabs MOV ESI,ADDR Hello2 ;get in esi the address of the string labelled Hello2 MOV ESI,ADDR HelloX+10h ;get in esi the address 16 bytes beyond HelloXHere is an example using a locally scoped re-usable label:-
MOV ESI,ADDR CALCULATE.fin ;get in esi the address of the code label .fin in the CALCULATE procedureHere is an example using a formal structure:-
MOV ESI,ADDR Lv1.pszText ;get in esi the address of the psztext member in the formal structure Lv1
MOV ESI,ADDR Hello1 ;get in esi the address of the dword Hello1 MOV EAX,[ESI] ;get in eax the value of Hello1or this which does the same thing:-
MOV EAX,[Hello1] ;get in eax the value of Hello1
MOV ESI,ADDR Hello1 ;get in esi the address of the dword Hello1 MOV [ESI],EAX ;write the value in eax to Hello1or this which does the same thing:-
MOV [Hello1],EAX ;write the value in eax to Hello1
PARAM_DATA DD 0 ;+0h
DD 0 ;+4h
DD 55h ;+8h
DD 0 ;+0Ch
DD 0 ;10h
Then you can use the label to read from and write to a particular part of
the structure using a displacement value as follows:-
MOV ESI,ADDR PARAM_DATA MOV EAX,[ESI+8h] ;get in eax value of third dword MOV [ESI+8h],EDX ;and insert edx insteador this which does the same thing:-
MOV EAX,[PARAM_DATA+8h] ;get in eax value of third dword MOV [PARAM_DATA+8h],EDX ;and insert edx insteadThe displacement value can be any value up to 0FFFFFFFFh. It can be positive or negative. Non-numeric elements must be separated by the plus sign.
PARAM_DATA DD 10h DUP 0Then you could use indexation (scaling) to multiply the index register to suit:-
MOV ESI,ADDR PARAM_DATA MOV EAX,[ESI+ECX*4] ;get in eax value of ecx dword MOV [ESI+ECX*4],EDX ;and insert edx insteador this which does the same thing:-
MOV EAX,[PARAM_DATA+ECX*4] ;get in eax value of ecx dword MOV [PARAM_DATA+ECX*4],EDX ;and insert edx insteadYou can use indexation of 0,2,4 or 8. The following instructions are all valid:-
MOV EAX,[BPARAM_DATA+ECX] ;get in eax value of ecx byte MOV EAX,[WPARAM_DATA+ECX*2] ;get in eax value of ecx word MOV [QPARAM_DATA+ECX*8],EDX ;insert edx at ecx qwordNon-numeric elements must be separated by the plus sign.
PARAM_DATA DD 19h,0,0,22222h
DD 1Ah,0,0,44444h
DD 1Bh,0,0,66666h
DD 1Ch,0,0,88888h
DD 1Dh,0,0,0AAAAAh
DD 1Eh,0,0,0CCCCCh
Then you could use indexation (scaling) and displacement as follows:-
MOV ESI,ADDR PARAM_DATA CMP EAX,[ESI+ECX*4] ;see if there is eax value at ecx dword JNZ >L2 ;no MOV EDX,[ESI+ECX*4+0Ch] ;yes so get the result in edxor this which does the same thing:-
CMP EAX,[PARAM_DATA+ECX*4] ;see if there is eax value at ecx dword JNZ >L2 ;no MOV EDX,[PARAM_DATA+ECX*4+0Ch] ;yes so get the result in edxYou can use indexation of 0,2,4 or 8. The displacement value can be any value up to 0FFFFFFFFh. In your source script it can be positive or negative. Non-numeric elements must be separated by the plus sign.
PROCESS_HASH: ;label to the procedure XOR EAX,EAX MOV EDX,ESI CALL PH23 MOV EDX,866h ;return from the procedure with edx=866h RET
PROCESS_HASH: XOR EAX,EAX MOV EDX,ESI CALL PH23 ;transfer execution to the PH23 procedure and return MOV EDX,866h ;return from the procedure with edx=866h JMP >SOMEWHERE_ELSE ; START: ;start place for execution JMP PROCESS_HASH ;
CALL PROCESS_HASH JMP PROCESS_HASHSometimes the address of the procedure to go to is held in memory pointed to by a label or a register or even held at a known place in memory in which case you can use for example:-
CALL [PROCADDRESS] CALL [PROCTABLE+20h] CALL [ESI] CALL [ESI+EDX] JMP [4000000h]Sometimes the address of the procedure to go to is held in a register in which case you can use for example:-
CALL EAX JMP EDI
#define Hello PROCESS_HASH CALL Hello ;treated as a call to PROCESS_HASH CALL 100h ;treated as a call to a relative address CALL [HELLO3+ECX+EDX*4] CALL [HELLO3+ECX+EDX*4+9000h] CALL $$ ;a call to the start of the current section CALL $+20h ;a call 20h bytes ahead
So if you want to call a procedure in another source script (which will be producing another object file) just call it in the usual way. Similarly if you have a procedure in another executable (usually a Dll) you can do the same.
For example, suppose you have written My.Dll containing a calculation algorithm you wish to use with the label CALCULATE. You could call it as follows:-
CALL CALCULATE
In your list of Dlls you give to GoLink you will specify My.Dll. GoLink
will first look for the code label CALCULATE in the object files, but will then
look in the specified Dlls. Most other linkers look in library files (.lib files)
for the functions they contain, which means you have to make a lib file.
Either way, in GoAsm syntax there is nothing further for you to do in
your source script. If the linker does not find the destination of the call,
an error will be shown.
This form of the call is a relative call using the opcode E8.
You could also use this form:-
CALL [CALCULATE]For this type of call GoAsm uses the opcodes FF15. This is a call to an absolute address. In 32-bit assembly this is a call to a 32-bit address, but in 64-bit assembly its a call to a 64-bit address.
See also:-
using static code libraries
direct importing by ordinal or specific Dll
using the C Run-time library
Calling Windows APIs (which reside in Windows system Dlls) is very simple where there are no parameters, for example in 32-bit Windows you can use:-
CALL GetModuleHandleor its more advanced alternative which can be used either for 32-bit or 64-bit Windows:-
INVOKE GetModuleHandleThere is nothing else to put in the source script. Since the function being called resides outside the executable you are making, it is the linker's job to find the Dll which contains the GetModuleHandle procedure and it will record the name of the Dll in your executable. GoLink does this from a list of Dlls which you supply.
Most Windows APIs, however, expect to be sent parameters (also known as "arguments") when they are called. It is the programmer's job to ensure that these parameters are sent to the API correctly. The parameters contain the information, or pointers to information, which tell the API what to do. Sometimes the parameters contain addresses of places in memory where the API will insert information.
How you send the parameters depends on whether you are assembling for 32-bit or 64-bits Windows. This is because they each use different calling conventions, and this affects the way parameters are sent and used. 32-bit Windows uses the standard calling convention (STDCALL) and 64-bit Windows uses the so-called fast calling convention (FASTCALL).
GoAsm provides ARG and INVOKE which can be used for both platforms. GoAsm creates the correct code to suit the calling convention to be used. If you are writing only for 32-bits you can use PUSH and CALL to send the parameters, but if you want to port your code to 64-bit Windows later, you will need to change these to ARG and INVOKE. In both 32-bit and 64-bit source code you would use CALL to call procedures in your own executables, unless you are sending parameters to them using one of these calling conventions.
In the STDCALL calling convention used in 32-bit Windows, all the parameters are put on the
stack by the caller, and the stack pointer (ESP) is moved to the top of the parameters on the stack.
Then the API is called. The API uses the parameters on the stack and before returning it restores
the stack to equilibrium by moving the stack pointer to the position it was before the first
parameter was put on the stack.
In the FASTCALL calling convention used in 64-bit Windows, the first
four parameters are put in the RCX,RDX,R8 and R9 registers instead of on the stack. However,
subsequent parameters are put on the stack. The caller needs to ensure that the stack
pointer (in this case RSP) is moved to the top of the parameters as usual, allowing for the
first four parameters which are held in registers (this is to permit the API to keep them on
the stack as if they had been put there in the first place). Another difference is that the
API does not restore the stack into equilibrium before returning from the call (this change makes
it easier for a handful of APIs which do not have a fixed number of parameters).
To enable the same source to be used both for 32-bit and 64-bit programming you would send the parameters using ARG and then call the API using INVOKE, for example:-
ARG 40h,RDX,RAX,[hwnd] INVOKE MessageBoxAIn 32-bit assembly the ARG simply does the same as PUSH, and INVOKE does the same as CALL. GoAsm accepts a PUSH instruction of a 64-bit General Purpose register, so PUSH RDX is treated the same as PUSH EDX. Therefore the above call works on both platforms. In 32-bit assembly it translates as:-
PUSH 40h,EDX,EAX,[hwnd] CALL MessageBoxAHowever in 64-bit assembly, the same code translates as:-
MOV R9,40h MOV R8,RDX MOV RDX,RAX MOV RCX,[hwnd] SUB RSP,20h CALL MessageBoxA ADD RSP,20hSee writing 64-bit programs for more details.
int MessageBox(
HWND hwnd, // handle of owner window
LPCTSTR lpText, // address of text in message box
LPCTSTR lpCaption, // address of title of message box
UINT uType // style of message box
);
Using INVOKE you can follow the same order, for example:-
INVOKE MessageBoxA, [hwnd],EAX,EDX,40hwhich is the same as:-
ARG 40h,RDX,RAX,[hwnd] INVOKE MessageBoxANote that ARG (like PUSH) reads the parameters one way, whereas parameters after INVOKE are read the other way.
INVOKE lets you straddle two or more lines using the continuation character:-
INVOKE CreateWindowExA, WS_EX_OVERLAPPEDWINDOW, ADDR szClassName, \
ADDR szWindowName,\
WS_OVERLAPPEDWINDOW+THING,\
100,16,400,0,0,0,[hInstance],0
Since GoAsm looks at the parameters to INVOKE starting from the end,
errors near the end will be found first.
When using INVOKE, if you like to tuck away your parameters in a defined word then GoAsm will still get them in the correct order, for example:-
z_function_params=3,2,1 INVOKE z_function, z_function_paramsproduces the same code as:-
ARG 1,2,3 INVOKE z_function
MBTITLE DB 'Hello',0 MBMESSAGE DB 'Click OK',0 PUSH 40h, ADDR MBTITLE, ADDR MBMESSAGE, [hwnd] CALL MessageBoxATo make this easier GoAsm permits the use of PUSH or ARG like this:-
PUSH 40h,'Hello','Click OK',[hwnd] CALL MessageBoxAor, if you were writing source for 32-bit or 64-bit platforms:-
ARG 40h,'Hello','Click OK',[hwnd] INVOKE MessageBoxAor if you prefer to send parameters after INVOKE:-
INVOKE MessageBoxA, [hwnd],'Click OK','Hello',40hYou can also use this with Unicode strings as follows:-
ARG 40h,L'Hello',L'Click OK',[hwnd] INVOKE MessageBoxW INVOKE MessageBoxW, [hwnd],L'Click OK',L'Hello',40hWhen you use any of these forms the string will always be null-terminated. What is happening here is that GoAsm places the string in the const section if there is one (or the data section if there is one, if not, in the code section) and adds a null-terminator. Then GoAsm creates the correct instruction and gives it a pointer to the string. No symbol is made for debugging purposes.
In 64-bit assembly, GoAsm ensures that Unicode strings are aligned on a word boundary as required by the system.
PUSH <23,24,25> ;push a pointer to the bytes 23,24,25or
PUSH <23,6 DUP 20h,23> ;push a pointer to the bytes 23,six spaces then 23or
PUSH <'Hi',0Dh,0Ah,'There',0> ;push a pointer to the null terminated string on two linesYou can also use the < and > operators in this way with ARG and after INVOKE. What is happening here is that GoAsm places the data declaration between the < and > operators in the const section if there is one (or the data section if there is one, if not, in the code section). Then GoAsm creates the correct instruction and gives it a pointer to the data. No symbol is made for debugging purposes.
In 64-bit assembly, GoAsm ensures that data is aligned on a word boundary as would be required by the system if the data contains Unicode strings.
MOV EAX,ADDR 'This is a string' MOV EAX,ADDR <'String',0Dh,0Ah>When GoAsm deals with this code it places a null terminated string or the data between the < and > operators in the const section if there is one (or the data section if there is one, if not, in the code section). Then GoAsm gives the pointer to the data so created to the instruction. No symbol is made for debugging purposes.
This works the same way in 64-bit programming except that GoAsm ensures that a Unicode string or data is word aligned in memory as required by the system.
MOV AL,'1' MOV AX,'12' ;regarded as bytes - 1 first then 2 MOV EAX,'ABCD' ;regarded as bytes - A first, then B then C then DThis makes it much easier to add short strings to memory eg. to add the extension .fil to a filename in memory you can code:-
MOV [EDI],'.fil' ;or MOV EAX,'.fil' MOV [EDI],EAXand not
MOV [EDI],'lif.' ;or MOV EAX,'lif.' MOV [EDI],EAXCMP works in the same way for example:-
CMP AL,'1' CMP EAX,'ABCD' CMP [EDI],'.fil'This does not change the usual reverse order of material not in quotes so for example when you want to add a carriage return and then a linefeed to text you can still use:-
MOV AX,0A0Dh STOSWHere the carriage return (0Dh) which is in AL, is loaded into memory first, then the linefeed (0Ah) in AH is loaded into memory.
MOV EAX,'ABC' ;codes as A then B then C then zeroWhen writing source code for Unicode programs you can ensure that character immediates are Unicode or if necessary, switched between ANSI and Unicode see using the correct string in quoted immediates and switching quoted strings and immediates.
In 64-bit programming you can use the 64-bit registers to contain character immediates which are 8 characters long, for example:-
MOV RAX,'Saturday'However, the CMP instruction is limited to 32-bits, so for example
CMP RAX,'Saturday'would show an error.
MOV [ESI],20hThis puts the number 20h into a place in memory whose address is contained in the register esi. But what is missing from this instruction is whether the number should be loaded as a byte, as a word or as a dword. In other words should one, two or four bytes of memory be altered? All assemblers require a type indicator in instructions of this sort. The syntax in other assemblers is (using dword as an example):-
MOV DWORD PTR [ESI],20h ;MASM MOV DWORD [ESI],20h ;NASM MOV D[ESI],20h ;A386Of course I have used the A386 syntax which requires a lot less typing so that in GoAsm the type indicators you can use are:-
B meaning byte W meaning word (two bytes) D meaning dword (four bytes) Q meaning qword (eight bytes) T meaning tword (ten bytes)
INC [COUNT]Here GoAsm does not know (and in fact does not care) whether COUNT is a byte, word or dword. Therefore you must give this a type indicator too for example:-
INC B[COUNT]Although this is a little more work for the programmer, in fact it can be argued that it makes your source script easier to read and understand, since you can always see the size of the operation from the instruction itself, rather than having to go back to see if COUNT was declared as a byte, word or dword.
AND B[MAINFLAG],0FEh ADC W[EAX],66h ADD D[MEM_AREA],66h BT D[EBX],31D CMP D[HELLOWORD],0Dh DEC D[ECX] DIV B[HELLO] INC D[EDX] MOV B[MEM_AREA],23h MOVSX EDX,B[EDI] MUL B[HELLO] NEG W[ESI] NOT D[HELLO3] OR B[MAINFLAG],1h SETZ B[BYTETEST] SHL W[IAMAWORD],23h SHL D[IAMADWORD],CL SUB D[EBP+10h],20D TEST B[ESP+4h],1h XOR D[IMAWORD],11111111hAnd in 64-bit programming you might also see, for example
ADC W[RAX],66h BT D[R12],31D INC Q[RDX] NEG W[R15D]
AND [MAINFLAG],CL CMP [HELLOWORD],EDI MOV [IAMABYTE],AL MOV [IAMADWORD],ESI OR [MAINFLAG],BH XCHG CL,[ESI]Also none of the mmx, xmm or 3DNow! instructions require a type indicator. Several of the x87 floating point instructions do not need a type indicator. Those which do can take more than one operand size. There are also several instructions which can only take one operand size so with these there is no need for a type indicator. For example CALL, JMP, PUSH, and POP always take a dword. See half stack operations for the use of PUSHW and POPW. Also some less common instructions do not need a type indicator, for example ARPL, BOUND, BSF, BSR, CMOV (in all forms), CMPXCHG, and CMPXCHG8B.
PUSH 0,23h,[hwnd],ADDR lParam,EAX POP EAX,[EBP+2Ch],[hwnd] DEC ECX,EDX,[COUNT] INC [EBP+10h],EDI DB 23h,24h,25hThe instructions here are always assembled in left-to-right order.
66ABCDEh ;a hex number 34567789 ;a decimal number 1100011B ;a binary number 1.0 ;a real number 1.0E0 ;a real numberGoAsm accepts these numbers but also supports numbers in these formats:-
9999999D ;a decimal number 0x456789 ;a hex numberA hex number which begins with a letter (that is A to F, being values 10 to 15 decimal) must begin with a zero, for example:-
0A789ABCDh or 0xA789ABCD
Be careful using the OR, AND and NOT logical operators, since these are actually mnemonics. Although GoAsm recognises them if you use them in places where mnemonics are not expected, you can use instead | for OR, & for AND, and ! for NOT.
Arithmetic in brackets is carried out first, otherwise calculations are carried out in strict left-to-right order. Here are some examples:-
DB 2*3 DB (2+30h)/(2+1) DD (2000h+40h-20h)/2 DD SIZEOF HELLO/2 DD 444444h & 226222h DB 20h/2 DUP 44h DB 6+2 DUP 0 #define globule (2*3)/2 DB globule DD globule|100h DD 2D00h>>8 DQ 2D00h<<48 MOV EAX,globule|100h MOV EAX,SIZEOF HELLO*2 MOV EAX,ADDR HELLO+10h MOV EAX,0x68+0x69-0x70 MOV EAX,[MemName+0x68+0x69-0x70] MOV EAX,[ESI*4+45000h] MOV EAX,[ESI*4+SIZEOF HELLO/2] MOV EAX,8+8*2 ;result is 32 MOV EAX,8+(8*2) ;result is 24Divisions are rounded according to the result eg.
MOV EAX,32/3 ;puts 11 into eax MOV EAX,31/3 ;puts 10 into eax MOV EAX,10/4 ;puts 3 into eaxGoAsm assumes that all multiplication and division is carried out using unsigned numbers. MUL and DIV are used at compile-time and not their signed counterparts IMUL and IDIV. See understand signed numbers for more about signed numbers.
DD 1.6789E3 DQ 1.6789E3 DT 1.6789E3 DD 3 DUP 7.6789E-2 DQ 678.27896435E3 DT 1.2You may also declare PI directly either as a tword, qword or dword as follows:
DD PI ;pi as a dword DQ PI ;pi as a qword DT PI ;pi as a twordGoAsm tries to achieve maximum accuracy in providing pi by writing a known number directly into the mantissa.
You can also declare real numbers as follows:-
PUSH 1.1 MOV EAX,1.1Both of these use a 32-bit format for the real number. The first places that number on the stack and the second moves it into the specified register.
DIRECT_PI DT 4000C90FDAA22168C235hand load it using:-
FLD T[DIRECT_PI]The most significant bit (bit 79) in this tword declaration is a sign bit indicating whether the real number is positive or negative. In this case the number is positive because the sign bit is not set. The remainder of the first four hex digits contain the exponent. This is biased by a value of +3FFEh in 80 bit real numbers. This permits exponents of between -3FEEh and +4001h to be handled without using the most significant bit (the exponents become 0 to 7FFFh). The remainder of the hex digits contain the mantissa.
Mess DB 'I am a string of characters',0 PUSH 'This is supposed to be a carat ^' MOV EAX,'£$|@'It must be asked what actual values are loaded by GoAsm when issuing these instructions? At assemble time GoAsm views your source script using Windows file mapping, and then reads it character by character. In other words GoAsm is given the value of the characters in the source script by Windows. When GoAsm loads in the object file strings of the sort shown above, it loads the same value character as given to it by Windows. In the case of conversions from ANSI to Unicode strings, these are passed first through the API MultiByteToWideChar. This means that the value given to GoAsm by Windows will match that in the current character set (code page). Accordingly you need to ensure that the character set used in the computer which runs GoAsm is the character set for which your program is designed to run.
If you are using a source script which is in a Unicode format (UTF-8 or UTF-16) then the codepage issue disappears. The correct characters are given by their Unicode value.
CMP AL,124D ;see if character is an OR as in some character sets JZ >L4 ;yes CMP AL,221D ;see if character is an OR as in some character sets JZ >L4 ;yesHere you have already allowed for a possible variation in the user's own character set. If necessary you can arrange for your code to test the user's character set at run-time, and to test for the correct characters or use the correct strings accordingly. You can also test the language of the user's machine and provide strings in the correct language. The resource APIs provide a way this can be done automatically - see the manual to GoRC, my resource compiler.
, - the instruction is not finished, continue ; or // - a comment line - ignore to end of line /*.........*/ - continuous comment - ignore between the marks \ - the material is continuing on the next line - number - the number is negative ! number - invert the number (like NOT) NOT - invert the number ~ number - same + - the plus sign - - the minus sign * - the multiply sign / - the divide sign | or ¦ - bitwise OR OR - bitwise OR & - bitwise AND AND - bitwise AND << number - bit shift left by the number >> number - bit shift right by the number (....) - perform calculation in brackets first
## in a definition has a special meaning see
using double hashes in definitions.
PUSH ADDR LV_COLUMN,EAX,101Bh,hListView CALL SendMessageA ;insert eax columnNow let's look more closely at the LV_COLUMN structure.
LV_COLUMN DD 6 DUP 0However, in the Windows information, each of the six dwords has a name which gives some idea of what it is used for, which is useful. Also the very first dword is a mask which identifies which of the later members of the structure are valid. This mask is important because a later version of the structure has another two members, and the mask needs to be different. So it might be better to declare the structure in data like this so that the mask can be initialised with a value, and so that you can see the names in your source script:-
LV_COLUMN DD 0Fh ;+0h mask DD 2h ;+4h fmt=LVCFMT_CENTER=2 DD 0 ;+8h cx DD 0 ;+0Ch pszText DD 0 ;+10h cchTextMax DD 0 ;+14h iSubItemHere see that whilst declaring the structure in data we have taken the opportunity to initialise two of the members with values which will not change and have included in the comments the offset details, member names and other information.
MOV EDI,ADDR LV_COLUMN MOV ESI,ADDR ColumnText ;get the column text to use MOV [EDI+0Ch],ESI ;and give it to the structure MOV D[EDI+8h],50D ;and make the width 50 pixelsor you can use:-
MOV ESI,ADDR ColumnText ;get the column text to use MOV [LV_COLUMN+0Ch],ESI ;and give it to the structure MOV D[LV_COLUMN+8h],50D ;and make the width 50 pixels
Here is an example of a structure template made with the name LV_COLUMN:-
LV_COLUMN STRUCT mask DD 0Fh ;mask fmt DD 2h ;LVCFMT_CENTER=2 cx DD 0 pszText DD 0 cchTextMax DD 0 iSubItem DD 0 ENDSI have added some comments here to help understand the initialisation of two members of the structure. Note ENDS (literally END STRUCT) marks the end of the template. If you prefer you can also mark the end of the template by giving the structure name again followed by ENDS eg.
LV_COLUMN ENDSThe second stage is to use the template. You do this by using the template in the data section, usually preceded by a label, for example:-
Lv1 LV_COLUMNHere you have declared six dwords using the LV_COLUMN structure template and you have given the structure declaration the label Lv1.
RECT STRUCT
left DD
top DD
right DD
bottom DD
ENDS
rc RECT
creates the following symbols:-
rc rc.left rc.top rc.right rc.bottom
MOV ESI,ADDR ColumnText ;get the column text to use MOV [Lv1.pszText],ESI ;and give it to the structure MOV D[Lv1.cx],50D ;and make the width 50 pixelsor even
MOV ESI,ADDR ColumnText ;get the column text to use MOV EDX,ADDR Lv1.pszText ;get the psztext member MOV [EDX],ESI ;and load the text to use MOV EDX,ADDR Lv1.cx ;get the cx member MOV D[EDX],50D ;and make the width 50 pixelsBut there is still nothing to stop you from doing this which is the same thing:-
MOV ESI,ADDR ColumnText ;get the column text to use MOV [Lv1+0Ch],ESI ;and give it to the structure MOV D[Lv1+8h],50D ;and make the width 50 pixelsAlthough it is more complex to set up, the advantage of the former method is that when you look at your code in the symbolic debugger the symbols in the structure will appear in full, with both the structure label and the member name appearing which is some advantage. This is because GoAsm creates symbols for all the members of the structure and passes these to the linker. As far as I am aware this is unique to GoAsm and other assemblers do not do this.
POINT STRUCT left DD 0 right DD 0 ENDSThen
MOV EBX,POINT.rightThis loads the value 4 into EBX, which is the distance of the member from the beginning of the structure.
This way of getting an offset is sometimes useful to get information sent by Windows in a structure. As an example, the OFNHookProc callback procedure receives from Windows information in a WM_NOTIFY message. The lParam parameter contains a pointer to an OFNOTIFY structure. This is a nested structure with the following form:-
OFNOTIFY STRUCT hdr NMHDR lpOFN DD pszFile DD ENDSwhere the NMHDR structure is:-
NMHDR STRUCT hwndFrom DD idFrom DD code DD ENDSSo within your window procedure you can get the value of the member idFrom in the NMHDR (identifier of the control sending the message) as follows:-
MOV ESI,[EBP+14h] ;get the pointer to the OFNOTIFY structure MOV EAX,[ESI+OFNOTIFY.hdr.idFrom] MOV EDX,[ESI+OFNOTIFY.pszFile]In fact what is happening here is that OFNOTIFY.hdr.idFrom resolves to a value of 4; OFNOTIFY.pszFile resolves to a value of 10h. These are their correct offsets from the beginning of the OFNOTIFY structure. Of course the structures concerned must be known to GoAsm. This is done by including the structure templates in the assembler source script, somewhere earlier in the file.
RECT STRUCT
left DD 10
top DD 10
right DD 120
bottom DD 90
ENDS
You can override the initialisation of the structure using the < and >,
{ and } operators
for example
rc1 RECT <0,20,120,300>sets the dwords in the data structure to 0, 20, 120 and 300 respectively.
rc1 RECT <0,?,?,300> rc1 RECT <0,,,300>here you override only the first and fourth members of the structure.
Using braces you can pick and choose which members to override:-
rc1 RECT {left=2,top=5}
or you can mix the two methods:-
rc1 RECT <{left=2,top=5},300h>
When using braces you don't need to specify the full symbol name
(in the above example this would be "rc1.left" and "rc1.top"). Instead
you only specify the ultimate name ("left" and "top"). The override
is also carried out into nested structures, so if you use the same
names for members within a nested structure it is possible to
initialise several members at once using one brace override.
UP STRUCT DB 27 DUP 0 DB 2 DUP 0 ENDS Pent UP <'My cat was born on 23 April',<23h,4h>>So, for example here is the GUID structure and a typical initialisation for COM:-
GUID STRUCT
Data1 dd ?
Data2 dw ?
Data3 dw ?
Data4 db 8 dup ?
GUID ENDS
IID_IShellLink GUID <0000214eeh, 00000h, 00000h, <0c0h, 00h, 00h, 00h, 00h, 00h, 00h, 46h>>
RECT STRUCT
left DD
top DD
right DD
bottom DD
ENDS
and
RECT STRUCT
left DD 0
DD 2 DUP 0
bottom DD 0
ENDS
and
RECT STRUCT DD 4 DUP 0 ENDSare equally valid structure declarations. However, where members are named they must be on a new line.
RECT STRUCT
left DD 0
top DD 0
right DD 0
bottom DD 0
ENDS
RECT2 STRUCT
left DD 0
top DD 0
right DD 0
bottom DD 0
ENDS
If you use ? in the initialisation of the structure members
this has the same effect as using zero. This does not result in the
data being recorded as uninitialised, as it
would do with an ordinary data declaration, so
RECT STRUCT
left DD ?
top DD ?
right DD ?
bottom DD ?
ENDS
rc1 RECT
is perfectly valid, but the data will go in the section initialised
to zero as if zeroes had been used.
In a structure template you can make additional data on one line in the usual way so that this would be a structure template of four dwords:-
RECT STRUCT
lefttop DD 0,0
rightbottom DD 0,0
ENDS
RECT <>,<>,<>,<>Creates four RECT structures (four dwords in each). Since no label has been used in front of the RECT, no symbols at all will be created and passed to the debugger. In this example:-
Buffer RECT <0,0,10,10>,<5,5,20,20>,<8,8,30,30>an array is made of three RECT structures (four dwords in each) initialised to the values provided. Symbols will only be made for the very first structure. This is to avoid duplication of symbol names.
If you want the members of the array to have unique symbol names you would need to use (for example):-
Buffer1 RECT <0,0,10,10> Buffer2 RECT <5,5,20,20> Buffer3 RECT <8,8,30,30>or
Buffer RECT3 <0,0,10,10, 5,5,20,20, 8,8,30,30>where RECT3 is a structure of 3 RECTS.
If you don't need to initialise the structures you can repeat them using either:-
Buffer RECT <>,<>,<>which creates three RECT structures, or
Buffer RECT,RECT,RECTwhich does the same thing.
You can also use DUP to repeat structures for example:-
ThreeRects RECT 3 DUP <> FiveRects RECT 5 DUP <23,24,25,26>In the second example each RECT is initialised to the same value. Initialisation of duplicated structures in this way can only be done at the top level and not in nested structures.
RECT STRUCT
left DD 0
top DD 0
right DD 0
bottom DD 0
ENDS
StructTest STRUCT
a DD 6
b RECT
c DD 7
d DD 8
ENDS
Then
Hello StructTestCreates seven dwords. The symbols created (and passed to the debugger) are:-
Hello Hello.a Hello.b Hello.b.left Hello.b.top Hello.b.right Hello.b.bottom Hello.c Hello.dand they can be read from or written to in the usual way, for example
MOV D[Hello.b.left],100h ;make rectangle start at 256 pixelsLike structure members, nested structures need not be named, so that this is perfectly valid:-
StructTest STRUCT
DD 6
RECT
c DD 7
d DD 8
ENDS
StructTest STRUCT
a DD 6
b STRUCT
left DD 0
top DD 0
right DD 0
bottom DD 0
ENDS
c DD 7
d DD 8
ENDS
Then
Hello StructTestproduces the same result as StructTest in the previous example. The only difference is that the RECT structure is not available for use elsewhere.