64 bit assembler > ASMC Development

Object-oriented programming (OOP) in Asmc

(1/2) > >>

nidud:
This is based on the Component Object Model (COM) introduced by Microsoft in 1993. In order to access these objects some "unconventional" methods where added to the assembler. The way COM objects are accessed in this case are from the C implementation declared in the Microsoft header files. A typical class will then be like this:

typedef struct IShellFolderVtbl {
   BEGIN_INTERFACE
    HRESULT ( STDMETHODCALLTYPE *QueryInterface )( IShellFolder * _This, REFIID riid, void **ppvObject);
    ...
   END_INTERFACE
    } IShellFolderVtbl;

interface IShellFolder {
   CONST_VTBL struct IShellFolderVtbl *lpVtbl;
    };

The assembler version:

--- Code: ---IShellFolderVtbl struct
STDMETHOD QueryInterface, :REFIID, :ptr
...
IShellFolderVtbl ends
IShellFolder struct
lpVtbl PVOID ?
IShellFolder ends
LPIShellFolder typedef ptr IShellFolder

--- End code ---

The objects are normally instantiated with a CreateInstance() function that takes the class as argument and we end up with a pointer or just AX. The call to the method is then something like this:

    mov rcx,rax   ; load args
    mov rdx,riid
    mov r8,ppvObject
    mov rax,[rcx] ; make the call
    call [rax].IShellFolderVtbl.QueryInterface

The assembler needs to know the type of the base class in order to use these unconventional methods.

    local s:IShellFolder
    s.QueryInterface(riid, ppvObject)
  * lea rcx,s

    local p:LPIShellFolder
    p.QueryInterface(riid, ppvObject)
  * mov rcx,p

    assume rbx:ptr IShellFolder
    [rbx].QueryInterface(riid, ppvObject)
  * mov rcx,rbx

If PROC is used inside a structure (normally an error) Asmc assumes this to be a pointer to a function. A structure member with a placeholder for strcmp/stricmp may then be defined as follow:

    Compare proc :LPSTR, :LPSTR

  * T$0001 typedef proto :LPSTR, :LPSTR
  * P$0001 typedef ptr T$0001
  * Compare P$0001 ?

The next target is a simplified class definition based on the above logic including static/external callbacks. The .CLASSDEF <name> [args] directive creates two structures and add the Vtbl pointer to the base.

.classdef IShellFolder

    QueryInterface      proc :REFIID, :ptr
    AddRef              proc
    Release             proc

    .ends

The first method closes the base and adds Vtbl to the name.

.classdef IShellFolder
  * LPISHELLFOLDER typedef ptr IShellFolder
  * LPISHELLFOLDERVtbl typedef ptr IShellFolderVtbl
  * IShellFolder@IShellFolder proto :ptr IShellFolder
  * IShellFolder struct 8
  * lpVtbl LPISHELLFOLDERVtbl ?
  * IShellFolder ends
  * IShellFolderVtbl struct 8
    QueryInterface      proc :REFIID, :ptr
  * T$000B typedef proto :ptr IShellFolder, :REFIID, :ptr
  * P$000B typedef ptr T$000B
  * QueryInterface P$000B ?
    ...
    .ends
  * IShellFolderVtbl ends

To define the Compare pointer as in the structure above locally in the base class the keyword LOCAL may be used. Locally defined functions are called directly without the _this argument omitted.

    Compare proc local :LPSTR, :LPSTR

Keywords like public could be consider later I guess, or any keyword not normally used after PROC but this in turn creates a half-baked OOP framework to exploit.

nidud:
Here's a class created on the stack. This one resets the stack on exit so the order in which they are deleted matters as oppose to delete everything in the end using LEAVE. Note that the start offset of the class (.base in this case) is off by 8 bytes hence the buffer is aligned 16.


--- Code: ---;
; Build: asmc -pe -win64 $*.asm
;
include stdio.inc

    .code

.classdef XPush :UINT

    base    QWORD ?
    buffer  OWORD 8 dup(?)

    Release proc

    .ends

    assume rcx:LPXPUSH

    option win64:rsp nosave noauto

XPush::Release proc

    movaps xmm0,[rcx].buffer[0x00]
    movaps xmm1,[rcx].buffer[0x10]
    movaps xmm2,[rcx].buffer[0x20]
    movaps xmm3,[rcx].buffer[0x30]
    movaps xmm4,[rcx].buffer[0x40]
    movaps xmm5,[rcx].buffer[0x50]
    movaps xmm6,[rcx].buffer[0x60]
    movaps xmm7,[rcx].buffer[0x70]
    mov rax,[rsp]
    mov rsp,[rcx].base
    jmp rax

XPush::Release endp

