Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to Intro to C on Windows. This book walks through topics in order. Your actual homework lives in one GitHub Classroom repository (see below)—you start from a base template with boilerplate and grow it chapter by chapter.

Important

GitHub Classroom

  1. Open the assignment: Accept the GitHub Classroom assignment.
  2. Sign in to GitHub if prompted, then accept the assignment so a personal repository is created for you.
  3. That repo is created from a base template project: it includes boilerplate (solution, project files, stubs, etc.) that you will keep using for the entire series.
  4. Watch for the invitation / acknowledgement email from GitHub (or your course) and complete any steps it asks for.
  5. Clone your Classroom repository to your machine (HTTPS or SSH, as your instructor prefers) and open that solution in Visual Studio for all labs.

Note

One project, growing over time

As you move through the chapters, add to the same template—new files, functions, and behavior stack on what you already wrote. You do not start a fresh project each week unless your instructor says otherwise. Chapter titles (e.g. “Pointers”, “The PEB and loader”) describe what you are learning next and what to implement inside your template next.

Tip

Submit work after each chapter

When you finish the code for a chapter (or whatever unit your instructor defines), commit your changes with a clear message (e.g. Complete Ch. 4 – lists and structs) and push to main (or the branch your instructor uses). That push is what signals “ready for review.” Push regularly so feedback can keep up with your progress—not only at the very end of the course.

Warning

The “Feedback” pull request

The Feedback pull request in your repo is for the instructor to leave comments and suggestions. Do not merge or close it. Keep implementing in your template on main (or the branch your instructor specifies), then commit and push; new commits on main are what get reviewed. The Feedback PR is a conversation surface, not a feature branch you complete or merge.

How this book works

  • Theory and guidance are written in each chapter: what the Windows APIs and C features are doing and why it matters.
  • Code in the book is what you study and reproduce: samples are complete enough to type in or adapt; they are not a second codebase you must hunt for elsewhere.
  • Your Classroom template holds the real project: you add .c / .h files, fill in main, split modules as directed, and wire everything to build.
  • At the end of each chapter, a short “Implement” section reminds you to build, test, commit, and push—the book does not assume you have a separate “solution repo” to copy from.

What you need

Note

VSCode in your browser

An attempt has been made to enable coding from your browser or even installing the GitHub Classroom VSCode extension. However, there is no guarantee that setup will work perfectly 100% of the time. The recommendation is to use VS2022 Community locally on a Windows host or Windows VM.

  • Windows and Visual Studio 2022 Community (or another build setup that can compile Windows C projects with the Windows SDK).
  • Many later labs use x64 only: several topics use __readgsqword to read the TEB/PEB and will fail to compile in 32-bit (x86) configurations by design.
  • Basic familiarity with a terminal, Git, and cloning/pushing to GitHub.

How this book is organized

Chapters proceed from numeric types and printf through pointers, structs, the module list, PE basics, threads and file search, and WinINet HTTP. Later optional material covers intrinsics and low-level patterns. The order matches how the course builds concepts—not a folder tree on disk.

Where things live

LocationPurpose
Your cloned Classroom repoThe base template you accepted from GitHub Classroom—this is where you edit, commit, and push all course work.
This book (IntroToCBook)Primary reference for explanations and code samples.
IntroToCBook/ (published site or repo)The rendered mdBook you read while coding.

Conventions

  • Long samples may be abbreviated with /* ... */ in the book; you still implement the full behavior in your template unless the chapter says otherwise.
  • Windows types such as DWORD, ULONG, PCHAR, and UNICODE_STRING come from Windows.h and related headers.
  • ERROR_SUCCESS is a common “success” return for Win32-style main examples.

Next chapter: Windows types—read the theory and samples, then implement them in your template, commit, and push when that stage is done.

Windows types, main, and printf

This chapter covers a minimal Windows C program, Win32 typedefs (INT, UINT, DWORD, …), and printing values with the correct printf format specifiers. Your Classroom template may already contain an empty or stub main—you will replace or extend it using the patterns here.

Note

No separate “lab solution” tree—everything you need to start is in this chapter plus whatever stubs your template provides.

The entry point: main

