News:

Masm32 SDK description, downloads and other helpful links
Message to All Guests

Main Menu

32/64 bit code

Started by JK, December 28, 2022, 12:21:43 AM

Previous topic - Next topic

JK

I´m trying to write code, which assembles in 32 and 64 bit. Many Windows APIs require 32 bit arguments in 32 bit and 64 bit arguments in 64 bit, e.g. SendMessage and wparam/lparam. As long as i can use variables, which change size too, everything is fine.

What if i want to pass e.g. a RGB/colorref value, which is 32 bit in both worlds, but i need it to be 64 bit in 64 bit. Must i write two versions, one for 32 bit and another one for 64 bit zero extending my color value before passing it?

Which would be the most elegant way of doing it? I´m looking for a generic solution for this problem, not only for SendMessage but for all cases, where a 32 bit value must be passed to an API or a procdure expecting a 32 bit value in 64 bit and a 64 bit value in 64 bit.

Thanks

JK

jj2007

In my own JBasic implementation, I use SIZE_P as a synonym for "size of a pointer". SIZE_P is 32 for 32-bit assembly, and 64 for 64-bit assembly - very simple and easy.

The problem is that you often don't know if you really need to pass a pointer. Most of the SendMessage stuff, for example, expects DWORDs in 64-bit land, COLORREFs included - except if you pass a pointer to memory.

Below a messy attempt to heuristically sort C/C++ types into DWORD and SIZE_P by parsing their names. Check, for example, this SOF post for finding out what a POINTER_32 is :cool:

BOOL            A Boolean variable (should be TRUE or FALSE). typedef int BOOL
BOOLEAN         A Boolean variable (should be TRUE or FALSE). typedef BYTE BOOLEAN
COLORREF        The red, green, blue (RGB) color value (32 bits)
DWORDLONG       A 64-bit unsigned integer. The range is 0 through 18446744073709551615 decimal.
DWORD32         A 32-bit unsigned integer
DWORD64         A 64-bit unsigned integer
DWORD_PTR       An unsigned long type ... commonly used for general 32-bit parameters that have been extended to 64 bits in 64-bit Windows
HANDLE         typedef PVOID HANDLE;
HFILE          A handle to a file opened by OpenFile, not CreateFile. typedef int HFILE; To close call the CloseHandle function using handle ->HANDLE WINAPI CreateFile
HRESULT        typedef LONG HRESULT (32);
LRESULT        typedef LONG_PTR LRESULT (64);
INT64           A 64-bit signed integer. typedef signed __int64 INT64;
LONG            A 32-bit signed integer
LONGLONG        A 64-bit signed integer
LPARAM          typedef LONG_PTR LPARAM;
WPARAM          typedef UINT_PTR WPARAM;
LPBOOL          A pointer to a BOOL
POINTER_32     A 32-bit pointer. On a 32-bit system, this is a native pointer. On a 64-bit system, this is a truncated 64-bit pointer.
POINTER_64     A 64-bit pointer. On a 64-bit system, this is a native pointer. On a 32-bit system, this is a sign-extended 32-bit pointer.
PULONG32       A pointer to a ULONG32; typedef ULONG32 *PULONG32
QWORD           A 64-bit unsigned integer.
SC_HANDLE       typedef HANDLE SC_HANDLE
SC_LOCK         typedef LPVOID SC_LOCK
SIZE_T          typedef ULONG_PTR SIZE_T
SSIZE_T         typedef LONG_PTR SSIZE_T;
UINT            An unsigned INT. The range is 0 through 4294967295 decimal
UINT64          An unsigned INT64. The range is 0 through 18446744073709551615 decimal.
ULONG           An unsigned LONG. The range is 0 through 4294967295 decimal
ULONGLONG       A 64-bit unsigned integer. The range is 0 through 18446744073709551615 decimal.
ULONG32         An unsigned LONG32. The range is 0 through 4294967295 decimal.
VOID            Any type. This type is declared in WinNT.h as follows: #define VOID void

zedd151

COLORREF is still 32 bit even in 64 bit code.


You can always write a test piece if you are unsure if a certain value still need to be 32 bit in 64 bit land, when possible. And optionally check the results in a 64 bit debugger. (A la x64dbg, et. al.)

hutch--

JK,

You can use an equate that switches the values but keep in mind that 64 bit Windows is not fully 64 bit, some things are still 32 bit as is the case with certain structures like RECT and a few others. The actual 64 bit calling convention is fully 64 bit but in each location you can write any of the data sizes from QWORD down to BYTE and it is designed that way.

While it is ugly in earlier OS versions, you can load eax then use rax in a 64 bit call, in fact its the nly way in some instances. I get to see it with the structure CHARRANGE which has 2 x 32 bit values, cr.cpMin and cr.cpMax.

JK

