Intro to Win32: types, pointers and structures

Headers

  • #include <Windows.h> → Win32 core (includes constants, macros and data types)
  • #include <TlHelp32.h> → Functions such as CreateToolhelp32Snapshot Tlhelp32.h header - Win32 apps
  • #include <stdio.h> → printf(), scanf(), getchar(), putchar(), gets(), puts().

Type Names and Naming Conventions

Windows uses many type names that are usually aliases of C/C++ types but are easier to read.
Example: unsigned long, void*, wchar_t* → DWORD, PVOID, HANDLE, LPWSTR, SIZE_T
Full list: Windows Data Types (BaseTsd.h) - Win32 apps
The most common ones:
  • BYTE: 8-bit unsigned integer (0 to 255). Use: raw bytes, buffers, shellcode bytes, binary data
  • WORD: 16-bit unsigned integer (0 to 65,535). Use: small numeric values, flags, PE structure fields
  • DWORD: 32-bit unsigned integer ("Double WORD"). Use: flags, process IDs (PID), thread IDs (TID), error codes, sizes
  • LONG: 32-bit signed integer (can be negative). Use: signed values, status fields, structure members
  • ULONG: 32-bit unsigned integer ("Unsigned LONG"). Use: unsigned values, counters, flags
  • BOOL: Boolean stored as an integer (4 bytes). Use: return value of Win32 functions: TRUE on success, FALSE on failure
  • BOOLEAN: Boolean stored in a single byte. Use: low-level Windows structures and NT-style code (ntdll)
  • CHAR: 8-bit ANSI character. Use: ANSI strings and byte-sized data
  • WCHAR: 16-bit "wide" character ("Wide CHAR"). Use: UTF-16 Unicode strings, which is what Windows uses internally
  • LPSTR: Pointer to a modifiable ANSI string. Use: buffers where ANSI text is written
  • LPCSTR: Pointer to a constant (read-only) ANSI string. Use: ANSI strings that will not be modified
  • LPWSTR: Pointer to a modifiable Unicode string. Use: buffers where Unicode text is written
  • LPCWSTR: Pointer to a constant (read-only) Unicode string. Use: Unicode strings that will not be modified
  • PVOID: Generic pointer with no specific type (equivalent to void*). Use: memory addresses and generic buffers
  • LPVOID: Same as PVOID (generic pointer, void*). Use: memory addresses and generic buffers
  • HANDLE: An identifier Windows gives you to refer to an object. Use: files, processes, threads, snapshots, events, mutexes
  • HMODULE: Handle to a module loaded in memory. Use: reference to a loaded DLL or EXE (in practice, its base address)
  • SIZE_T: Unsigned integer large enough to represent memory sizes. Use: buffer sizes, allocation sizes, memory operations
  • DWORD_PTR: Pointer-sized unsigned integer. Use: storing values that must "grow" when moving from x86 to x64
  • ULONG_PTR: Pointer-sized unsigned integer. Use: pointer arithmetic and pointer-sized values
  • UINT_PTR: Pointer-sized unsigned integer. Use: pointer-sized unsigned values

Pointers & Pointer-Sized

They matter because on 32-bit pointers take 4 bytes (32 bits), and on 64-bit they take 8 bytes
⚠️ WRONG on 64-bit
c
DWORD value = (DWORD) pBuffer; // DWORD is 32 bits
✅ RIGHT on 64-bit and 32-bit
c
ULONG_PTR value = (ULONG_PTR)pBuffer;

Prefixes