Every C program needs a place to start. On Windows, when you build a typical console application and link the C runtime library (CRT), the executable does not begin executing your main immediately: the CRT’s startup code runs first (stack, globals, security cookie work, and so on). When that setup is done, the CRT calls your main. That is why you write a function named main: it is the contract between your code and the CRT for this kind of app.

Function signature

The signature is the function’s return type, name, and parameter list—taken together, they tell the compiler how main can be called and what it returns.

Common forms you will see:

int main(void);

int main(int argc, char *argv[]);

In Windows headers, INT is a typedef for a signed integer type (typically the same width as int), and VOID means “no parameters,” so you will often see:

INT main(VOID)
{
    /* ... */
    return 0;
}

Your template or rubric may use int or INT; both describe the same idea when INT is typedef’d to match int.

Function body

The body is the block in { ... } after the signature. That is where you:

  • Declare variables
  • Call functions (e.g. printf)
  • Use control flow (if, while, …)
  • return a value to the caller

For main, the return value is passed back through the CRT to the operating system: by convention, 0 means success; non-zero often signals an error (your course may standardize on return 0 or return ERROR_SUCCESS from Windows.h).

Why main (and not something arbitrary)?

The linker + CRT are built to look for a symbol with a specific name for the C console entry path—traditionally main (or wmain for wide-character argv). If you rename it to start_here without also changing startup objects and linker settings, the link step will fail or you will get the wrong entry point. WinMain is a different story: that is the usual entry for Windows subsystem GUI programs, with the CRT calling WinMain instead of main. For this course’s console-style labs, main is the right name.


Variables that hold numbers

Before you print or combine values, you store them in variables: a name, a type, and (usually) an initial value.

On Windows, Windows.h (and related headers) provide typedefs so the same names appear in sample code and in system APIs:

Typedef (typical use)Role
INTSigned integer (often 32-bit); good for values that can be negative.
UINTUnsigned integer; counts and bit patterns that should not be negative.
LONG, ULONGSigned / unsigned long-width integers.
DWORDUnsigned 32-bit type (“double word”); very common in Win32 APIs.

Example declarations with initializers:

INT   courseNumber = 670;
UINT  flags        = 0U;
DWORD mask         = 0x0000FFFFUL;

The type tells the compiler how much storage to use and how to interpret the bits (especially important for signed vs unsigned). The value after = sets the starting contents; you can change them later with assignment (=, +=, …).

sizeof: how big is this type or variable?

C provides the sizeof operator. It yields the size in bytes of a type or of an expression’s type. You use it when the correct amount of memory matters—buffers, clearing structs, or matching what an API expects—instead of hard-coding numbers like 4 that might be wrong on another platform or for another type.

On a type (parentheses required around the type name):

size_t n = sizeof(UINT);   /* how many bytes one UINT occupies */

On a variable or expression (parentheses around the variable are optional but common):

DWORD mask = 0xFF00FF00UL;
printf("mask uses %zu bytes\n", sizeof mask);

sizeof is evaluated at compile time for normal types (not counting run-time variable values—only the type). The result has type size_t; use %zu with printf to print it portably.

Why bother?

  • Clarity: sizeof(buffer) documents “all of this object,” not a magic constant.
  • Safety: If you change a variable’s type later, sizeof updates automatically.
  • APIs: Many functions want a size in bytes; passing sizeof avoids off-by-one errors when paired with the right object.

Tip

sizeof does not tell you the length of a string in characters—use strlen for that. sizeof(char_array) includes the '\0' if the array was declared with fixed size; strlen does not.


How many bytes? Overflow and wrapping

Exact byte widths are implementation-defined, but on Visual Studio / MSVC for typical Windows x64 console programs, the Win32 typedefs you use early in this course are almost always 32-bit (4 bytes) for integers like INT, UINT, DWORD, LONG, and ULONG. Wider types exist for 64-bit values (e.g. LONG64, ULONG64, DWORD64) and are 8 bytes.

IdeaTypical size (MSVC, x64 labs in this course)
CHAR1 byte
SHORT / USHORT2 bytes
INT / UINT / DWORD / LONG / ULONG4 bytes
LONG64 / ULONG64 / DWORD648 bytes
Pointers (PCHAR, void *, …)8 bytes on x64, 4 on x86

To verify on your machine, print them once in a scratch main:

printf("sizeof(INT)=%zu, sizeof(DWORD)=%zu, sizeof(void*)=%zu\n",
    sizeof(INT), sizeof(DWORD), sizeof(void *));

