Assembly Fun!!

Programming, for all ages and all languages.
Post Reply
pcmattman
Member
Member
Posts: 2566
Joined: Sun Jan 14, 2007 9:15 pm
Libera.chat IRC: miselin
Location: Sydney, Australia (I come from a land down under!)
Contact:

Assembly Fun!!

Post by pcmattman »

I'm trying to figure out how to write an assembly function that takes a variable number of arguments that can be called from C.

So far, I can read each individual argument if it's hard-coded (code below)

Hard-code inividual reading:

Code: Select all

push ebp
mov ebp,esp

mov eax,[ebp+16]

mov esp,ebp
pop ebp

ret
And this works...

Problem is, I need to work with a variable number of arguments... I've tried the following (you finish the argument list with 0x1a2b):

Code: Select all

pushloop:

		cmp dword [esp],0x1a2b
		je complete

		push dword [esp]
		add esp,4

		jmp pushloop

complete:
But it doesn't work...

Any ideas as to how to read the list of arguments, and push them onto the stack for me to call another function?
User avatar
os64dev
Member
Member
Posts: 553
Joined: Sat Jan 27, 2007 3:21 pm
Location: Best, Netherlands

Post by os64dev »

in assembly it might be different but you could do it in c with the ... parameter, for instance void vargsfunc(void * base, ...); and then compile and do an object dump of the object file, you will get all the assembly you need. might i suggest you do it in C anyway because when you port the functionality, for instance 64 long mode, the C part will adapt by you assembly will fail misserably.

regards
Author of COBOS
pcmattman
Member
Member
Posts: 2566
Joined: Sun Jan 14, 2007 9:15 pm
Libera.chat IRC: miselin
Location: Sydney, Australia (I come from a land down under!)
Contact:

Post by pcmattman »

Well, now I can actually run the functions via the assembly... New problem is that it crashes whenever the actual caller returns... not good at all.

Edit: the actual code is below, first argument to the function is the address of the function to call, the rest is the arguments

Code: Select all

_CallProg:

	push ebp
	push ebx

	mov ecx,[esp]

	mov ebp,esp

	mov eax,[ebp+12]		; get the address of the function

	; skip the trash
	add esp,16

	; call it
	call eax

	mov esp,ebp
	pop ebx
	pop ebp

	; this is important: one more pop must be done because if it isn't then
	; the return will not be appropriate
	pop edx

	push ecx
	retn
Edit 2: It seems to work but with only some functions. For instance, MessageBoxA works perfectly, but then MessageBeep also works but then crashes the program with an access violation... it seems that the program tries to execute code that isn't anywhere near my program :?
User avatar
os64dev
Member
Member
Posts: 553
Joined: Sat Jan 27, 2007 3:21 pm
Location: Best, Netherlands

Post by os64dev »

Code: Select all

_CallProg: 

   push ebp 
   push ebx 

   mov ecx,[esp] 

   mov ebp,esp 

   mov eax,[ebp+12]      ; get the address of the function 

   ; skip the trash 
   add esp,16 

   ; call it 
   call eax 

   mov esp,ebp 
   pop ebx 
   pop ebp 

Both ecx and eax get clobbered so you might wanna save them, well maybe not eax because that will problably contain the return value. Then again if the function to be called doesn't have a return code then it should be restored. so it is better to have one additional parameter to the _Callprog function that gets filled with the return value(eax) after the call eax statement and restore eax to the original value. You also don't know which registers the function might changed so it's problably better to save them all.

Code: Select all

   ; this is important: one more pop must be done because if it isn't then 
   ; the return will not be appropriate 
   pop edx 

   push ecx 
   retn
Are you sure about this, because it sound kind a freaky?
Author of COBOS
Otter
Member
Member
Posts: 75
Joined: Sun Dec 31, 2006 11:56 am
Location: Germany

Post by Otter »