Maybe i was unclear with my question. It´s not about the bitness of arguments in 32 vs. 64 bit (which is a real challenge sometimes, so thanks for your suggestions). It´s about writing code, where argument size changes from 32 to 64.

How would i write code for such a situation? E.g. i want to set a specific color for a control using Sendmessage and the appropriate message, where either wparam or lparam holds the desired color value. But this color value (RGB/colorref) is always 32 bit, so i must zero extend it to a 64 bit value in 64 bit before passing it to Sendmessage. In 32 bit i just can pass it as is, so i cannot have the same code for both.

Must i really write two versions

Pseudo code:
if 32 bit
32 bit code here ...
else
64 bit code here ...
endif


... or is there a more elegant way?

JK

hutch--

It has to do with setting an equate that specifies the data size. I don't remember the details at the moment but if you set a default data size for either platform (32 or 64) bit, if you set an equate to do the switching so the data size is 32 bit with one setting but 64 bit in the other.

SendMessage type, type, type, type and you specify if "type" is 32 or 64 bit.

It does not sound like much fun though.

zedd151


Quoteif 32 bit
32 bit code here ...
else
64 bit code here ...
endif


Okay I think what you want to do is conditional assembly, that would produce either a 32 or 64 bit executable from the same source?
In which case, is above my pay grade. (er, skillset)


Quote from: hutch-- on December 28, 2022, 01:25:35 AM
It does not sound like much fun though.
Sounds more like a nightmare, unless it will be a rather simple application.

jj2007

Quote from: JK on December 28, 2022, 01:16:41 AMMust i really write two versions

Pseudo code:
if 32 bit
32 bit code here ...
else
64 bit code here ...
endif


... or is there a more elegant way?

One version is enough. Working code, assembles in both worlds:
invoke SendMessage, rbx, EM_EXLIMITTEXT, 0, -1 ; no limit
invoke SendMessage, rbx, WM_SETFONT, rv(GetStockObject, ANSI_FIXED_FONT), 0


For 32-bit assembly, you will need an equate rbx equ ebx - that's all.

rv(GetStockObject, ANSI_FIXED_FONT) will return rax in 64-bit code, eax in 32-bit code.

As Hutch already mentioned, the register calling convention makes sure that 64-bit SendMessage picks whatever it needs: you pass rax, it needs 32-bits only, no problem, they are in eax. Note also that a mov eax, 123 propagates to mov rax, 123.

HSE

Hi Jack!

To make dual bitness code is not so hard. If you want efficient code in 32 bits (or translate faster from 32 bits to 64 bits) things are a little more complicated.

Quote from: JK on December 28, 2022, 12:21:43 AM
Which would be the most elegant way of doing it? I´m looking for a generic solution for this problem, not only for SendMessage but for all cases, where a 32 bit value must be passed to an API or a procdure expecting a 32 bit value in 64 bit and a 64 bit value in 64 bit.

No doubt, Biterider's solution is that. See https://github.com/ObjAsm/ObjAsm-C.2/blob/master/Code/Macros/System.inc (line 128)

···
if TARGET_BITNESS eq 64
  XAX   textequ   <rax>
  XBX   textequ   <rbx>
  XCX   textequ   <rcx>
  XDX   textequ   <rdx>
  XDI   textequ   <rdi>
  XSI   textequ   <rsi>
  XBP   textequ   <rbp>
  XSP   textequ   <rsp>
else
  XAX   textequ   <eax>
  XBX   textequ   <ebx>
  XCX   textequ   <ecx>
  XDX   textequ   <edx>
  XDI   textequ   <edi>
  XSI   textequ   <esi>
  XBP   textequ   <ebp>
  XSP   textequ   <esp>
endif
···


But that is the first step.

Second step is to solve push/pop problems in 64 bits. Only way I find easy requiere some complex macros: https://github.com/ASMHSE/SmplMath/blob/main/SmplMath/SmplReg.inc

Quotefreg_push  /  freg_pop: store and retrive GPR from stack.
freg_peek retrieve in a GPR last value stored without remotion.

7.3.2   Pseudo push/pop variables
freg_pushv / freg_popv: store and retrive variables from stack.
freg_peekv retrieve in a variable last value stored without remotion.
These requiere a GPR to move value (by default are EAX and R10 but you can use other).

7.3.3   Corrections
freg_correction is a not so automatic correction for unbalanced number of push/pop. That happen in conditional flow. Must be positive before extra pop and negative before extra push.

Third step is to reserve rcx and rdx in 64 bits for callings, but to use ecx and edx in 32 bits  :biggrin:. See https://github.com/ASMHSE/SmplMath/blob/main/SmplMath/translation3264.inc

