For Variable Length Arguments, I chose the approach that the Delphi/C++ Builder compilers have chosen in which all parameters are first pushed onto the stack in whatever order you like and then the length of the arguments, which is the number of arguments, are pushed onto the stack. To make it more consistent, all parameters are recommended to be 4 bytes long on 32-bit architecture and 2 bytes on 16-bit (Real-mode).
What you will be left to do is to:
1. Push the parameters from right to left as in StdCall or Left to right as in Pascal.
2. Push the number of parameters as the last parameter, onto the stack.
3. Call the procedure that uses Variable Length Arguments.
4. Push whatever value that you like onto the stack including general purpose registers and build your stack pointer, using the base pointer (EBP).
5. Calculate the number of bytes that are required for you to reach the last parameter, which is the number of parameters pushed onto the stack for the current procedure.
6. Add the above value to the current value of the EBP and there you will be able to reach the number of parameters.
7. Create and iteration and add 0x00000004 to the EBP for DWORD parameters or 0x0002 to WORD parameters to reach consecutive parameters.
8. Destruct the stack frame and return to the calling procedure.
9. The calling procedure MUST then clear the parameters from the stack.
As an example, I have written the below code for you in MASM which creates a procedure that can accept a variable number of strings and then will display them on the screen using the MessageBox Win32 API:
Code: Select all
.386
.MODEL FLAT, STDCALL
OPTION CASEMAP:NONE
INCLUDE \MASM32\INCLUDE\windows.inc
INCLUDE \MASM32\INCLUDE\user32.inc
INCLUDE \MASM32\INCLUDE\kernel32.inc
INCLUDELIB \MASM32\lib\user32.lib
INCLUDELIB \MASM32\lib\kernel32.lib
ALIGN 04
.DATA
String1 DB 'Awesome', 0
String2 DB 'Is', 0
String3 DB 'Assembly', 0
ALIGN 04
.CODE
OPTION PROLOGUE:NONE
OPTION EPILOGUE:NONE
EVEN
; -----------------------------------------
VarArgumentMsgBox PROC
PUSH EAX ; Push the accumulator onto the stack
PUSH EBX ; Push the base index onto the stack
PUSH EBP ; Push the base pointer onto the stack
MOV EBP , ESP ; Move the stack pointer to the base pointer
ADD EBP , 00000010h ; Access the Length parameter (The last)
MOV EBX , DWORD PTR [EBP] ; The number of parameters
@@__Iterate: ; The iteration begins here
ADD EBP , 0004h ; Move to the next parameter
MOV EAX , DWORD PTR [EBP]
INVOKE MessageBox, 0b, EAX, 0b, MB_ICONINFORMATION
DEC EBX
JNZ @@__Iterate
POP EBP
POP EBX
POP EAX
RET
VarArgumentMsgBox ENDP
; -----------------------------------------
EVEN
START:
PUSH OFFSET String1
PUSH OFFSET String2
PUSH OFFSET String3
PUSH 00000003h
CALL VarArgumentMsgBox
ADD ESP , 0000000Ch
INVOKE ExitProcess, 0b
END START
Note that the “MessageBox” Win32 API destructs the value of both the counter register (ECX) and the Data Register (EDX) therefore, you are better of using the base index (EBX) as the counter or else you will lose your counter’s initial and current value.
I have also used this approach in my Open-source Assembly Library (OASML). One of the procedures is implemented in this way:
Code: Select all
; ------------------------------
WriteStrFilter PROC
COMMENT *
Description : Writes a null-terminated string with filters, to the screen.
See note(s).
Calling Convention : Push from left to right.
Parameter(s) :
WORD Param1 = Source string's offset.
WORD Param2 = First parameter's offset.
WORD Param3 = Second parameter's offset.
...
...
...
WORD ParamN = Last parameter's offset
WORD ParamN+1 = The offset of the 2 bytes filter.
Stack Usage: 12 Bytes.
Note : 1) The [WriteStrFilter] uses a variable argument list as filters which
will be applied to the Source string later when being printed
to the screen.
2) The [WriteStrFilter] changes the first occurrence of [ParamN]
inside [Param1] to [Param2], the second one to [Param3], the
third one to [Param4] and the last one to [ParamN].
3) The AX register should indicate the total number of parameters
which are pushed onto the stack by the programmer.
4) Each and all of the parameters except for the [ParamN+1]
parameter which is the main filer, should be terminated by a
null-character/byte.
5) The [WriteStrfilter] works in the same way as the "printf"
procedure does in the C programming language. For example,
by adding the filter "%d" in a string, you
will later be able to replace it with a decimal value and etc
6) The [ParamN] which works as the filter must be 2 bytes long
and will be found anywhere inside the [Param1] which is the
source string.
7) The search for [ParamN+1] inside [Param1] to be replaced with
[Param1]...[ParamN] is case sensitive.
8) Although the [ParamN+1] which works as the filter can contain
more than 2 bytes/characters but only the first two ones are
important to this procedure.
9) The value of the AX register will remain unchanged after
the execution of the procedure.
Example : 1) Write the string "%S is a powerful %S" with the first
%S filter changed to the string "Assembly" and the second
one to "programming language".
.DATA
StrSrc DB '%S is a powerful %S', 0
String1 DB 'Assembly', 0
String2 DB 'programming language', 0
StrFilt DB '%S'
.CODE
PUSH OFFSET StrSrc ; The source string
PUSH OFFSET String1 ; The first parameter
PUSH OFFSET String2 ; The second parameter
PUSH OFFSET StrFilt ; The 2 bytes filter
MOV AX , 0002h ; We only have 2 parameters
CALL WriteStrFilter ; Write the filtered string
ADD SP , 0008h ; Remove all the offsets from the stack
; Prints 'Assembly is a powerful programming language'
Example : 2) Write a sentence which includes your favorite music genres by
applying a filter to the main sentence.
.DATA
StrSrc DB '?? Metal, ?? Metal and ?? Metal ',\
'are my favorite music genres', 0
String1 DB 'Heavy', 0
String2 DB 'Thrash', 0
String3 DB 'Black', 0
StrFilt DB '??'
.CODE
PUSH OFFSET StrSrc ; The source string
PUSH OFFSET String1 ; The first parameter
PUSH OFFSET String2 ; The second parameter
PUSH OFFSET String3 ; The third parameter
PUSH OFFSET StrFilt ; The 2 bytes filter
MOV AX , 0003h ; We have 3 parameters
CALL WriteStrFilter ; Write the filtered string
ADD SP , 000Ah ; Remove all the offsets from the stack
*
PUSH AX ; Push the accumulator onto the stack
PUSH BX ; Push the base index onto the stack
PUSH CX ; Push the count register onto the stack
PUSH DX ; Push the data register onto the stack
PUSH SI ; Push the source index onto the stack
PUSH BP ; Push the base pointer onto the stack
MOV BP , SP ; Move the stack pointer to the base pointer
MOV BX , WORD PTR [BP+0Eh] ; BX now points to the delimiter
MOV DX , WORD PTR [BX] ; DL=1st, DH=2nd character in delimiter
MOV CX , AX ; CX = count of the parameters
SHL AX , 01h ; Each parameter is two bytes long
ADD AX , 0010h ; Add the fixed pushed items
MOV SI , AX ; Move the current position to the source index
MOV BX , WORD PTR [BP+SI] ; BX now points to the main string
@@__WriteStrFilerLoop: ; The main loop
MOV AX , WORD PTR [BX] ; Read two bytes from the string to AX
TEST AL , AL ; See if the first read byte is zero
JE @@__WriteStrFilerEp ; Jump to the end of the procedure if yes
TEST CX , CX ; See if the parameter count is zero
JE @@__WriteStrFilerLoopTail; Jump to ... if yes
CMP AL , DL ; See if the read character is equal to DL
JNE @@__WriteStrFilerLoopTail; Jump to ... if not
CMP AH , DH ; See if the second read char is equal to DH
JNE @@__WriteStrFilerLoopTail; Jump to ... if not
ADD BX , 0002h ; Skip two bytes in the source string
MOV AX , CX ; AX is the total number of parameters
DEC CX ; Decrement the total number of parameters
SHL AX , 01h ; Each item is 2 bytes long
ADD AX , 000Eh ; Add the fixed number of pushes to AX
MOV SI , AX ; SI now points to the current parameter's offset
MOV SI , WORD PTR [BP+SI] ; SI now points to the paramater
MOV AH , 0Eh ; Character Printing function
@@__WriteStrFilerIL: ; The inner loop
MOV AL , BYTE PTR [SI] ; Read one byte from the parameter string
TEST AL , AL ; See if it is zero
JE @@__WriteStrFilerLoop ; Jump to ... if yes
INC SI ; Move to the next byte in the parameter
DW 10CDh ; Issue the interrupt
JMP @@__WriteStrFilerIL ; Keep doing all these again until AL!=0
@@__WriteStrFilerLoopTail: ; The outer loop's tail
MOV AH , 0Eh ; Character printing function
DW 10CDh ; Issue the interrupt
INC BX ; Move to the next byte in the buffer
JMP @@__WriteStrFilerLoop ; Keep doing all these until AL!=0
@@__WriteStrFilerEP: ; End of the procedure routine
POP BP ; Restore the base pointer
POP SI ; Restore the source index
POP DX ; Restore the data register
POP CX ; Restore the count register
POP BX ; Restore the base index
POP AX ; Restore the accumulator
RET ; Return to the calling procedure
WriteStrFilter ENDP
; ------------------------------
Note that the Delphi compiler uses N-1 as the number of parameters while the number of parameters passed into the procedure are N. So if you have 3 parameters, Delphi pushes $00000002 as the last parameter. Hope I could help.