What if the value does not fit?

Each unsigned integer type can only hold 0 .. 2^(8×N) − 1 for N bytes. If you assign or compute a value that is too large:

  • Unsigned types: the C standard defines wrapping (reduction modulo 2^(8×N)). The value “folds” and you keep only the low bits—silently. Example: putting 300 into an unsigned char (often 1 byte) does not store 300; it stores 300 % 256.

  • Signed types: overflow in arithmetic (e.g. INT_MAX + 1 in a pure int expression) is undefined behavior in C—you cannot rely on it wrapping like two’s-complement hardware might. Compilers may assume it never happens and optimize in surprising ways.

Practical takeaway: pick a type wide enough for the range you need (DWORD vs DWORD64, etc.), check sizes with sizeof when writing to buffers, and be careful mixing signed and unsigned in expressions (see the next section).


Signed vs unsigned—and mixing them

Signed types can represent negative numbers; unsigned types treat the same storage as non-negative only. If you assign or arithmetic-mix signed and unsigned values, C’s usual arithmetic conversions can widen or reinterpret values in ways that surprise you. Always know which type each variable has before you combine them.

Here is a small program that mixes INT and UINT and then prints the results. An INT is pushed past its maximum positive value by adding a UINT, and a UINT is decremented:

INT mix = 0x7FFFFFFF;
UINT match = 1U;
mix += match;
match -= 2;

printf("mix:   (hex)[0x%x] (signed)[%i] (unsigned)[%u] \n", mix, mix, mix);
printf("match: (hex)[0x%x] (signed)[%i] (unsigned)[%u] \n", match, match, match);

The same bits in a variable can be printed three ways: %x (hex), %i (signed decimal), %u (unsigned decimal). The format specifier tells printf how to interpret the value you pass—not the other way around. Passing the wrong combination of type and format is undefined behavior or misleading output. That is why choosing the right type (INT, UINT, DWORD, …) and the matching format matters.


More arithmetic: compound assignment and bit shifts

Extend main with:

  • More variables of types like INT, UINT, DWORD with given initial values (including hex constants and expressions like 1 << 12).
  • Additional printf lines with correct specifiers for hex vs decimal.
  • Compound assignment, e.g. updating an INT with a UINT (mix += something).

Bit shift examples:

DWORD forty96 = 1 << 12;  /* 4096 */
DWORD ten24   = 1 << 10;  /* 1024 */

printf format cheat sheet (MSVC / Windows)

Use this table while you choose specifiers (see also Microsoft printf format specification)):

/*
 * Common conversions:
 *   %d or %i   signed decimal
 *   %u         unsigned decimal
 *   %o         unsigned octal
 *   %x / %X    unsigned hex (lower/upper)
 *
 * Size / width hints (examples):
 *   %lld       long long
 *   %I32d      32-bit int (MSVC)
 *   %I64d      64-bit int (MSVC)
 *   %zu        size_t (C99; use for strlen result, etc.)
 */

Tip

If output looks wrong, check both the variable type and the format specifier. Mismatches (e.g. printing a DWORD with the wrong size or signedness) cause confusing output or undefined behavior.

Implement

  1. In your Classroom template, ensure main has the correct signature and body for a console CRT app, then add the numeric variables, the signed/unsigned demo, and any bit-shift / printf work your rubric requires.
  2. Build (correct platform: usually x64 for later chapters, but this chapter may work in either—follow your template).
  3. Run and verify the printed hex/signed/unsigned lines match your expectations.
  4. Commit and push when this chapter’s goals are met.

Pointers and addresses

This chapter covers taking addresses, dereferencing, pointer sizes, and how pointer arithmetic depends on the pointed-to type. It ends with parallel arrays of names and function pointers—a simplified picture of an export table.

Address-of and dereference

ULONG ulCourse = 670UL;
PULONG pCourse = &ulCourse;

printf("1-address of ulCourse 0x%p \n", (void*)&ulCourse);
printf("2-address of ulCourse 0x%p \n", (void*)pCourse);
printf("3-address of pCourse 0x%p \n", (void*)&pCourse);
printf("4-dereference pCourse 0x%08lx \n", (unsigned long)*pCourse);

*pCourse = 665;