XPush::XPush proc ReservedStack:UINT

    .repeat

        .if !rcx

            mov r8,rsp
            lea rax,[r8+rdx]        ; stack base
            lea rcx,[rdx+8*16+16]   ; alloc size
            sub rax,rcx
            and al,-16
            test [rax],al           ; probe..
            mov rcx,[r8]            ; return addr
            mov rsp,rax             ; new stack
            mov [rax],rcx
            and dl,-16              ; shadow space
            lea rcx,[rax+rdx+16]
            add r8,8
            mov [rcx].base,r8
        .endif

        movaps [rcx].buffer[0x00],xmm0
        movaps [rcx].buffer[0x10],xmm1
        movaps [rcx].buffer[0x20],xmm2
        movaps [rcx].buffer[0x30],xmm3
        movaps [rcx].buffer[0x40],xmm4
        movaps [rcx].buffer[0x50],xmm5
        movaps [rcx].buffer[0x60],xmm6
        movaps [rcx].buffer[0x70],xmm7

        lea rdx,[rax+0x80]
        mov [rcx],rdx
        lea rax,XPush@Release
        mov [rdx],rax
        mov rax,rcx
    .until 1
    ret

XPush::XPush endp

    option win64:rbp auto

main proc

  local p:LPXPUSH

    printf("rsp: %p\n", rsp)
    mov p,XPush::XPush( NULL, @ReservedStack )
    printf("rsp: %p\n", rsp)
    p.Release()
    printf("rsp: %p\n", rsp)
    ret

main endp

    end main

--- End code ---

nidud:
.COMDEF
 
Added a separate directive for COM without any types added. This is now used in the windows include files. In addition to this a default method (Release()) is added to .CLASSDEF.

    Method1 proc local  ; static
    Method2 proc :ptr   ; first virtual function
  * Class ends
  * ClassVtbl struct
  * T$000B typedef proto :ptr Class
  * P$000B typedef ptr T$000B
  * Release P$000B ?
  * T$000B typedef proto :ptr Class
  * P$000B typedef ptr T$000B
  * Method2 P$000B ?

This didn't work (fixed): externdef Profile:ptr CProfile

This is indeed a good way of bundle up software where objects could be created on the fly or used as global pointers. Data may not be needed or accessed using methods so the public information may not be the same as in the local implementation.

Local definition

MAXPROFILES equ 32

PROFILE     STRUC
string      LPSTR ?
begin       dq ?
result      dq ?
PROFILE     ENDS

.classdef CProfile

    table PROFILE MAXPROFILES dup(<>)

    Start proc :UINT, :LPSTR
    Stop  proc :UINT
    Save  proc :UINT, :LPSTR
    Clear proc

    .ends

Global header

.classdef CProfile

    Start proc :dword, :ptr
    Stop  proc :dword
    Save  proc :dword, :ptr
    Clear proc

    .ends

externdef Profile:LPCPROFILE

Guess you may see this as protected or private members of the class. As for sheared members this may be done using pointers (as in COM) or inheritance for code reuse.

Two classes using the same methods:

.classdef CHeap BufferSize:UINT

    heap_base   PVOID ?
    heap_free   PVOID ?

    Alloc       proc :UINT
    Free        proc :PVOID
    Realloc     proc :PVOID, :UINT
    Aligned     proc :UINT, :UINT
    NewString   proc :LPSTR
    Coreleft    proc

    .ends

.classdef CStack BufferSize:UINT, ReservedStack:UINT

    heap_base   PVOID ?
    heap_free   PVOID ?
    stack_ptr   SIZE_T ?

    Alloc       proc :UINT
    Free        proc :PVOID
    Realloc     proc :PVOID, :UINT
    Aligned     proc :UINT, :UINT
    NewString   proc :LPSTR
    Coreleft    proc

    .ends

The linkage of these modules is based on usage so the instance of Profile is auto created and ready to go in the same way as the __argv table. The stack buffer (CStack) however needs to be created in a stack frame so this have to be dynamically created.

The heap functions may then be profiled as follow:

--- Code: ---if 0 ; compare to LIBC.malloc()
include malloc.inc
else
malloc  equ <memory.Alloc>
free    equ <memory.Free>
endif

; global pointer

.classdef CStack :dword, :dword
    Alloc       proc :dword
    Free        proc :ptr
    .ends

    .data
    memory  LPCSTACK 0
    .code

; create a one million byte stack buffer

main proc

  local stk ; create a standard stack frame

    mov memory, CStack::CStack(NULL, 1000000, @ReservedStack)
    heap_loop()
    memory.Release()

    Profile.Save(4, "CStack::CStack()")
    ret

main endp

; allocate some memory