QuoteA big problem in code translations from 32 bits to 64 bits it's related to X64 ABI Calling Convention. RCX and RDX have an specific function in 64 bits that ECX and EDX never have in 32 bits.
Beside, more simple replacement of registers, like XCX that become ECX in 32 bits and RCX in 64 bits, sometimes result incomplete because 32 bits registers can be used like DWORD or like POINTER, and they are same thing in 32 bits but have differents sizes in 64 bits.
But in 64 bits you have more registers. Then this macro make a systematic replace of registers to use exclusive 64 bits registers instead of RCX and RDX, and prefix "__" or "_" declare if register is used like DWORD or POINTER

A complete example is Complex Numbers in 64 bits
Equations in Assembly: SmplMath

jj2007

A look under the hood with a practical example:

int 3
jinvoke SendMessage, rbx, WM_SETFONT, rv(GetStockObject, ANSI_FIXED_FONT), 0
nops 2


Assembled as 32-bit code:
0040114A  |.  CC            int3
0040114B  |.  6A 0B         push 0B
0040114D  |.  FF15 E4444000 call near [4044E4]             ; GetStockObject
00401153  |.  6A 00         push 0
00401155  |.  50            push eax
00401156  |.  6A 30         push 30
00401158  |.  53            push ebx
00401159  |.  FF15 E0444000 call near [4044E0]             ; SendMessage
0040115F  |.  90            nop
00401160  |.  90            nop


Assembled as 64-bit code:
1400011FA | CC                         | int3                            |
1400011FB | B9 0B000000                | mov ecx,B                       |
140001200 | FF15 AE330000              | call [<&GetStockObject>]        |
140001206 | 45:33C9                    | xor r9d,r9d                     |
140001209 | 4C:8BC0                    | mov r8,rax                      |
14000120C | BA 30000000                | mov edx,30                      |
140001211 | 48:8BCB                    | mov rcx,rbx                     |
140001214 | FF15 92330000              | call [<&SendMessageA>]          |
14000121A | 90                         | nop                             |
14000121B | 90                         | nop                             |


As you can see, the passing of parameters is entirely different, so jinvoke is clearly a macro. Same for Hutch' Masm64 SDK invoke macro.

The usage of xax vs rax is a matter of taste. 32-bit Masm and clones don't know rax, so a simple rax equ eax does the job - you just use rax for both worlds, except if you know it's a DWORD, so you pass eax.

Note there should be no pushing in 64-bit code, so m2m edx, 30h becomes mov edx, 30h:
48:C7C2 30000000           | mov rdx,30
BA 30000000                | mov edx,30
6A 30                      | push 30
5A                         | pop rdx


mov edx, 30h is two bytes longer than push 30h + pop rdx, but two bytes shorter than the equivalent mov rdx, 30h.

JK

When moving a 32 bit value to a register in 64 bit (mov eax, 1) the upper half of RAX is reset to zero, which in effect is an implicit zero extension of this 32 bit value. This works nicely as long as you pass unsigned values. It is bound to fail, if you need to pass signed values, because these need a sign extension.

What i have in mind is maybe some kind of macro which can be combined with "invoke"
invoke SendMessage handle, message, set64(32_bit_variable), set64_signed(32_bit_variable)
where "set64" zero extends "32_bit_variable" and "set64_signed" sign extends "32_bit_variable" in 64 bit code and does nothing in 32 bit code

JK

jj2007

Quote from: JK on December 28, 2022, 03:35:07 AM"set64_signed" sign extends "32_bit_variable" in 64 bit code

movsx rax, ax works; if ax=-1 -> rax=-1

Unfortunately, there is no movsx rax, eax :sad:

Here is an elegant workaround:
  and rax, 0 ; set to zero
  mov eax, -123 ; leaves the upper half of rax at zero
  or rax, 0ffffffff00000000h


Assembles fine with UAsm, but unfortunately it's not a valid instruction :sad:

Here is one that works correctly:
  and rax, 0 ; set to zero
  mov eax, -123 ; leaves the upper half of rax at zero
  .DATA
  Minus1 dq 0ffffffff00000000h
  .CODE
  or rax, Minus1

HSE

Equations in Assembly: SmplMath

jj2007

Quote from: HSE on December 28, 2022, 04:58:13 AM
Quote from: jj2007 on December 28, 2022, 04:39:34 AM
Unfortunately, there is no movsx rax, eax :sad:

movsxd rax, eax

Right, I missed that one :thumbsup:

However, cdqe does the same (for rax) and is one byte shorter. Speedwise they are identical.

Both are not listed in my 2008 instructions manual, which probably means they were introduced after 2008 :cool:

daydreamer

wndproc code can use hiword and loword macros,to part lParam into higher and lower part,so replace those macros to work in 64bit mode?
macros if you like use old x86 opcodes that is no more in x64
massive use of "search and replace" for DWORD to QWORD ?
my none asm creations
https://masm32.com/board/index.php?topic=6937.msg74303#msg74303
I am an Invoker
"An Invoker is a mage who specializes in the manipulation of raw and elemental energies."
Like SIMD coding