pCourse holds the address of ulCourse; *pCourse reads or writes the ULONG at that address.

Pointer size vs pointee size

printf("pointers, always the same size : %zu, %zu, %zu, %zu \n",
    sizeof(PCHAR), sizeof(PULONG), sizeof(PWORD), sizeof(LPSTR));
printf("what they point to is NOT always the same size : %zu, %zu, %zu, %zu \n",
    sizeof(CHAR), sizeof(ULONG), sizeof(WORD), sizeof(CHAR));

On a given platform, all data pointers are typically the same width (e.g. 8 bytes on x64), but sizeof(*p) depends on the type p points to.

Pointer arithmetic and CHAR vs WORD vs ULONG

The same underlying bytes (e.g. a string "yoda") can be walked with different pointer types; incrementing advances by sizeof(*p):

  • PCHAR / LPSTR: +1 byte
  • PWORD: +2 bytes
  • PULONG: +4 bytes

Then walk a C string byte-by-byte:

PCHAR p = jediMaster;
while ('\0' != *p)
{
    printf("char: %c, address : 0x%p \n", *p, (void*)p);
    p++;
}

Warning

Casting a PCHAR to PULONG or PWORD and then incrementing is unsafe unless you know the buffer is large enough and aligned for that access. This exercise illustrates sizes and arithmetic, not production string code.

Parallel arrays (names and function pointers)

Finish with arrays of names and PVOID (or typed) function pointers, then find an index with strcmp—the same logical shape as names / ordinals / functions in a PE export directory.

/* Example shape only—adjust types and count for your rubric */
#define COUNTOF_NAMES 4
PCHAR AddressOfNames[COUNTOF_NAMES] = { "MyA", "MyB", "MyC", "MyD" };
PVOID AddressOfFunctions[COUNTOF_NAMES] = { /* &MyA, &MyB, ... */ };

for (ULONG i = 0UL; i < COUNTOF_NAMES; i++)
{
    if (0 == strcmp("MyC", AddressOfNames[i]))
    {
        printf("found at index %lu @ %p \n", i, AddressOfFunctions[i]);
    }
}

Implement

  1. In your template, implement the pointer demos, the unsafe multi-type walk only if your rubric requires it, and the parallel-array lookup with real stub functions.
  2. Build and run.
  3. Commit and push.

Structs, UNICODE_STRING, and doubly linked lists

This chapter covers Windows-style structs, designated initializers, allocation from the process heap, and linking records with LIST_ENTRY (doubly linked list), the same abstraction the loader uses for modules.

Struct types to model

Define types similar to kernel patterns (names may vary; match your rubric):

  • UNICODE_STRINGLength, MaximumLength, Buffer.
  • STRING_EX (optional) — narrow or wide buffer depending on whether you compile with UNICODE.
  • A record (e.g. PROCESS_INFO_EX) that embeds a LIST_ENTRY (e.g. ProcessInfoLinks) plus a module or process name.

Initializing structs

Examples:

STRING_EX Name2 = { 0 };
RtlSecureZeroMemory(&Name3, sizeof(STRING_EX));

STRING_EX Name4 = { .Buffer = s, .MaximumLength = sizeof(s), .Length = sizeof(s) - sizeof((s)[0]) };

Use an RTL_CONSTANT_STRING-style macro for string literals if your instructor provides or asks you to write one.

Heap allocation and list operations

Allocate a list head with HeapAlloc / GetProcessHeap, check for NULL, then:

InitializeListHead(TempHeadList);
bIsEmpty = IsListEmpty(TempHeadList);

Allocate a node struct, fill fields (including a UNICODE_STRING from RTL_CONSTANT_STRING or manual setup), then insert:

InsertTailList(TempHeadList, &(pProcInfo->ProcessInfoLinks));

List helper routines

You need InitializeListHead, IsListEmpty, InsertTailList, RemoveEntryList, and related routines—the same operations the Windows loader uses for module lists. Your template may already ship ListHelpers.h; if not, implement these FORCEINLINE-style functions in your own header (match the classic doubly linked list algorithms).

Note

Printing a UNICODE_STRING with printf may require a vendor-specific format (e.g. %wZ) or manual loops over Buffer. If your build complains, print Length / Buffer in a simple loop for debugging.