heap_loop proc uses rsi rdi rbx r12

    Profile.Start(0, "main")

    .for (r12d=1000 : r12d : r12d-- )

        Profile.Start(1, "loop")

        .break .if !malloc( 1000 * size_t )
        mov rsi,rax

        Profile.Start(2, "malloc")
        .for ( rdi=rsi, ebx=0 : ebx < 1000 : ebx++ )

            malloc( 512 )
            stosq
        .endf
        Profile.Stop(2)

        Profile.Start(3, "free")
        .for ( rdi=rsi, ebx=0 : ebx < 1000 : ebx++ )

            lodsq
            free(rax)
        .endf
        Profile.Stop(3)

        free(rdi)
        Profile.Stop(1)
    .endf
    Profile.Stop(0)
    ret

heap_loop endp

--- End code ---

In this case the .Save() command append result to a text file in this format:

--- Code: ---CStack::CStack() - 2018-4-2 21:13:58:
 0:       52290 main
 1:       46500 loop
 2:        9524 malloc
 3:        8708 free
LIBC.malloc() - 2018-4-2 21:14:58:
 0:       71304 main
 1:       65418 loop
 2:       26041 malloc
 3:       16665 free

--- End code ---

As for a more sophisticated implementation of OOP this I think will be rather clunky in assembler. It will also demand a lot of compiling in addition to all the bloat produced. The 64-bit code seems to clean up very nicely using this approach, having RCX pinned to the class. Making a habit of also returning RCX may simplify even further.

foo proc

  local p:LPCLASS

    [rcx].Method1()
    [rcx].Method2(rdx)
    [rcx].Release()
    ret

foo endp

Code produced:

   0: 55            push   rbp
   1: 48 8b ec      mov    rbp,rsp
   4: 48 83 ec 30   sub    rsp,0x30
   8: ff 51 10      call   QWORD PTR [rcx+0x10]
   b: 48 8b 01      mov    rax,QWORD PTR [rcx]
   e: ff 50 08      call   QWORD PTR [rax+0x8]
  11: 48 8b 01      mov    rax,QWORD PTR [rcx]
  14: ff 10         call   QWORD PTR [rax]
  16: c9            leave
  17: c3            ret

Greenhorn:
Hi nidud,

there seems to be a name conflict when creating methods with names matching from protos in other includes, e.g. windows header files.

For example, if you create a method named OpenFile or CreateFile the call to it will throw an error.

"class.inc"

--- Code: ---include windows.inc ;// OpenFile declared in winbase.inc
include stdio.inc
include malloc.inc

.classdef Class :LPSTR

    string LPSTR ?

    Print proc
    OpenFile proc :LPTSTR ;// Class::OpenFile ( ) declaration

    .ends


--- End code ---

"class.asm"

--- Code: ---include class.inc

    .data
    virtual_table PVOID 0

    .code

Class::Print proc

    printf( "%s\n", [rcx].Class.string )
    ret

Class::Print endp

;/////////////////////////////////////////////////
;//  Class::OpenFile procedure/method definition
;//
Class::OpenFile proc szPath:LPTSTR

xor rax, rax
ret
Class::OpenFile endp

Class::Class proc String:LPSTR

    .repeat

        .if !rcx

            .break .if !malloc( sizeof(Class) )
            mov rcx,rax
            mov rdx,String
        .endif
        mov [rcx].Class.string,rdx
        mov rax,virtual_table
        mov [rcx].Class.lpVtbl,rax
        mov rax,rcx
    .until 1
    ret

Class::Class endp

Install proc private

    .if malloc( sizeof(ClassVtbl) )

        mov virtual_table,rax
        lea rcx,free
        mov [rax].ClassVtbl.Release,rcx
        lea rcx,Class@Print
        mov [rax].ClassVtbl.Print,rcx
    .endif
    ret
Install endp

.pragma init Install 50

    end

--- End code ---

"test.asm"

--- Code: ---include class.inc

    .code

main proc

  local p:LPCLASS, s:Class

    .if Class::Class( NULL, "String" )

        mov p,rax

        p.Print()
        p.OpenFile("test.inc") ;// error A2008: syntax error : p
        p.Release()
    .endif

    .if Class::Class( &s, "String2" )

        s.Print()
    .endif
    ret

main endp

    end

--- End code ---

Output
--- Code: ---test.asm(14) : error A2008: syntax error : p
--- End code ---

This is because asmc assumes in the call function OpenFile from winbase.inc.


Regards
Greenhorn

nidud:

--- Quote from: Greenhorn on April 16, 2018, 06:02:10 AM ---there seems to be a name conflict when creating methods with names matching from protos in other includes, e.g. windows header files.

--- End quote ---

Yes. It follows the same logic as a struct members. I usually add an underscore in front of these names.

I guess it would be possible to add some exception to this but that will destroy the simplicity of it all. As of now the .COMDEF and .CLASSDEF directives only creates two structures and add members accordingly, so this will also work without these directives by simply declaring a COM object as done in C.

Navigation

[0] Message Index

[#] Next page

Go to full version