Example: dwSize
  • dw → prefix meaning "it is a DWORD"
  • Size → what it represents: "a size"
  • b: Boolean or byte (BOOL, BOOLEAN, BYTE). E.g. bEnabled, bInheritHandle. Usually true/false, but check the real type
  • f: Flag or boolean condition (BOOL). E.g. fSuccess, fCreate. Indicates a yes/no condition
  • w: WORD. E.g. wVersion, wFlags. 16-bit unsigned integer
  • dw: DWORD. E.g. dwSize, dwFlags, dwProcessId. The most common in Win32
  • u: Unsigned integer (UINT). E.g. uExitCode, uMsg
  • ul: Unsigned long (ULONG). E.g. ulSize, ulFlags
  • n: Number or count (int). E.g. nSize, nIndex, nCmdShow. Plain integer
  • i: Index (int). E.g. i, iItem, iIndex. Loop counters, positions
  • cb: Count of Bytes: number of bytes. E.g. cbSize, cbBuffer. Key for buffers and memory allocations
  • cch: Count of Characters: number of characters. E.g. cchName, cchPath. Not the same as bytes (see below)
  • ch: A character (CHAR, WCHAR). E.g. chDrive. A single character
  • sz: String Zero: string terminated with \0. E.g. szName, szPath. Usually a character array
  • psz: Pointer to a \0-terminated string. E.g. pszName, pszPath
  • lpsz: Long pointer to a \0-terminated string. E.g. lpszFileName, lpszCommandLine. Older style, same as psz
  • p: Pointer. E.g. pBuffer, pInfo. Points to an object, buffer or structure
  • pp: Pointer to pointer. E.g. ppBuffer. When a function "returns" a pointer through a parameter
  • pv: Pointer to void (PVOID). E.g. pvBuffer, pvReserved. Generic pointer
  • ppv: Pointer to pointer to void (void**). E.g. ppvObject. Very common in COM
  • lp: Long pointer. E.g. lpBuffer, lpAddress. A 16-bit legacy; today it is the same as p
  • h: Handle (HANDLE, HMODULE, HWND, HKEY). E.g. hFile, hProcess, hThread
  • fn: Function or callback. E.g. fnCallback
  • pfn / lpfn: Function pointer. E.g. pfnCallback, lpfnWndProc. Callbacks, window procedures
  • rg: Array (range). E.g. rgHandles, rgData. Older style
  • rgsz: Array of strings. E.g. rgszNames. Uncommon
Prefixes combine, for example lpszFileName → lp (pointer) + sz (zero-terminated string) + FileName
The important detail: cb vs cch
It is the most typical source of bugs with Unicode strings. A WCHAR takes 2 bytes, so:
c
WCHAR szPath[260];
DWORD cchPath = 260;              // 260 characters
DWORD cbPath  = sizeof(szPath);   // 520 bytes

ANSI and Unicode Windows API

A for ANSI versions and W for Unicode versions
c
CreateFileA(...);
CreateFileW(...);
In modern code, Unicode (W) is preferred

Windows String Data Types

  • char / CHAR: An 8-bit (1 byte) ANSI character. E.g. CHAR chValue = 'A';
  • char* / PSTR / LPSTR: Pointer to a modifiable ANSI string. E.g. LPSTR lpString = Buffer;
  • const char* / PCSTR / LPCSTR: Pointer to a constant ANSI string. E.g. LPCSTR lpString = "notepad.exe";
  • wchar_t / WCHAR: A "wide" character. On Windows it is a 16-bit (2 byte) UTF-16 unit. E.g. WCHAR chValue = L'A';
  • wchar_t* / PWSTR / LPWSTR: Pointer to a modifiable Unicode string. E.g. LPWSTR lpString = Buffer;
  • const wchar_t* / PCWSTR / LPCWSTR: Pointer to a constant Unicode string. E.g. LPCWSTR lpString = L"notepad.exe";
  • TCHAR: "Generic" character: becomes WCHAR if UNICODE is defined, and CHAR if not. E.g. TCHAR chValue = TEXT('A');
  • LPTSTR: Pointer to a modifiable generic string → LPWSTR or LPSTR depending on UNICODE. E.g. LPTSTR lpString = Buffer;
  • LPCTSTR: Pointer to a constant generic string → LPCWSTR or LPCSTR depending on UNICODE. E.g. LPCTSTR lpString = TEXT("notepad.exe");