Implement

  1. Add the struct definitions and list helpers to headers/sources in your template.
  2. In main, allocate head + at least one node, insert, and verify IsListEmpty toggles as expected.
  3. Build, run, commit, and push.

The PEB, loader data, and module lists

This chapter covers reading the TEB/PEB on x64, reaching PEB->Ldr, and walking InMemoryOrderModuleList with CONTAINING_RECORD.

Warning

This topic requires x64. Use __readgsqword(FIELD_OFFSET(TEB, ProcessEnvironmentBlock)) and reject x86 builds with #error if your rubric says so. Always select an x64 configuration in Visual Studio.

Get a pointer to the PEB

PPEB PebPtr = (PPEB)__readgsqword(FIELD_OFFSET(TEB, ProcessEnvironmentBlock));
PPEB_LDR_DATA LdrDataPtr = (PPEB_LDR_DATA)PebPtr->Ldr;

You will need winternl.h / ntddk.h-style types your template exposes (TEB, PEB, PEB_LDR_DATA, LDR_DATA_TABLE_ENTRY, etc.)—often via a single NT headers include your instructor supplies.

Walk InMemoryOrderModuleList

The list is circular; stop when CurrentModule == HeadList:

PLIST_ENTRY HeadList = &(LdrDataPtr->InMemoryOrderModuleList);
PLIST_ENTRY CurrentModule = HeadList->Flink;

while (CurrentModule != HeadList)
{
    PLDR_DATA_TABLE_ENTRY LdrDataEntry =
        CONTAINING_RECORD(CurrentModule, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
    /* e.g. wprintf(L"%wZ @ %p\n", LdrDataEntry->BaseDllName, LdrDataEntry->DllBase); */
    CurrentModule = CurrentModule->Flink;
}

CONTAINING_RECORD converts a pointer to an embedded LIST_ENTRY into a pointer to the containing LDR_DATA_TABLE_ENTRY.

Optional: hashing and the loader hash table

Advanced follow-on (if assigned): implement LdrpHashUnicodeString-style hashing, LdrHashEntry, and logic that takes the loader lock, walks initialization-order modules, and locates the hash bucket array. Treat this as a second milestone after the basic InMemoryOrder walk works.

Tip

Loader data can change under you—other threads load DLLs. Any serious walk should use LoaderLock (EnterCriticalSection / LeaveCriticalSection) the way your rubric describes.

Implement

  1. In your template, implement PEB → Ldr → InMemoryOrder walk and print each module’s base and name (as your instructor specifies).
  2. Add the optional hash-table exploration only if assigned.
  3. Build x64, run, commit, and push.

Locating modules by name or hash and reading PE headers

This chapter covers finding a loaded module’s base address from the PEB module list, then parsing PE structures in memory (DOS header, NT headers, optional header, export directory).

FindSystemModule-style API

Design a function (name it as your rubric says) that accepts optional UNICODE_STRING* and/or ULONG* hash:

  • If name is NULL, a hash must be supplied; otherwise return ERROR_INVALID_PARAMETER (or equivalent).
  • Set an internal bHashOnly flag and call into GetModuleBase.

GetModuleBase and the loader lock

GetModuleBase should:

  1. Resolve the PEB (e.g. GetPebPtr using __readgsqword / TEB).
  2. EnterCriticalSection(PebPtr->LoaderLock) before reading loader lists.
  3. Walk InMemoryOrderModuleList, comparing either:
    • BaseNameHashValue to the supplied hash (RtlCompareMemory), or
    • _wcsicmp on wide name buffers when matching by name.
  4. LeaveCriticalSection on every path.

Important

Always release the loader lock on all exit paths. Early returns during debugging must not skip LeaveCriticalSection or you can deadlock the process.

PE parsing overview (in-memory)

After you have ModuleBase:

  • Validate IMAGE_DOS_SIGNATURE at the base (DOS header).
  • Use e_lfanew to locate the NT headers.
  • Read the file header and optional header (32 vs 64 depends on Machine).
  • Locate the export directory via IMAGE_DIRECTORY_ENTRY_EXPORT and convert RVAs to VAs with a macro such as:
#define RVA2VA(Type, Base, Rva) ((Type)((DWORD_PTR)(Base) + (DWORD_PTR)(Rva)))

Add DumpDosHeader, DumpFileHeader, DumpExports (or similar) as separate functions that print fields you care about. Link imagehlp.lib if you use ImageHlp helpers (ImageRvaToVa, etc.).

Second pass (optional reinforcement)

If your course assigns a second milestone with the same APIs (e.g. hash-only lookup vs name lookup), treat it as practice—same lock rules, same PE steps—rather than a different codebase.

Implement

  1. Implement lookup by name and/or by hash per rubric.
  2. Parse and print DOS + file headers (and exports if required).
  3. Build x64, test against a known module (e.g. KERNELBASE.dll), commit, and push.

Threads, heap args, and directory enumeration

This chapter covers passing a structure to a worker thread with CreateThread, and enumerating files with FindFirstFileA / FindNextFileA / FindClose.

Passing a parameter to CreateThread

Allocate a struct (e.g. ARGS) on the heap, fill fields such as a search pattern, pass the pointer as lpParameter, then wait and clean up:

typedef struct _ARGS {
    CHAR fileName[MAX_PATH];
} ARGS, *PARGS;

PARGS pArgs = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ARGS));
if (!pArgs)
    return GetLastError();

