Linked lists sound simple. Most developers have seen the classic pattern: a structure holds some data, plus a next pointer and maybe a previous pointer, and that’s how the list is maintained.
Windows does something different.
In the kernel, and in the native API structures that are closely tied to it, linked lists are built around a small generic structure called LIST_ENTRY. You’ll find it everywhere: the active process list, thread lists, and the loader’s module lists in user space. Once you understand how that pattern works, a lot of Windows data structures become easier to read in a debugger and easier to work with in code.
Why Windows Uses A Different Pattern
The classic linked-list design is fine, but it has two practical weaknesses.
First, it is type-specific. If you write code to insert or remove a MY_DATA object from a list, that code is tied to MY_DATA. If you want to manage some other structure, you end up rewriting the same logic for a different type.
Second, it does not scale well when the same object needs to live in more than one list at the same time. Now you need another pair of pointers, then another, and the whole thing gets messy very quickly.
Windows solves that by separating the list mechanics from the data.
The Key Structure: LIST_ENTRY
LIST_ENTRY is just two pointers:
- forward link
- backward link
By itself, it looks useless. There is no payload. That is the point.
Instead of building the list around the whole data structure, Windows places a LIST_ENTRY inside the structure you want to manage. The list APIs only know about LIST_ENTRY. They do not need to know whether the owning structure is an EPROCESS, an ETHREAD, or an LDR_DATA_TABLE_ENTRY.
That makes the list code generic.
It also means the same structure can participate in multiple lists simply by containing multiple LIST_ENTRY members.
One Important Rule: These Lists Are Circular
This is the detail that trips people up first.
Windows linked lists using LIST_ENTRY are always circular. The list head is itself a LIST_ENTRY. It does not carry data; it just marks the list and links to the first and last entries.
That means:
- the end of the list is not indicated by
NULL - if you iterate until
NULL, you will keep going forever - iteration ends when you come back to the head
This is convenient because a separate tail pointer is not required. With a circular list, the head already gives you enough information to insert at the front or the back.
The Missing Piece: How Do You Get Back To The Real Object?
If all you have is a pointer to a LIST_ENTRY, that pointer is not necessarily the start of the owning structure. It points to the LIST_ENTRY member inside the next/previous structure.
So, how do you recover the actual object?
You move backward in memory by the offset of that member from the start of the structure.
Windows gives you a macro for exactly that:
CONTAINING_RECORD
That macro takes:
- the pointer you currently have
- the type you want
- the member name that contains the list link
From there, you get a properly typed pointer to the full structure.
This is the trick that makes generic list handling practical.
$1,478
$1182 or $120 X 10 payments
Windows Master Developer
Takes you from a “generic” C programmer to a master Windows programmer in user mode and kernel mode.
Kernel Example: The Active Process List
A very common example is the list of active processes.
The debugger can enumerate processes because the kernel keeps a list head in PsActiveProcessHead. That head points to LIST_ENTRY nodes stored inside EPROCESS objects.
The relevant member is:
ActiveProcessLinks
So if you look at the list entry directly, you are not looking at the beginning of an EPROCESS. You are looking at the embedded list node. To get back to the process object, you need the offset of ActiveProcessLinks and subtract it, or use the equivalent containing-record logic.
The same idea applies to thread lists inside a process. Again, the list itself is generic; the owning structure changes.
User-Mode Example: The Loader’s Module Lists
The same pattern appears in user mode.
Inside the PEB, the loader data (PEB_LDR_DATA) maintains module lists. In practice, you’ll find multiple lists that expose the same modules in different orders, such as load order and memory order.
Each entry is an LDR_DATA_TABLE_ENTRY, and it contains one or more LIST_ENTRY members that connect it to the list.
The iteration logic is exactly the same:
- start from the head’s forward link
- keep moving through
Flink - stop when you reach the head again
- use
CONTAINING_RECORDto get from the link back to the full loader entry
Once you do that, you can get details like module base address and full DLL name.
Why Windows Still Uses Linked Lists
Arrays and vectors are often faster because memory is contiguous. So why not use them everywhere?
Because these Windows objects are often large, frequently created and destroyed, and need cheap insertion and removal.
That matters for things like processes and threads. Removing an entry from a linked list can be constant time if you already have the node. Removing from a vector usually means shifting memory around, which is slower and more disruptive.
There is another issue: if other code holds pointers into those structures, moving memory is dangerous. Linked lists avoid that problem because each object stays where it is.
So while linked lists are not the best answer for everything, they make sense for many of the kernel’s dynamic object-tracking scenarios. In such a list, the head already gives you enough information to insert at the front or the back.
$1300
$1040 or $104 X 10 payments
Windows Internals Master
Broadens and deepens your understanding of the inner workings of Windows.
Why This Matters For TrainSec Students
This is one of those Windows patterns that keeps showing up.
If you understand LIST_ENTRY, circular list iteration, and CONTAINING_RECORD, you can read a lot more of what the kernel and native API are doing without treating every structure as a mystery.
That helps in several places:
- reading debugger output
- understanding object relationships in kernel structures
- writing code that traverses native Windows structures cleanly
Once this clicks, a lot of Windows internals start feeling less magical and more mechanical.
Liked the content?
Subscribe to the free TrainSec knowledge library, and get insider access to new content, discounts and additional materials.
Keep Learning with TrainSec
This content is part of the free TrainSec Knowledge Library, where students can deepen their understanding of Windows internals, malware analysis, and reverse engineering. Subscribe for free and continue learning with us:
https://trainsec.net/library






































