Over the past few months I have been developing my programming language, Squeak, which compiles Squeak files into first byte executable instructions for Windows systems.
Recently I have been adding support for x64 Windows in addition to x32 Windows. I ran into this frustrating behaviour while trying to calculate offsets for member variables in structs/pointer offsets and thought it might be helpful for others.
PROCESSENTRY32
The PROCESSENTRY32 struct is defined in tlhelp32.h
. The PROCESSENTRY32 structure has been reproduced from PROCESSENTRY32 structure (tlhelp32.h) below.
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID;
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
CHAR szExeFile[MAX_PATH];
} PROCESSENTRY32;
In x86 Windows, the size of this structure is as expected, 296
. This is broken down in the tables below.
Windows x86 (32 bit)
Information here comes from various sources, but the ‘guide’ can be found at the Windows documentation.
Type | Width |
DWORD | 0d04 |
ULONG_PTR | 0d04 |
LONG | 0d04 |
CHAR | 0d01 |
260
.This gives us the following layout:
Member | Type (0dWidth ) | Offset |
dwSize | DWORD (4 ) | 0d00 |
cntUsage | DWORD (4 ) | 0d04 |
th32ProcessID | DWORD (4 ) | 0d08 |
th32DefaultHeapID | ULONG_PTR (4 ) | 0d12 |
th32ModuleID | DWORD (4 ) | 0d16 |
cntThreads | DWORD (4 ) | 0d20 |
th32ParentProcessID | DWORD (4 ) | 0d24 |
pcPriClassBase | LONG (4 ) | 0d28 |
dwFlags | DWORD (4 ) | 0d32 |
szExeFile[MAX_PATH] | CHAR (260 ) | 0d36 |
This results in sizeof(PROCESSENTRY32)
being 296 as expected.
For Windows x64, however, we do not experience the same behaviour, even when we accommodate for the width of ULONG_PTR
.
Windows x64 (64 bit)
On Windows x64, we have similar sizes as with x86:
Type | Width |
DWORD | 0d04 |
ULONG_PTR | 0d08 |
LONG | 0d04 |
CHAR | 0d01 |
260
.The additional 4 bytes in the span of the ULONG_PTR
type would leave one to assume that the sizeof(PROCESSENTRY32)
in x64 would be 300. This is not correct however. For struct elements 0d08
bytes wide, they seem to need to be aligned. This results in Windows, and the x86_64-w64-mingw32-gcc
compiler inserting a wasted 0d04
bytes between th32ProcessID
and th32DefaultHeapID
and results in the sizeof(PROCESSENTRY32)
being 304 bytes. At first I thought this was due to the constraint of pointer types requiring 16 byte alignment for x64; but the mingw32 compiler family seems to apply this for all 0d08
byte wide member fields for unpacked structs.
In the screenshot below shows the PROCESSENTRY32
struct on x64 in x64dbg, shows the structure. Here, a process with PID 308
and PPID 480
has a total of 0d08
bytes of padding between the th32ProcessID
and th32DefaultHeapID
fields.
This was a painful experience for me, and I wish I understood it better at the onset as I expended about 8 hours of my day trying to figure out what was happening within this structure when calling it within assembly for my compiler. For the time being, I have adjusted the offset calculator within Squeak as follows:
if ( arch == ARCHITECTURE_WIN_64 ) {
if (SqType_Is_Pointer( sqObject->type) ){
if ( sqStruct->size % 16 != 0 ){
sqStruct->size += (16 - (sqStruct->size % 16));
}
}
}
Where the SqType_Is_Pointer
currently returns true for ‘pointer’ types. This may end up being required for any 0d08
byte wide field, who knows. If anyone has documentation that covers this, please let me know!
I’ve done a bit of further reading/research, turns out I could have found this information myself on Wikipedia. On Windows, 8 byte types will be 8 byte aligned. This has resulted in the following code:
if ( arch == ARCHITECTURE_WIN_64 ) {
unsigned int alignment = SQUEAKTYPEWIDTH( sqObject- >type, ARCHITECTURE_WIN_64);
if ( sqStruct->size % alignment != 0 ){
SqStruct->size += (alignment - (sqStruct->size % alignment));
}
}
Time will tell if it actually works in all cases.
Leave a Reply