memcpy(pArgs->fileName, "C:\\Users\\*", strlen("C:\\Users\\*") + 1);

HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)DirSearch, pArgs, 0, NULL);
if (!hThread) {
    HeapFree(GetProcessHeap(), 0, pArgs);
    return GetLastError();
}

WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
HeapFree(GetProcessHeap(), 0, pArgs);

Warning

Casting DirSearch to LPTHREAD_START_ROUTINE only works if DirSearch uses the correct return type and calling convention (DWORD WINAPI ThreadProc(LPVOID)). Mismatches cause stack corruption.

Thread procedure: FindFirstFile loop

Inside DirSearch (or your name):

  • If lpParam is non-NULL, read ((PARGS)lpParam)->fileName (or use a default pattern).
  • FindFirstFileA → check INVALID_HANDLE_VALUEGetLastError on failure.
  • dowhile (FindNextFileA(...)).
  • FileTimeToSystemTime for display.
  • Combine nFileSizeHigh / nFileSizeLow with LARGE_INTEGER for 64-bit size.
  • Skip . and ..; branch on FILE_ATTRIBUTE_DIRECTORY.
  • FindClose before return.

Note

Pick a directory you are allowed to scan. C:\* or C:\Users\* are teaching examples only.

Implement

  1. Define ARGS (or use the template’s typedef), implement DirSearch in its own .c file if your rubric splits modules.
  2. CreateThread from main, wait, free heap.
  3. Build, run, commit, and push.

HTTP with WinINet

This chapter covers a minimal HTTP GET using WinINet (wininet.dll), plus readable errors via FormatMessageW pulling messages from wininet.dll.

Link Wininet.lib and include Wininet.h.

Flow in main

Typical sequence:

  1. InitSessionInternetOpenA with a user agent and INTERNET_OPEN_TYPE_PRECONFIG.
  2. MakeRequestInternetConnectA, then HttpOpenRequestA (and related) to obtain an HINTERNET request handle.
  3. SendRequest — send headers/body; on failure, call your error helper.
  4. ProcessResultsInternetReadFile (or similar) in a loop until done.
  5. CloseSessionInternetCloseHandle for each open handle (request, connection, session).

Split these across .c files in your template (HttpApis.c, Errors.c, etc.) if your rubric asks for separation—the book describes the pattern; your repo holds the actual file names.

Formatting WinINet errors

#define BUFFER_SIZE 1024

void CheckLastInetError(LPCWSTR message, DWORD dwLastErr)
{
    WCHAR buf[BUFFER_SIZE];
    DWORD n = FormatMessageW(
        FORMAT_MESSAGE_FROM_HMODULE,
        GetModuleHandleW(L"wininet.dll"),
        dwLastErr,
        0UL,
        buf,
        BUFFER_SIZE,
        NULL
    );
    if (n)
        wprintf(L"%s: %s", message, buf);
}

Tip

Distinguish Win32 errors from InternetError / extended WinINet codes your debugger shows—always thread GetLastError (or the API’s documented error path) into FormatMessage with the right module when possible.

Note

Samples often target 127.0.0.1:8080. Run a local listener or change host / port to match your lab.