Equivalent functions, ANSI / Unicode:
  • strlen / wcslen: Length in characters
  • strcpy / wcscpy: Copy a string
  • strcmp / wcscmp: Compare strings
  • printf("%s") / wprintf(L"%ls"): Print

Pointers

A variable that stores a memory address
c
int Value = 10;
int* pValue = &Value;
  • Value: Name of the variable
  • pValue: Pointer to the integer
  • &Value : The memory address value
  • pValue : Stores that value
We use %p to print the memory address (0XDEADBEEF)
c
printf("Address of Value: %p\n", (void*)pValue);
With * we get the value stored at that memory address
c
printf("Value: %d\n", *pValue);
We can also modify the value
c
*pValue = 20;

printf("Value: %d\n", Value);
txt
Value: 20
They are used all over the Windows API, through aliases such as
PVOID, PBYTE, PDWORD, LPVOID, LPWSTR, and LPCWSTR
c
BYTE  Buffer[16] = { 0 };
PBYTE pBuffer    = Buffer;

pBuffer[0] = 0x41;   // 'A'
pBuffer[1] = 0x42;   // 'B'
pBuffer[2] = 0x43;   // 'C'
pBuffer points to the first byte of Buffer, 0x41
Generic pointers: PVOID
c
PVOID pAddress = Buffer; // A pointer that does not say what it holds (bytes, integers or a structure)
You cannot read or write directly through a PVOID; you have to state the data type
c
PVOID pAddress = Buffer;
PBYTE pBytes = (PBYTE)pAddress; // "treat this address as bytes"
printf("First byte: 0x%02X\n", pBytes[0]); // prints 0x41
This is very common on Windows: functions such as VirtualAlloc return a PVOID (untyped memory) and you decide how to interpret it.
NULL: "points to nothing"
c
int* pValue = NULL;
It is address 0
c
if (pValue != NULL)
{
    printf("%d\n", *pValue);   // only if it points to something
}
You see it a lot on Windows
c
PBYTE pBuffer = (PBYTE)VirtualAlloc(NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pBuffer == NULL)
{
    return FALSE;   // the allocation failed, stop here
}
GOOD HABITS
c
int* p = NULL;        // 1. Always initialise to NULL
...
free(p);
p = NULL;             // 2. After freeing, set it back to NULL

Structures

A way of grouping several related pieces of data into a single package
c
typedef struct _READER_INFO {
    int   Id;
    int   Age;
    char  Initial;
} READER_INFO, *PREADER_INFO;
  • _READER_INFO: The original "tag" of the structure. Analogy: the technical name of the mould
  • READER_INFO: An alias for the structure. Analogy: the handy name of the mould
  • PREADER_INFO: An alias for a pointer to the structure. Analogy: the address where a record card is
typedef is used to create aliases. Without it you would have to write struct _READER_INFO every time;
These two lines mean exactly the same:
c
struct _READER_INFO Card1;   // long form
READER_INFO         Card2;   // short form (the usual one)
Windows uses this style in all its headers: PROCESSENTRY32 / PPROCESSENTRY32, STARTUPINFOW / LPSTARTUPINFOW, etc.
Initialising: { 0 }
c
READER_INFO Card = { 0 };     // the card, with every field set to zero
PREADER_INFO pCard = NULL;    // a pointer that does not point to any card yet
Here is the key rule, and it is very simple:
  • If you have the structure, you use the dot .
  • If you have a pointer to the structure, you use the arrow ->
c
READER_INFO Card = { 0 };
Card.Id  = 1470;      // I have the card → dot
Card.Age = 34;

PREADER_INFO pCard = &Card;   // pCard points to Card
pCard->Id = 8765;             // I have a pointer → arrow

printf("%d\n", Card.Id);      // prints 8765, because pCard points to the same card
These two lines are identical:
c
pCard->Id = 8765;
(*pCard).Id = 8765;   // "go to where pCard points, and from that card take Id"