You try to use a trick to avoid copying the parameters passed to _CallProg to the function you want to call. I read your code so I assume that your routine _CallProg uses the cdecl calling convention, that means that the caller of _CallProg cleans the stack. But this means, that the stack has to be unmodified if you return to the caller. You need to make sure that you thougt about the following:
1) If _CallProg returns, esp has to be the same value as when _CallProg starts.
2) At [esp] you need the return address of the caller.
Because you use cdecl calling convention you have also to make sure that ebp does not change during _CallProg.

You thougt about the first point, so you use

Code: Select all

mov ebp,esp
...
mov esp,ebp
That's correct. But all the other stuff trashes your original stack and is not able to restore it. For example, at the end, you pop data from the stack which are not valid any more, because of your "add esp,16". You should NEVER ADD any value to esp if you do not exactly understand what you do, especially not if you have pushed data which you want to pop later.

[edit]Btw, you should know about the calling convention of the function you call. MessageBoxA for example uses stdcall, not cdecl.[/edit]
pcmattman
Member
Member
Posts: 2566
Joined: Sun Jan 14, 2007 9:15 pm
Libera.chat IRC: miselin
Location: Sydney, Australia (I come from a land down under!)
Contact:

Post by pcmattman »

Ok... how exactly do I get those arguments onto the stack, without modifying esp?

Edit: For the time being assume that all functions called via this use stdcall
User avatar
Combuster
Member
Member
Posts: 9301
Joined: Wed Oct 18, 2006 3:45 am
Libera.chat IRC: [com]buster
Location: On the balcony, where I can actually keep 1½m distance
Contact:

Post by Combuster »

stdcall and cdecl have one big difference: stdcall pops all arguments from the stack, cdecl leaves the stack intact. Since the caller knows the amount of arguments he can clean it up easier than the called function (which would need to do some nasty stack manipulation, while the caller can just use an add esp, 4*no_of_arguments)

so why you use stdcall is beyond me, it is more trouble than you need

gcc defaults to cdecl, so you'd need to leave the stack intact.
hence you'd need to do something like this:

(untested)

Code: Select all

my_function:
; standard prolog
push ebp
mov ebp, esp

; one local variable (here: backup for esi - you can add more)
sub esp, 4

; cdecl: esi, edi, ebp, esp and ebx need to be preserved
mov [ebp-4], esi

; create a pointer to the source
lea esi, [ebp+4]

; for each argument you want to use
lodsd
; which will load the argument in eax
; (it does mov eax, [esi] - add esi, 4)
; esi will point to the next argument
; do this as much as you need (leave esi untouched)

; restore esi
mov esi, [ebp-4]
; standard epilog
leave
ret


; in c:
void my_function(...); // NO stdcall
"Certainly avoid yourself. He is a newbie and might not realize it. You'll hate his code deeply a few years down the road." - Sortie
[ My OS ] [ VDisk/SFS ]
pcmattman
Member
Member
Posts: 2566
Joined: Sun Jan 14, 2007 9:15 pm
Libera.chat IRC: miselin
Location: Sydney, Australia (I come from a land down under!)
Contact:

Post by pcmattman »

Well, actually... There's a slight problem with that code...

CallProg is called as so (p holds the address of MessageBoxA for example):

Code: Select all

CallProg( (int) *p, NULL, "CallProg calling with unknown number of arguments!", "main->CallProg->MessageBoxA", MB_OK | MB_ICONINFORMATION, 0x1a2b /* argument terminator */ );
How can I do this, for any cdecl function?
Otter
Member
Member
Posts: 75
Joined: Sun Dec 31, 2006 11:56 am
Location: Germany

Post by Otter »

One thing ... MessageBoxA is a winApi function and it is stdcall, not cdecl. Maybe you always use stdcall or you need a new parameter to CallProg which contains the calling convention.

I see only one possible solution: You have to copy all the parameters ! You need to step through all the parameters passed to CallProg and push them again onto the stack. Then, you could simply call your function.
User avatar
XCHG
Member
Member
Posts: 416
Joined: Sat Nov 25, 2006 3:55 am
Location: Wisconsin
Contact:

Post by XCHG »

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.
Post Reply