Implement

  1. Add WinINet session/connect/request/send/read/close in your template.
  2. Wire CheckLastInetError (or equivalent) for failed steps.
  3. Build, test against a real or mock server, commit, and push.

Unicode vs ANSI builds

This short chapter explains how UNICODE / _UNICODE affect which printf family you use—and how easy it is to mix wide and narrow APIs by mistake.

One macro, two personalities

#ifdef _UNICODE
#define print wprintf
#else
#define print printf
#endif

int main(void)
{
    print(L"this is a cool \n");  /* BUG if _UNICODE is not defined: printf + wide literal */
    return 0;
}

When _UNICODE is defined, print becomes wprintf and the wide literal L"..." is consistent. When it is not defined, print is printf but the string is still wide—that is a bug.

Warning

Do not mix wide string literals with printf, or narrow literals with wprintf, without explicit conversion. Pick one story for your template: A narrow (CHAR, printf, *A APIs) or B wide (wchar_t, wprintf, *W APIs, UNICODE_STRING).

Why this matters

Later topics use CHAR and FindFirstFileA alongside UNICODE_STRING and wide system types. Your project properties and #ifdef UNICODE blocks should match—avoid “half Unicode” builds.

Implement

  1. Align your template with your instructor’s required configuration (ANSI vs Unicode).
  2. Fix or remove any print macro that mixes families incorrectly.
  3. Build, verify, commit, and push.

SIMD intrinsics, hashing, and low-level extras

This chapter is optional and more advanced than the core track. Topics include SSE-style hashing of wide strings, SIMD XOR of buffers, and (if assigned) shellcode or raw bytes embedded in C—only in environments your instructor authorizes.

Warning

These examples use SSE/AVX intrinsics (immintrin.h, emmintrin.h, etc.). You need CPU + compiler support for the instruction set you use, and you must understand alignment and strict aliasing before using intrinsics in real code.

XOR-fold hash with SSE (wide string)

Pattern:

  1. Initialize __m128i hash to zero.
  2. For each 8-WCHAR chunk (16 bytes), _mm_loadu_si128, _mm_xor_si128 into hash.
  3. _mm_storeu_si128 to a DWORD[4], XOR the four lanes into one DWORD.
  4. XOR remaining WCHARs in a scalar loop.

Build a UNICODE_STRING for a module name using a wide char array if you need per-character control (e.g. 'K','E',...,'L','L', UNICODE_NULL).

IntrinsicXOR: 16-byte blocks

Load a 16-byte key into __m128i. For each full block of plaintext: _mm_loadu_si128, _mm_xor_si128, _mm_storeu_si128. Handle a tail smaller than 16 bytes with a byte loop. Print ciphertext as hex for debugging.

Shellcode / hexData arrays (assigned only)

Some courses embed machine code as `unsigned char hexData[] = { 0x48, … }; and jump into it or study it in a debugger. That belongs only in controlled lab VMs.

Caution

Running or adapting shellcode and low-level process memory tricks can violate policy or law outside an authorized class environment. Follow your instructor’s rules only.

Placeholder milestones

If your template includes a stub function (e.g. a single boo()), replace it when the rubric advances—or use it as a scratch entry point for intrinsic experiments.

Implement

  1. Only if assigned: add SIMD hashing and/or XOR demo .c files to your template, enable required compiler intrinsics flags.
  2. Build, run under a debugger if asked, commit, and push.

CRT-free DLL (optional)

Some courses end with a DLL that minimizes or avoids the C runtime, walks InMemoryOrderModuleList, and compares hashes—similar ideas to earlier chapters but with DllMain constraints.

Note

This is supplementary. Building DLLs involves exports, DllMain, loader lock re-entrancy, and special linker settings. Follow your instructor’s template and checklist.

Warning

Calling LoadLibrary-style APIs from DllMain while holding the loader lock can deadlock. Do not copy PEB-walking samples into DllMain without understanding what your init path calls.

What to implement (if assigned)

  • A DllMain that does minimal work (or defers heavy work per instructor guidance).
  • Optional export boo or similar that performs module enumeration / hash match outside dangerous lock contexts.
  • Linker settings and CRT avoidance exactly as the Classroom boilerplate specifies.

Implement

  1. Use only the DLL project and settings from your Classroom template.
  2. Build the DLL, load/test per rubric, commit, and push.