Repository: cilium/ebpf
Pull request #1710 (2025-03-04 15:26:03)

ebpf: add Windows support
Basic support for the `ebpf` package. `link` doesn't work yet, so there is no way to attach a link at the moment.

all: add windows constants

    Add constants for Windows map and program types as well as builtin
    functions.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>

ebpf: add basic Windows support

    Add minimal support for Windows, which includes being able to create maps, 
    load programs and interact with them via the usual APIs.

    Unsupported APIs return ErrNotSupported or ErrNotSupportedOnOS.

    Signed-off-by: Lorenz Bauer <lmb@isovalent.com>


lmb: Here is a summary of skipped tests, and their reason for skipping:

```
=== Skipped
=== SKIP: . TestMapReplacementsDataSections (0.01s)
    helpers_test.go:195: field ROData: map .rodata: map create: flags: not supported on windows

=== SKIP: . TestLoadCollectionSpec/loader-clang-11-el.elf (0.00s)
    elf_reader_test.go:261: map array_of_hash_map: inner map: map create: flags: not supported on windows

=== SKIP: . TestLoadCollectionSpec/loader-clang-14-el.elf (0.00s)
    elf_reader_test.go:261: map hash_map: map create: flags: not supported on windows

=== SKIP: . TestLoadCollectionSpec/loader-clang-17-el.elf (0.00s)
    elf_reader_test.go:261: map hash_map: map create: flags: not supported on windows

=== SKIP: . TestLoadCollectionSpec/loader-el.elf (0.00s)
    elf_reader_test.go:261: map perf_event_array: map type PerfEventArray (linux): not supported on windows

=== SKIP: . TestDataSections (0.00s)
    elf_reader_test.go:301: data_sections:
        	  ; __section("socket/3") int data_sections() {
        	 0: MovImm dst: r0 imm: 141
        	  ; if (uneg != (unsigned int)-1)
        	 1: LoadMapValue dst: r1, fd: 0 off: 8 <.rodata>
        	 3: LdXMemW dst: r1 src: r1 off: 0 imm: 0
        	 4: LdImmDW dst: r2 imm: 4294967295
        	  ; if (uneg != (unsigned int)-1)
        	 6: JNEReg dst: r1 off: 22 src: r2
        	 7: MovImm dst: r0 imm: 144
        	  ; if (neg != -2)
        	 8: LoadMapValue dst: r1, fd: 0 off: 12 <.rodata>
        	10: LdXMemW dst: r1 src: r1 off: 0 imm: 0
        	11: LdImmDW dst: r2 imm: 4294967294
        	  ; if (neg != -2)
        	13: JNEReg dst: r1 off: 15 src: r2
        	14: MovImm dst: r0 imm: 147
        	  ; if (static_uneg != (unsigned int)-3)
        	15: LoadMapValue dst: r1, fd: 0 off: 16 <.rodata>
        	17: LdXMemW dst: r1 src: r1 off: 0 imm: 0
        	18: LdImmDW dst: r2 imm: 4294967293
        	  ; if (static_uneg != (unsigned int)-3)
        	20: JNEReg dst: r1 off: 8 src: r2
        	  ; if (static_neg != -4)
        	21: LoadMapValue dst: r1, fd: 0 off: 20 <.rodata>
        	23: LdXMemW dst: r1 src: r1 off: 0 imm: 0
        	24: MovImm dst: r0 imm: 0
        	25: LdImmDW dst: r2 imm: 4294967292
        	27: JEqReg dst: r1 off: 1 src: r2
        	28: MovImm dst: r0 imm: 150
        	  ; }
        	29: Exit
        
    helpers_test.go:195: field Program: program data_sections: map .rodata: map create: flags: not supported on windows

=== SKIP: . TestFreezeRodata (0.00s)
    elf_reader_test.go:346: Ignoring version constraint 5.9 for sk_lookup program type on windows
    helpers_test.go:195: field Program: program freeze_rodata: map .rodata: map create: flags: not supported on windows

=== SKIP: . TestLoadInitializedBTFMap/btf_map_init-eb.elf/NewCollection (0.00s)
    elf_reader_test.go:435: Skipping BigEndian collection

=== SKIP: . TestStringSection (0.00s)
    elf_reader_test.go:528: my_map
    elf_reader_test.go:528: .rodata.str1.1
    elf_reader_test.go:544: map .rodata.str1.1: map create: flags: not supported on windows

=== SKIP: . TestLoadRawTracepoint (0.00s)
    elf_reader_test.go:579: Ignoring version constraint 4.17 for BPF_RAW_TRACEPOINT API on windows
    elf_reader_test.go:592: program sched_process_exec: program type RawTracepoint (linux): not supported on windows

=== SKIP: . TestKconfig (0.00s)
    elf_reader_test.go:648: field Main: program kconfig: program type SocketFilter (linux): not supported on windows

=== SKIP: . TestKsym (0.00s)
    elf_reader_test.go:674: populating kallsyms caches: getting addresses from kallsyms: read /proc/kallsyms: not supported on windows

=== SKIP: . TestKsymWeakMissing (0.00s)
    elf_reader_test.go:710: populating kallsyms caches: getting addresses from kallsyms: read /proc/kallsyms: not supported on windows

=== SKIP: . TestKfunc (0.00s)
    elf_reader_test.go:721: Ignoring version constraint 5.18 for kfunc support on windows
    elf_reader_test.go:733: populating kallsyms caches: getting modules from kallsyms: read /proc/kallsyms: not supported on windows

=== SKIP: . TestWeakKfunc (0.00s)
    elf_reader_test.go:751: Ignoring version constraint 5.18 for kfunc support on windows
    elf_reader_test.go:764: populating kallsyms caches: getting modules from kallsyms: read /proc/kallsyms: not supported on windows

=== SKIP: . TestInvalidKfunc (0.00s)
    elf_reader_test.go:773: Ignoring version constraint 5.18 for kfunc support on windows
    elf_reader_test.go:774: Ignoring version constraint 5.11 for bpf_testmod on windows
    elf_reader_test.go:774: bpf_testmod not loaded

=== SKIP: . TestKfuncKmod (0.00s)
    elf_reader_test.go:790: Ignoring version constraint 5.18 for Kernel module function calls on windows
    elf_reader_test.go:791: Ignoring version constraint 5.11 for bpf_testmod on windows
    elf_reader_test.go:791: bpf_testmod not loaded

=== SKIP: . TestSubprogRelocation (0.00s)
    elf_reader_test.go:822: Ignoring version constraint 5.13 for bpf_for_each_map_elem on windows
    elf_reader_test.go:835: field Main: program fp_relocation: instruction 16: function FnForEachMapElem (linux): not supported on windows

=== SKIP: . TestIPRoute2Compat (0.00s)
    elf_reader_test.go:923: map hash_map: map type Hash (linux): not supported on windows

=== SKIP: . TestLibBPFCompat (0.00s)
    elf_reader_test.go:938: No path specified

=== SKIP: . TestWindowsELFCompat (0.00s)
    elf_reader_test.go:1331: No path specified

=== SKIP: . TestMapInfoFromProc (0.00s)
    info_test.go:46: creating map: map create: flags: not supported on windows

=== SKIP: . TestMapInfoFromProcOuterMap (0.00s)
    info_test.go:81: read fdinfo: not supported on windows

=== SKIP: . TestProgramInfoProc (0.00s)
    info_test.go:165: read fdinfo: not supported on windows

=== SKIP: . TestStats (0.00s)
    info_test.go:304: Ignoring version constraint 5.8 for BPF_ENABLE_STATS on windows
    info_test.go:338: failed to enable stats: not supported on windows

=== SKIP: . TestProgInfoExtBTF (0.00s)
    info_test.go:446: Ignoring version constraint 5.0 for Program BTF (func/line_info) on windows
    info_test.go:457: field Main: program xdp_prog: map hash_map: map create: flags: not supported on windows

=== SKIP: . TestMapBatch/Array (0.00s)
    map_test.go:139: map batch api: not supported on windows

=== SKIP: . TestMapBatch/PerCPUArray (0.00s)
    map_test.go:133: Ignoring version constraint 5.13 for batched ops support for percpu array on windows
    map_test.go:139: map batch api: not supported on windows

=== SKIP: . TestMapBatch/Hash (0.00s)
    map_test.go:164: map batch api: not supported on windows

=== SKIP: . TestMapBatch/PerCPUHash (0.00s)
    map_test.go:164: map batch api: not supported on windows

=== SKIP: . TestMapBatchCursorReuse (0.00s)
    map_test.go:211: map batch lookup: map batch api: not supported on windows

=== SKIP: . TestBatchAPIMapDelete (0.00s)
    map_test.go:242: batch api not available: map batch api: not supported on windows

=== SKIP: . TestBatchMapWithLock (0.00s)
    map_test.go:298: Ignoring version constraint 5.13 for MAP BATCH BPF_F_LOCK on windows
    map_test.go:318: map batch api: not supported on windows

=== SKIP: . TestMapWithLock (0.00s)
    map_test.go:350: Ignoring version constraint 5.13 for MAP BPF_F_LOCK on windows
    map_test.go:371: Windows doesn't support UpdateLock

=== SKIP: . TestMapLoadReusePinned/DevMap (0.00s)
    map_test.go:584: Ignoring version constraint 4.14 for devmap on windows
    map_test.go:599: creating map: map type DevMap (linux): not supported on windows

=== SKIP: . TestMapLoadReusePinned/DevMapHash (0.00s)
    map_test.go:587: Ignoring version constraint 5.4 for devmap_hash on windows
    map_test.go:599: creating map: map type DevMapHash (linux): not supported on windows

=== SKIP: . TestMapLoadPinnedWithOptions/read-only (0.00s)
    map_test.go:647: Windows doesn't support file_flags in OBJ_GET

=== SKIP: . TestMapLoadPinnedWithOptions/write-only (0.00s)
    map_test.go:665: Windows doesn't support file_flags in OBJ_GET

=== SKIP: . TestPerfEventArray (0.00s)
    map_test.go:817: creating map: map type PerfEventArray (linux): not supported on windows

=== SKIP: . TestCPUMap (0.00s)
    map_test.go:822: Ignoring version constraint 4.15 for cpu map on windows
    map_test.go:824: creating map: map type CPUMap (linux): not supported on windows

=== SKIP: . TestIterateEmptyMap/SockHash (0.00s)
    map_test.go:854: creating map: map type SockHash (linux): not supported on windows

=== SKIP: . TestIterateEmptyMap/SockMap (0.00s)
    map_test.go:854: creating map: map type SockMap (linux): not supported on windows

=== SKIP: . TestMapBatchLookupAllocations (0.00s)
    map_test.go:967: map batch api: not supported on windows

=== SKIP: . TestMapGuessNonExistentKey (0.00s)
    map_test.go:1022: No need to test linux quirk on windows

=== SKIP: . TestCgroupPerCPUStorageMarshaling (0.00s)
    map_test.go:1253: Ignoring version constraint 5.9 for per-CPU CGoup storage with write from user space support on windows
    map_test.go:1255: creating map: map type PerCPUCGroupStorage (linux): not supported on windows

=== SKIP: . TestMapFreeze (0.00s)
    map_test.go:1448: can't freeze map: read- and write-only maps: not supported on windows

=== SKIP: . TestMapHandle (0.00s)
    map_test.go:1586: map WindowsHash#3: retrieve BTF ID: not supported

=== SKIP: . TestPerfEventArrayCompatible (0.00s)
    map_test.go:1604: creating map: map type PerfEventArray (linux): not supported on windows

=== SKIP: . TestMemory (0.00s)
    memory_test.go:33: creating map: map create: flags: not supported on windows

=== SKIP: . TestMemoryBounds (0.00s)
    memory_test.go:57: creating map: map create: flags: not supported on windows

=== SKIP: . TestMemoryReadOnly (0.00s)
    memory_test.go:73: creating map: map create: flags: not supported on windows

=== SKIP: . TestMemoryUnmap (0.00s)
    memory_test.go:88: creating map: map create: flags: not supported on windows

=== SKIP: . TestProgramRunRawTracepoint (0.00s)
    prog_test.go:135: Ignoring version constraint 5.10 for RawTracepoint test run on windows
    prog_test.go:137: program type RawTracepoint (linux): not supported on windows

=== SKIP: . TestProgramKernelVersion (0.00s)
    prog_test.go:298: Ignoring version constraint 4.20 for KernelVersion on windows
    prog_test.go:300: program type Kprobe (linux): not supported on windows

=== SKIP: . TestProgramVerifierLog (0.00s)
    prog_test.go:355: log level: not supported on windows

=== SKIP: . TestProgramAttachToKernel/LSMMac:task_getpgid (0.00s)
    prog_test.go:681: program type LSM (linux): not supported on windows

=== SKIP: . TestProgramAttachToKernel/TraceFEntry:inet_dgram_connect (0.00s)
    prog_test.go:681: program type Tracing (linux): not supported on windows

=== SKIP: . TestProgramAttachToKernel/TraceFExit:inet_dgram_connect (0.00s)
    prog_test.go:681: program type Tracing (linux): not supported on windows

=== SKIP: . TestProgramAttachToKernel/ModifyReturn:bpf_modify_return_test (0.00s)
    prog_test.go:681: program type Tracing (linux): not supported on windows

=== SKIP: . TestProgramAttachToKernel/TraceRawTp:kfree_skb (0.00s)
    prog_test.go:681: program type Tracing (linux): not supported on windows

=== SKIP: . TestProgramAttachToKernel/TraceFEntry:bpf_testmod_test_read (0.00s)
    prog_test.go:678: Ignoring version constraint 5.11 for bpf_testmod on windows
    prog_test.go:678: bpf_testmod not loaded

=== SKIP: . TestProgramAttachToKernel/TraceFExit:bpf_testmod_test_read (0.00s)
    prog_test.go:678: Ignoring version constraint 5.11 for bpf_testmod on windows
    prog_test.go:678: bpf_testmod not loaded

=== SKIP: . TestProgramAttachToKernel/ModifyReturn:bpf_testmod_test_read (0.00s)
    prog_test.go:678: Ignoring version constraint 5.11 for bpf_testmod on windows
    prog_test.go:678: bpf_testmod not loaded

=== SKIP: . TestProgramAttachToKernel/TraceRawTp:bpf_testmod_test_read (0.00s)
    prog_test.go:678: Ignoring version constraint 5.11 for bpf_testmod on windows
    prog_test.go:678: bpf_testmod not loaded

=== SKIP: . TestProgramKernelTypes (0.00s)
    prog_test.go:698: /sys/kernel/btf/vmlinux not present

=== SKIP: . TestProgramInstructions (0.01s)
    prog_test.go:757: read instructions: not supported on windows

=== SKIP: . TestProgramLoadErrors/poisoned_single (0.00s)
    prog_test.go:801: poisoned_single:
        	  ; __section("socket") int poisoned_single() {
        	 0: MovImm dst: r0 imm: 18
        	 1: LdImmDW dst: r1 imm: 2
        	  ; FORCE_BTF;
        	 3: JEqImm dst: r1 off: 3 imm: 0
        	 4: MovReg dst: r1 src: rfp
        	  ; return core_access(ne.non_exist);
        	 5: AddImm dst: r1 imm: -4
        	 6: LdXMemW dst: r0 src: r1 off: 0 imm: 0
        	  ; }
        	 7: Exit
        
    prog_test.go:802: apply CO-RE relocations: fixup for CORERelocation(local_type_id, Int:"int"[0], local_id=2): CO-RE fixup: not supported on windows

=== SKIP: . TestProgramLoadErrors/poisoned_double (0.00s)
    prog_test.go:801: poisoned_double:
        	 ; __section("socket") int poisoned_double() {
        	0: MovImm dst: r0 imm: 24
        	1: LdImmDW dst: r1 imm: 2
        	 ; FORCE_BTF;
        	3: JEqImm dst: r1 off: 2 imm: 0
        	4: LdImmDW dst: r0 imm: 1
        	 ; }
        	6: Exit
        
    prog_test.go:802: apply CO-RE relocations: fixup for CORERelocation(local_type_id, Int:"int"[0], local_id=2): CO-RE fixup: not supported on windows

=== SKIP: . TestProgramLoadErrors/poisoned_kfunc (0.00s)
    prog_test.go:801: poisoned_kfunc:
        	 ; return invalid_kfunc();
        	0: Call Kfunc(-1) <invalid_kfunc>
        	 ; return invalid_kfunc();
        	1: Exit
        
    prog_test.go:802: fixing up kfuncs: not supported on windows

=== SKIP: . TestProgramAttachToKernelModule (0.00s)
    prog_test.go:825: Ignoring version constraint 5.11 for bpf_testmod on windows
    prog_test.go:825: bpf_testmod not loaded

=== SKIP: . TestHaveBatchAPI (0.00s)
    syscalls_test.go:27: map batch api: not supported on windows

=== SKIP: . TestHaveMapMutabilityModifiers (0.00s)
    syscalls_test.go:43: read- and write-only maps: not supported on windows

=== SKIP: . TestHaveMmapableMaps (0.00s)
    syscalls_test.go:47: mmapable maps: not supported on windows

=== SKIP: . TestHaveInnerMaps (0.00s)
    syscalls_test.go:51: inner maps: not supported on windows

=== SKIP: . TestHaveProbeReadKernel (0.00s)
    syscalls_test.go:55: bpf_probe_read_kernel: not supported on windows

=== SKIP: . TestHaveBPFToBPFCalls (0.00s)
    syscalls_test.go:59: bpf2bpf calls: not supported on windows

=== SKIP: . TestHaveSyscallWrapper (0.00s)
    syscalls_test.go:63: syscall wrapper: not supported on windows

=== SKIP: . TestHaveProgramExtInfos (0.00s)
    syscalls_test.go:67: program ext_infos: not supported on windows

=== SKIP: . TestVariable (0.00s)
    variable_test.go:84: mmapable maps: not supported on windows

=== SKIP: . TestVariableConst (0.00s)
    variable_test.go:136: mmapable maps: not supported on windows

DONE 327 tests, 80 skipped in 2.426s
```

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 353a673..51f28af 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -346,6 +346,7 @@ jobs:
         run: >
           gotestsum --raw-command --ignore-non-json-output-lines --junitfile junit.xml --
           go test -short -count 1 -json
+          ./
           ./asm
           ./btf
           ./internal
diff --git a/asm/func_string.go b/asm/func_string.go
index 4fbef79..d5d624f 100644
--- a/asm/func_string.go
+++ b/asm/func_string.go
@@ -220,15 +220,57 @@ func _() {
 	_ = x[FnUserRingbufDrain-209]
 	_ = x[FnCgrpStorageGet-210]
 	_ = x[FnCgrpStorageDelete-211]
+	_ = x[WindowsFnMapLookupElem-268435457]
+	_ = x[WindowsFnMapUpdateElem-268435458]
+	_ = x[WindowsFnMapDeleteElem-268435459]
+	_ = x[WindowsFnMapLookupAndDeleteElem-268435460]
+	_ = x[WindowsFnTailCall-268435461]
+	_ = x[WindowsFnGetPrandomU32-268435462]
+	_ = x[WindowsFnKtimeGetBootNs-268435463]
+	_ = x[WindowsFnGetSmpProcessorId-268435464]
+	_ = x[WindowsFnKtimeGetNs-268435465]
+	_ = x[WindowsFnCsumDiff-268435466]
+	_ = x[WindowsFnRingbufOutput-268435467]
+	_ = x[WindowsFnTracePrintk2-268435468]
+	_ = x[WindowsFnTracePrintk3-268435469]
+	_ = x[WindowsFnTracePrintk4-268435470]
+	_ = x[WindowsFnTracePrintk5-268435471]
+	_ = x[WindowsFnMapPushElem-268435472]
+	_ = x[WindowsFnMapPopElem-268435473]
+	_ = x[WindowsFnMapPeekElem-268435474]
+	_ = x[WindowsFnGetCurrentPidTgid-268435475]
+	_ = x[WindowsFnGetCurrentLogonId-268435476]
+	_ = x[WindowsFnIsCurrentAdmin-268435477]
+	_ = x[WindowsFnMemcpy-268435478]
+	_ = x[WindowsFnMemcmp-268435479]
+	_ = x[WindowsFnMemset-268435480]
+	_ = x[WindowsFnMemmove-268435481]
+	_ = x[WindowsFnGetSocketCookie-268435482]
+	_ = x[WindowsFnStrncpyS-268435483]
+	_ = x[WindowsFnStrncatS-268435484]
+	_ = x[WindowsFnStrnlenS-268435485]
+	_ = x[WindowsFnKtimeGetBootMs-268435486]
+	_ = x[WindowsFnKtimeGetMs-268435487]
 }
 
-const _BuiltinFunc_name = "FnUnspecFnMapLookupElemFnMapUpdateElemFnMapDeleteElemFnProbeReadFnKtimeGetNsFnTracePrintkFnGetPrandomU32FnGetSmpProcessorIdFnSkbStoreBytesFnL3CsumReplaceFnL4CsumReplaceFnTailCallFnCloneRedirectFnGetCurrentPidTgidFnGetCurrentUidGidFnGetCurrentCommFnGetCgroupClassidFnSkbVlanPushFnSkbVlanPopFnSkbGetTunnelKeyFnSkbSetTunnelKeyFnPerfEventReadFnRedirectFnGetRouteRealmFnPerfEventOutputFnSkbLoadBytesFnGetStackidFnCsumDiffFnSkbGetTunnelOptFnSkbSetTunnelOptFnSkbChangeProtoFnSkbChangeTypeFnSkbUnderCgroupFnGetHashRecalcFnGetCurrentTaskFnProbeWriteUserFnCurrentTaskUnderCgroupFnSkbChangeTailFnSkbPullDataFnCsumUpdateFnSetHashInvalidFnGetNumaNodeIdFnSkbChangeHeadFnXdpAdjustHeadFnProbeReadStrFnGetSocketCookieFnGetSocketUidFnSetHashFnSetsockoptFnSkbAdjustRoomFnRedirectMapFnSkRedirectMapFnSockMapUpdateFnXdpAdjustMetaFnPerfEventReadValueFnPerfProgReadValueFnGetsockoptFnOverrideReturnFnSockOpsCbFlagsSetFnMsgRedirectMapFnMsgApplyBytesFnMsgCorkBytesFnMsgPullDataFnBindFnXdpAdjustTailFnSkbGetXfrmStateFnGetStackFnSkbLoadBytesRelativeFnFibLookupFnSockHashUpdateFnMsgRedirectHashFnSkRedirectHashFnLwtPushEncapFnLwtSeg6StoreBytesFnLwtSeg6AdjustSrhFnLwtSeg6ActionFnRcRepeatFnRcKeydownFnSkbCgroupIdFnGetCurrentCgroupIdFnGetLocalStorageFnSkSelectReuseportFnSkbAncestorCgroupIdFnSkLookupTcpFnSkLookupUdpFnSkReleaseFnMapPushElemFnMapPopElemFnMapPeekElemFnMsgPushDataFnMsgPopDataFnRcPointerRelFnSpinLockFnSpinUnlockFnSkFullsockFnTcpSockFnSkbEcnSetCeFnGetListenerSockFnSkcLookupTcpFnTcpCheckSyncookieFnSysctlGetNameFnSysctlGetCurrentValueFnSysctlGetNewValueFnSysctlSetNewValueFnStrtolFnStrtoulFnSkStorageGetFnSkStorageDeleteFnSendSignalFnTcpGenSyncookieFnSkbOutputFnProbeReadUserFnProbeReadKernelFnProbeReadUserStrFnProbeReadKernelStrFnTcpSendAckFnSendSignalThreadFnJiffies64FnReadBranchRecordsFnGetNsCurrentPidTgidFnXdpOutputFnGetNetnsCookieFnGetCurrentAncestorCgroupIdFnSkAssignFnKtimeGetBootNsFnSeqPrintfFnSeqWriteFnSkCgroupIdFnSkAncestorCgroupIdFnRingbufOutputFnRingbufReserveFnRingbufSubmitFnRingbufDiscardFnRingbufQueryFnCsumLevelFnSkcToTcp6SockFnSkcToTcpSockFnSkcToTcpTimewaitSockFnSkcToTcpRequestSockFnSkcToUdp6SockFnGetTaskStackFnLoadHdrOptFnStoreHdrOptFnReserveHdrOptFnInodeStorageGetFnInodeStorageDeleteFnDPathFnCopyFromUserFnSnprintfBtfFnSeqPrintfBtfFnSkbCgroupClassidFnRedirectNeighFnPerCpuPtrFnThisCpuPtrFnRedirectPeerFnTaskStorageGetFnTaskStorageDeleteFnGetCurrentTaskBtfFnBprmOptsSetFnKtimeGetCoarseNsFnImaInodeHashFnSockFromFileFnCheckMtuFnForEachMapElemFnSnprintfFnSysBpfFnBtfFindByNameKindFnSysCloseFnTimerInitFnTimerSetCallbackFnTimerStartFnTimerCancelFnGetFuncIpFnGetAttachCookieFnTaskPtRegsFnGetBranchSnapshotFnTraceVprintkFnSkcToUnixSockFnKallsymsLookupNameFnFindVmaFnLoopFnStrncmpFnGetFuncArgFnGetFuncRetFnGetFuncArgCntFnGetRetvalFnSetRetvalFnXdpGetBuffLenFnXdpLoadBytesFnXdpStoreBytesFnCopyFromUserTaskFnSkbSetTstampFnImaFileHashFnKptrXchgFnMapLookupPercpuElemFnSkcToMptcpSockFnDynptrFromMemFnRingbufReserveDynptrFnRingbufSubmitDynptrFnRingbufDiscardDynptrFnDynptrReadFnDynptrWriteFnDynptrDataFnTcpRawGenSyncookieIpv4FnTcpRawGenSyncookieIpv6FnTcpRawCheckSyncookieIpv4FnTcpRawCheckSyncookieIpv6FnKtimeGetTaiNsFnUserRingbufDrainFnCgrpStorageGetFnCgrpStorageDelete"
+const (
+	_BuiltinFunc_name_0 = "FnUnspecFnMapLookupElemFnMapUpdateElemFnMapDeleteElemFnProbeReadFnKtimeGetNsFnTracePrintkFnGetPrandomU32FnGetSmpProcessorIdFnSkbStoreBytesFnL3CsumReplaceFnL4CsumReplaceFnTailCallFnCloneRedirectFnGetCurrentPidTgidFnGetCurrentUidGidFnGetCurrentCommFnGetCgroupClassidFnSkbVlanPushFnSkbVlanPopFnSkbGetTunnelKeyFnSkbSetTunnelKeyFnPerfEventReadFnRedirectFnGetRouteRealmFnPerfEventOutputFnSkbLoadBytesFnGetStackidFnCsumDiffFnSkbGetTunnelOptFnSkbSetTunnelOptFnSkbChangeProtoFnSkbChangeTypeFnSkbUnderCgroupFnGetHashRecalcFnGetCurrentTaskFnProbeWriteUserFnCurrentTaskUnderCgroupFnSkbChangeTailFnSkbPullDataFnCsumUpdateFnSetHashInvalidFnGetNumaNodeIdFnSkbChangeHeadFnXdpAdjustHeadFnProbeReadStrFnGetSocketCookieFnGetSocketUidFnSetHashFnSetsockoptFnSkbAdjustRoomFnRedirectMapFnSkRedirectMapFnSockMapUpdateFnXdpAdjustMetaFnPerfEventReadValueFnPerfProgReadValueFnGetsockoptFnOverrideReturnFnSockOpsCbFlagsSetFnMsgRedirectMapFnMsgApplyBytesFnMsgCorkBytesFnMsgPullDataFnBindFnXdpAdjustTailFnSkbGetXfrmStateFnGetStackFnSkbLoadBytesRelativeFnFibLookupFnSockHashUpdateFnMsgRedirectHashFnSkRedirectHashFnLwtPushEncapFnLwtSeg6StoreBytesFnLwtSeg6AdjustSrhFnLwtSeg6ActionFnRcRepeatFnRcKeydownFnSkbCgroupIdFnGetCurrentCgroupIdFnGetLocalStorageFnSkSelectReuseportFnSkbAncestorCgroupIdFnSkLookupTcpFnSkLookupUdpFnSkReleaseFnMapPushElemFnMapPopElemFnMapPeekElemFnMsgPushDataFnMsgPopDataFnRcPointerRelFnSpinLockFnSpinUnlockFnSkFullsockFnTcpSockFnSkbEcnSetCeFnGetListenerSockFnSkcLookupTcpFnTcpCheckSyncookieFnSysctlGetNameFnSysctlGetCurrentValueFnSysctlGetNewValueFnSysctlSetNewValueFnStrtolFnStrtoulFnSkStorageGetFnSkStorageDeleteFnSendSignalFnTcpGenSyncookieFnSkbOutputFnProbeReadUserFnProbeReadKernelFnProbeReadUserStrFnProbeReadKernelStrFnTcpSendAckFnSendSignalThreadFnJiffies64FnReadBranchRecordsFnGetNsCurrentPidTgidFnXdpOutputFnGetNetnsCookieFnGetCurrentAncestorCgroupIdFnSkAssignFnKtimeGetBootNsFnSeqPrintfFnSeqWriteFnSkCgroupIdFnSkAncestorCgroupIdFnRingbufOutputFnRingbufReserveFnRingbufSubmitFnRingbufDiscardFnRingbufQueryFnCsumLevelFnSkcToTcp6SockFnSkcToTcpSockFnSkcToTcpTimewaitSockFnSkcToTcpRequestSockFnSkcToUdp6SockFnGetTaskStackFnLoadHdrOptFnStoreHdrOptFnReserveHdrOptFnInodeStorageGetFnInodeStorageDeleteFnDPathFnCopyFromUserFnSnprintfBtfFnSeqPrintfBtfFnSkbCgroupClassidFnRedirectNeighFnPerCpuPtrFnThisCpuPtrFnRedirectPeerFnTaskStorageGetFnTaskStorageDeleteFnGetCurrentTaskBtfFnBprmOptsSetFnKtimeGetCoarseNsFnImaInodeHashFnSockFromFileFnCheckMtuFnForEachMapElemFnSnprintfFnSysBpfFnBtfFindByNameKindFnSysCloseFnTimerInitFnTimerSetCallbackFnTimerStartFnTimerCancelFnGetFuncIpFnGetAttachCookieFnTaskPtRegsFnGetBranchSnapshotFnTraceVprintkFnSkcToUnixSockFnKallsymsLookupNameFnFindVmaFnLoopFnStrncmpFnGetFuncArgFnGetFuncRetFnGetFuncArgCntFnGetRetvalFnSetRetvalFnXdpGetBuffLenFnXdpLoadBytesFnXdpStoreBytesFnCopyFromUserTaskFnSkbSetTstampFnImaFileHashFnKptrXchgFnMapLookupPercpuElemFnSkcToMptcpSockFnDynptrFromMemFnRingbufReserveDynptrFnRingbufSubmitDynptrFnRingbufDiscardDynptrFnDynptrReadFnDynptrWriteFnDynptrDataFnTcpRawGenSyncookieIpv4FnTcpRawGenSyncookieIpv6FnTcpRawCheckSyncookieIpv4FnTcpRawCheckSyncookieIpv6FnKtimeGetTaiNsFnUserRingbufDrainFnCgrpStorageGetFnCgrpStorageDelete"
+	_BuiltinFunc_name_1 = "WindowsFnMapLookupElemWindowsFnMapUpdateElemWindowsFnMapDeleteElemWindowsFnMapLookupAndDeleteElemWindowsFnTailCallWindowsFnGetPrandomU32WindowsFnKtimeGetBootNsWindowsFnGetSmpProcessorIdWindowsFnKtimeGetNsWindowsFnCsumDiffWindowsFnRingbufOutputWindowsFnTracePrintk2WindowsFnTracePrintk3WindowsFnTracePrintk4WindowsFnTracePrintk5WindowsFnMapPushElemWindowsFnMapPopElemWindowsFnMapPeekElemWindowsFnGetCurrentPidTgidWindowsFnGetCurrentLogonIdWindowsFnIsCurrentAdminWindowsFnMemcpyWindowsFnMemcmpWindowsFnMemsetWindowsFnMemmoveWindowsFnGetSocketCookieWindowsFnStrncpySWindowsFnStrncatSWindowsFnStrnlenSWindowsFnKtimeGetBootMsWindowsFnKtimeGetMs"
+)
 
-var _BuiltinFunc_index = [...]uint16{0, 8, 23, 38, 53, 64, 76, 89, 104, 123, 138, 153, 168, 178, 193, 212, 230, 246, 264, 277, 289, 306, 323, 338, 348, 363, 380, 394, 406, 416, 433, 450, 466, 481, 497, 512, 528, 544, 568, 583, 596, 608, 624, 639, 654, 669, 683, 700, 714, 723, 735, 750, 763, 778, 793, 808, 828, 847, 859, 875, 894, 910, 925, 939, 952, 958, 973, 990, 1000, 1022, 1033, 1049, 1066, 1082, 1096, 1115, 1133, 1148, 1158, 1169, 1182, 1202, 1219, 1238, 1259, 1272, 1285, 1296, 1309, 1321, 1334, 1347, 1359, 1373, 1383, 1395, 1407, 1416, 1429, 1446, 1460, 1479, 1494, 1517, 1536, 1555, 1563, 1572, 1586, 1603, 1615, 1632, 1643, 1658, 1675, 1693, 1713, 1725, 1743, 1754, 1773, 1794, 1805, 1821, 1849, 1859, 1875, 1886, 1896, 1908, 1928, 1943, 1959, 1974, 1990, 2004, 2015, 2030, 2044, 2066, 2087, 2102, 2116, 2128, 2141, 2156, 2173, 2193, 2200, 2214, 2227, 2241, 2259, 2274, 2285, 2297, 2311, 2327, 2346, 2365, 2378, 2396, 2410, 2424, 2434, 2450, 2460, 2468, 2487, 2497, 2508, 2526, 2538, 2551, 2562, 2579, 2591, 2610, 2624, 2639, 2659, 2668, 2674, 2683, 2695, 2707, 2722, 2733, 2744, 2759, 2773, 2788, 2806, 2820, 2833, 2843, 2864, 2880, 2895, 2917, 2938, 2960, 2972, 2985, 2997, 3021, 3045, 3071, 3097, 3112, 3130, 3146, 3165}
+var (
+	_BuiltinFunc_index_0 = [...]uint16{0, 8, 23, 38, 53, 64, 76, 89, 104, 123, 138, 153, 168, 178, 193, 212, 230, 246, 264, 277, 289, 306, 323, 338, 348, 363, 380, 394, 406, 416, 433, 450, 466, 481, 497, 512, 528, 544, 568, 583, 596, 608, 624, 639, 654, 669, 683, 700, 714, 723, 735, 750, 763, 778, 793, 808, 828, 847, 859, 875, 894, 910, 925, 939, 952, 958, 973, 990, 1000, 1022, 1033, 1049, 1066, 1082, 1096, 1115, 1133, 1148, 1158, 1169, 1182, 1202, 1219, 1238, 1259, 1272, 1285, 1296, 1309, 1321, 1334, 1347, 1359, 1373, 1383, 1395, 1407, 1416, 1429, 1446, 1460, 1479, 1494, 1517, 1536, 1555, 1563, 1572, 1586, 1603, 1615, 1632, 1643, 1658, 1675, 1693, 1713, 1725, 1743, 1754, 1773, 1794, 1805, 1821, 1849, 1859, 1875, 1886, 1896, 1908, 1928, 1943, 1959, 1974, 1990, 2004, 2015, 2030, 2044, 2066, 2087, 2102, 2116, 2128, 2141, 2156, 2173, 2193, 2200, 2214, 2227, 2241, 2259, 2274, 2285, 2297, 2311, 2327, 2346, 2365, 2378, 2396, 2410, 2424, 2434, 2450, 2460, 2468, 2487, 2497, 2508, 2526, 2538, 2551, 2562, 2579, 2591, 2610, 2624, 2639, 2659, 2668, 2674, 2683, 2695, 2707, 2722, 2733, 2744, 2759, 2773, 2788, 2806, 2820, 2833, 2843, 2864, 2880, 2895, 2917, 2938, 2960, 2972, 2985, 2997, 3021, 3045, 3071, 3097, 3112, 3130, 3146, 3165}
+	_BuiltinFunc_index_1 = [...]uint16{0, 22, 44, 66, 97, 114, 136, 159, 185, 204, 221, 243, 264, 285, 306, 327, 347, 366, 386, 412, 438, 461, 476, 491, 506, 522, 546, 563, 580, 597, 620, 639}
+)
 
 func (i BuiltinFunc) String() string {
-	if i >= BuiltinFunc(len(_BuiltinFunc_index)-1) {
+	switch {
+	case i <= 211:
+		return _BuiltinFunc_name_0[_BuiltinFunc_index_0[i]:_BuiltinFunc_index_0[i+1]]
+	case 268435457 <= i && i <= 268435487:
+		i -= 268435457
+		return _BuiltinFunc_name_1[_BuiltinFunc_index_1[i]:_BuiltinFunc_index_1[i+1]]
+	default:
 		return "BuiltinFunc(" + strconv.FormatInt(int64(i), 10) + ")"
 	}
-	return _BuiltinFunc_name[_BuiltinFunc_index[i]:_BuiltinFunc_index[i+1]]
 }
diff --git a/asm/func_win.go b/asm/func_win.go
new file mode 100644
index 0000000..b016f00
--- /dev/null
+++ b/asm/func_win.go
@@ -0,0 +1,44 @@
+﻿// Code generated by internal/cmd/genwinfunctions.awk; DO NOT EDIT.
+
+package asm
+
+// Code in this file is derived from eBPF for Windows, available under the MIT License.
+
+import (
+	"github.com/cilium/ebpf/internal/platform"
+)
+
+// Built-in functions (Windows).
+const (
+	WindowsFnMapLookupElem          = BuiltinFunc(platform.WindowsTag | 1)
+	WindowsFnMapUpdateElem          = BuiltinFunc(platform.WindowsTag | 2)
+	WindowsFnMapDeleteElem          = BuiltinFunc(platform.WindowsTag | 3)
+	WindowsFnMapLookupAndDeleteElem = BuiltinFunc(platform.WindowsTag | 4)
+	WindowsFnTailCall               = BuiltinFunc(platform.WindowsTag | 5)
+	WindowsFnGetPrandomU32          = BuiltinFunc(platform.WindowsTag | 6)
+	WindowsFnKtimeGetBootNs         = BuiltinFunc(platform.WindowsTag | 7)
+	WindowsFnGetSmpProcessorId      = BuiltinFunc(platform.WindowsTag | 8)
+	WindowsFnKtimeGetNs             = BuiltinFunc(platform.WindowsTag | 9)
+	WindowsFnCsumDiff               = BuiltinFunc(platform.WindowsTag | 10)
+	WindowsFnRingbufOutput          = BuiltinFunc(platform.WindowsTag | 11)
+	WindowsFnTracePrintk2           = BuiltinFunc(platform.WindowsTag | 12)
+	WindowsFnTracePrintk3           = BuiltinFunc(platform.WindowsTag | 13)
+	WindowsFnTracePrintk4           = BuiltinFunc(platform.WindowsTag | 14)
+	WindowsFnTracePrintk5           = BuiltinFunc(platform.WindowsTag | 15)
+	WindowsFnMapPushElem            = BuiltinFunc(platform.WindowsTag | 16)
+	WindowsFnMapPopElem             = BuiltinFunc(platform.WindowsTag | 17)
+	WindowsFnMapPeekElem            = BuiltinFunc(platform.WindowsTag | 18)
+	WindowsFnGetCurrentPidTgid      = BuiltinFunc(platform.WindowsTag | 19)
+	WindowsFnGetCurrentLogonId      = BuiltinFunc(platform.WindowsTag | 20)
+	WindowsFnIsCurrentAdmin         = BuiltinFunc(platform.WindowsTag | 21)
+	WindowsFnMemcpy                 = BuiltinFunc(platform.WindowsTag | 22)
+	WindowsFnMemcmp                 = BuiltinFunc(platform.WindowsTag | 23)
+	WindowsFnMemset                 = BuiltinFunc(platform.WindowsTag | 24)
+	WindowsFnMemmove                = BuiltinFunc(platform.WindowsTag | 25)
+	WindowsFnGetSocketCookie        = BuiltinFunc(platform.WindowsTag | 26)
+	WindowsFnStrncpyS               = BuiltinFunc(platform.WindowsTag | 27)
+	WindowsFnStrncatS               = BuiltinFunc(platform.WindowsTag | 28)
+	WindowsFnStrnlenS               = BuiltinFunc(platform.WindowsTag | 29)
+	WindowsFnKtimeGetBootMs         = BuiltinFunc(platform.WindowsTag | 30)
+	WindowsFnKtimeGetMs             = BuiltinFunc(platform.WindowsTag | 31)
+)
diff --git a/asm/instruction.go b/asm/instruction.go
index e71056e..10ca254 100644
--- a/asm/instruction.go
+++ b/asm/instruction.go
@@ -11,6 +11,7 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/platform"
 	"github.com/cilium/ebpf/internal/sys"
 )
@@ -144,7 +145,7 @@ func (ins Instruction) Marshal(w io.Writer, bo binary.ByteOrder) (uint64, error)
 		fn := BuiltinFunc(ins.Constant)
 		plat, value := platform.DecodeConstant(fn)
 		if plat != platform.Native {
-			return 0, fmt.Errorf("function %s is not supported on this platform", fn)
+			return 0, fmt.Errorf("function %s (%s): %w", fn, plat, internal.ErrNotSupportedOnOS)
 		}
 		cons = int32(value)
 	} else if ins.OpCode.Class().IsALU() {
diff --git a/attachtype_string.go b/attachtype_string.go
index 84445a3..efed516 100644
--- a/attachtype_string.go
+++ b/attachtype_string.go
@@ -66,15 +66,35 @@ func _() {
 	_ = x[AttachCgroupUnixGetsockname-53]
 	_ = x[AttachNetkitPrimary-54]
 	_ = x[AttachNetkitPeer-55]
+	_ = x[AttachWindowsXDP-268435457]
+	_ = x[AttachWindowsBind-268435458]
+	_ = x[AttachWindowsCGroupInet4Connect-268435459]
+	_ = x[AttachWindowsCGroupInet6Connect-268435460]
+	_ = x[AttachWindowsCgroupInet4RecvAccept-268435461]
+	_ = x[AttachWindowsCgroupInet6RecvAccept-268435462]
+	_ = x[AttachWindowsCGroupSockOps-268435463]
+	_ = x[AttachWindowsSample-268435464]
+	_ = x[AttachWindowsXDPTest-268435465]
 }
 
-const _AttachType_name = "NoneCGroupInetEgressCGroupInetSockCreateCGroupSockOpsSkSKBStreamParserSkSKBStreamVerdictCGroupDeviceSkMsgVerdictCGroupInet4BindCGroupInet6BindCGroupInet4ConnectCGroupInet6ConnectCGroupInet4PostBindCGroupInet6PostBindCGroupUDP4SendmsgCGroupUDP6SendmsgLircMode2FlowDissectorCGroupSysctlCGroupUDP4RecvmsgCGroupUDP6RecvmsgCGroupGetsockoptCGroupSetsockoptTraceRawTpTraceFEntryTraceFExitModifyReturnLSMMacTraceIterCgroupInet4GetPeernameCgroupInet6GetPeernameCgroupInet4GetSocknameCgroupInet6GetSocknameXDPDevMapCgroupInetSockReleaseXDPCPUMapSkLookupXDPSkSKBVerdictSkReuseportSelectSkReuseportSelectOrMigratePerfEventTraceKprobeMultiLSMCgroupStructOpsNetfilterTCXIngressTCXEgressTraceUprobeMultiCgroupUnixConnectCgroupUnixSendmsgCgroupUnixRecvmsgCgroupUnixGetpeernameCgroupUnixGetsocknameNetkitPrimaryNetkitPeerTraceKprobeSession"
+const (
+	_AttachType_name_0 = "NoneCGroupInetEgressCGroupInetSockCreateCGroupSockOpsSkSKBStreamParserSkSKBStreamVerdictCGroupDeviceSkMsgVerdictCGroupInet4BindCGroupInet6BindCGroupInet4ConnectCGroupInet6ConnectCGroupInet4PostBindCGroupInet6PostBindCGroupUDP4SendmsgCGroupUDP6SendmsgLircMode2FlowDissectorCGroupSysctlCGroupUDP4RecvmsgCGroupUDP6RecvmsgCGroupGetsockoptCGroupSetsockoptTraceRawTpTraceFEntryTraceFExitModifyReturnLSMMacTraceIterCgroupInet4GetPeernameCgroupInet6GetPeernameCgroupInet4GetSocknameCgroupInet6GetSocknameXDPDevMapCgroupInetSockReleaseXDPCPUMapSkLookupXDPSkSKBVerdictSkReuseportSelectSkReuseportSelectOrMigratePerfEventTraceKprobeMultiLSMCgroupStructOpsNetfilterTCXIngressTCXEgressTraceUprobeMultiCgroupUnixConnectCgroupUnixSendmsgCgroupUnixRecvmsgCgroupUnixGetpeernameCgroupUnixGetsocknameNetkitPrimaryNetkitPeerTraceKprobeSession"
+	_AttachType_name_1 = "WindowsXDPWindowsBindWindowsCGroupInet4ConnectWindowsCGroupInet6ConnectWindowsCgroupInet4RecvAcceptWindowsCgroupInet6RecvAcceptWindowsCGroupSockOpsWindowsSampleWindowsXDPTest"
+)
 
-var _AttachType_index = [...]uint16{0, 4, 20, 40, 53, 70, 88, 100, 112, 127, 142, 160, 178, 197, 216, 233, 250, 259, 272, 284, 301, 318, 334, 350, 360, 371, 381, 393, 399, 408, 430, 452, 474, 496, 505, 526, 535, 543, 546, 558, 575, 601, 610, 626, 635, 644, 653, 663, 672, 688, 705, 722, 739, 760, 781, 794, 804, 822}
+var (
+	_AttachType_index_0 = [...]uint16{0, 4, 20, 40, 53, 70, 88, 100, 112, 127, 142, 160, 178, 197, 216, 233, 250, 259, 272, 284, 301, 318, 334, 350, 360, 371, 381, 393, 399, 408, 430, 452, 474, 496, 505, 526, 535, 543, 546, 558, 575, 601, 610, 626, 635, 644, 653, 663, 672, 688, 705, 722, 739, 760, 781, 794, 804, 822}
+	_AttachType_index_1 = [...]uint8{0, 10, 21, 46, 71, 99, 127, 147, 160, 174}
+)
 
 func (i AttachType) String() string {
-	if i >= AttachType(len(_AttachType_index)-1) {
+	switch {
+	case i <= 56:
+		return _AttachType_name_0[_AttachType_index_0[i]:_AttachType_index_0[i+1]]
+	case 268435457 <= i && i <= 268435465:
+		i -= 268435457
+		return _AttachType_name_1[_AttachType_index_1[i]:_AttachType_index_1[i+1]]
+	default:
 		return "AttachType(" + strconv.FormatInt(int64(i), 10) + ")"
 	}
-	return _AttachType_name[_AttachType_index[i]:_AttachType_index[i+1]]
 }
diff --git a/collection.go b/collection.go
index 2b0371d..18cb2c8 100644
--- a/collection.go
+++ b/collection.go
@@ -13,6 +13,7 @@ import (
 	"github.com/cilium/ebpf/internal/kallsyms"
 	"github.com/cilium/ebpf/internal/kconfig"
 	"github.com/cilium/ebpf/internal/linux"
+	"github.com/cilium/ebpf/internal/platform"
 	"github.com/cilium/ebpf/internal/sys"
 )
 
@@ -311,8 +312,7 @@ func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions)
 
 	// Evaluate the loader's objects after all (lazy)loading has taken place.
 	for n, m := range loader.maps {
-		switch m.typ {
-		case ProgramArray:
+		if m.typ.canStoreProgram() {
 			// Require all lazy-loaded ProgramArrays to be assigned to the given object.
 			// The kernel empties a ProgramArray once the last user space reference
 			// to it closes, which leads to failed tail calls. Combined with the library
@@ -719,6 +719,10 @@ func resolveKconfig(m *MapSpec) error {
 		return errors.New("map value is not a Datasec")
 	}
 
+	if platform.IsWindows {
+		return fmt.Errorf(".kconfig: %w", internal.ErrNotSupportedOnOS)
+	}
+
 	type configInfo struct {
 		offset uint32
 		size   uint32
diff --git a/info.go b/info.go
index a6edbc3..667b0cc 100644
--- a/info.go
+++ b/info.go
@@ -291,6 +291,11 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
 		verifiedInstructions: info.VerifiedInsns,
 	}
 
+	if platform.IsWindows && info.Tag == [8]uint8{} {
+		// Windows doesn't support the tag field, clear it for now.
+		pi.Tag = ""
+	}
+
 	// Start with a clean struct for the second call, otherwise we may get EFAULT.
 	var info2 sys.ProgInfo
 
@@ -310,7 +315,7 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
 	}
 
 	// createdByUID and NrMapIds were introduced in the same kernel version.
-	if pi.maps != nil {
+	if pi.maps != nil && platform.IsLinux {
 		pi.createdByUID = info.CreatedByUid
 		pi.haveCreatedByUID = true
 	}
@@ -390,7 +395,7 @@ func newProgramInfoFromProc(fd *sys.FD) (*ProgramInfo, error) {
 		"prog_type": &progType,
 		"prog_tag":  &info.Tag,
 	})
-	if errors.Is(err, ErrNotSupported) {
+	if errors.Is(err, ErrNotSupported) && !errors.Is(err, internal.ErrNotSupportedOnOS) {
 		return nil, &internal.UnsupportedFeatureError{
 			Name:           "reading program info from /proc/self/fdinfo",
 			MinimumVersion: internal.Version{4, 10, 0},
@@ -537,6 +542,10 @@ func (pi *ProgramInfo) LineInfos() (btf.LineOffsets, error) {
 // Available from 4.13. Requires CAP_BPF or equivalent for plain instructions.
 // Requires CAP_SYS_ADMIN for instructions with metadata.
 func (pi *ProgramInfo) Instructions() (asm.Instructions, error) {
+	if platform.IsWindows && len(pi.insns) == 0 {
+		return nil, fmt.Errorf("read instructions: %w", internal.ErrNotSupportedOnOS)
+	}
+
 	// If the calling process is not BPF-capable or if the kernel doesn't
 	// support getting xlated instructions, the field will be zero.
 	if len(pi.insns) == 0 {
@@ -735,6 +744,10 @@ func (pi *ProgramInfo) FuncInfos() (btf.FuncOffsets, error) {
 }
 
 func scanFdInfo(fd *sys.FD, fields map[string]interface{}) error {
+	if platform.IsWindows {
+		return fmt.Errorf("read fdinfo: %w", internal.ErrNotSupportedOnOS)
+	}
+
 	fh, err := os.Open(fmt.Sprintf("/proc/self/fdinfo/%d", fd.Int()))
 	if err != nil {
 		return err
@@ -815,6 +828,11 @@ func EnableStats(which uint32) (io.Closer, error) {
 }
 
 var haveProgramInfoMapIDs = internal.NewFeatureTest("map IDs in program info", func() error {
+	if platform.IsWindows {
+		// We only support efW versions which have this feature, no need to probe.
+		return nil
+	}
+
 	prog, err := progLoad(asm.Instructions{
 		asm.LoadImm(asm.R0, 0, asm.DWord),
 		asm.Return(),
@@ -839,4 +857,4 @@ var haveProgramInfoMapIDs = internal.NewFeatureTest("map IDs in program info", f
 	}
 
 	return err
-}, "4.15")
+}, "4.15", "windows:0.20.0")
diff --git a/internal/cmd/genwinfunctions.awk b/internal/cmd/genwinfunctions.awk
new file mode 100644
index 0000000..430ea1b
--- /dev/null
+++ b/internal/cmd/genwinfunctions.awk
@@ -0,0 +1,36 @@
+#!/usr/bin/gawk -f
+# Generate constants from eBPF for Windows headers.
+#
+# This script expects include/ebpf_structs.h as input.
+
+BEGIN {
+	print "// Code generated by internal/cmd/genwinfunctions.awk; DO NOT EDIT."
+	print ""
+	print "package asm"
+	print ""
+	print "// Code in this file is derived from eBPF for Windows, available under the MIT License."
+	print ""
+	print "import \"github.com/cilium/ebpf/internal/platform\""
+	print ""
+	print "// Built-in functions (Windows)."
+	print "const ("
+}
+
+/BPF_FUNC_[[:alnum:]_]+ *= *[0-9]+,/ {
+	name = gensub(/.*BPF_FUNC_([[:alnum:]_]+) *=.*/, "\\1", 1)
+	id = gensub(/.*BPF_FUNC_[[:alnum:]_]+ *= *([0-9]+),.*/, "\\1", 1)
+
+	split(tolower(name), parts, "_")
+	result = "WindowsFn"
+	for (i in parts) {
+		part = parts[i]
+		result = result substr(toupper(substr(part,1,1)), 1, 1) substr(part, 2)
+	}
+
+	print "\t" result " = BuiltinFunc(platform.WindowsTag | " id ")"
+}
+
+END {
+	print ")"
+	print ""
+}
diff --git a/internal/platform/constants.go b/internal/platform/constants.go
index 3607414..f04af9b 100644
--- a/internal/platform/constants.go
+++ b/internal/platform/constants.go
@@ -7,6 +7,7 @@ import "fmt"
 // The value for Linux is zero so that existing constants do not change.
 const (
 	LinuxTag = uint32(iota) << platformShift
+	WindowsTag
 )
 
 const (
@@ -19,6 +20,8 @@ func tagForPlatform(platform string) (uint32, error) {
 	switch platform {
 	case Linux:
 		return LinuxTag, nil
+	case Windows:
+		return WindowsTag, nil
 	default:
 		return 0, fmt.Errorf("unrecognized platform: %s", platform)
 	}
@@ -29,6 +32,8 @@ func platformForConstant(c uint32) string {
 	switch tag {
 	case LinuxTag:
 		return Linux
+	case WindowsTag:
+		return Windows
 	default:
 		return ""
 	}
diff --git a/internal/platform/platform.go b/internal/platform/platform.go
index 1d9ffa1..2c6ecf4 100644
--- a/internal/platform/platform.go
+++ b/internal/platform/platform.go
@@ -7,7 +7,8 @@ import (
 )
 
 const (
-	Linux = "linux"
+	Linux   = "linux"
+	Windows = "windows"
 )
 
 const (
diff --git a/internal/platform/platform_other.go b/internal/platform/platform_other.go
index 39bd247..cd33b3f 100644
--- a/internal/platform/platform_other.go
+++ b/internal/platform/platform_other.go
@@ -1,4 +1,4 @@
-//go:build !linux
+//go:build !linux && !windows
 
 package platform
 
diff --git a/internal/platform/platform_windows.go b/internal/platform/platform_windows.go
new file mode 100644
index 0000000..26b4a8e
--- /dev/null
+++ b/internal/platform/platform_windows.go
@@ -0,0 +1,3 @@
+package platform
+
+const Native = Windows
diff --git a/map.go b/map.go
index 77c472e..a64c623 100644
--- a/map.go
+++ b/map.go
@@ -124,8 +124,8 @@ func (ms *MapSpec) Copy() *MapSpec {
 // The copy is only performed if fixups are necessary, so callers mustn't mutate
 // the returned spec.
 func (spec *MapSpec) fixupMagicFields() (*MapSpec, error) {
-	switch spec.Type {
-	case ArrayOfMaps, HashOfMaps:
+	switch {
+	case spec.Type.canStoreMap():
 		if spec.ValueSize != 0 && spec.ValueSize != 4 {
 			return nil, errors.New("ValueSize must be zero or four for map of map")
 		}
@@ -133,7 +133,7 @@ func (spec *MapSpec) fixupMagicFields() (*MapSpec, error) {
 		spec = spec.Copy()
 		spec.ValueSize = 4
 
-	case PerfEventArray:
+	case spec.Type == PerfEventArray:
 		if spec.KeySize != 0 && spec.KeySize != 4 {
 			return nil, errors.New("KeySize must be zero or four for perf event array")
 		}
@@ -160,7 +160,7 @@ func (spec *MapSpec) fixupMagicFields() (*MapSpec, error) {
 			spec.MaxEntries = n
 		}
 
-	case CPUMap:
+	case spec.Type == CPUMap:
 		n, err := PossibleCPU()
 		if err != nil {
 			return nil, fmt.Errorf("fixup cpu map: %w", err)
@@ -237,11 +237,16 @@ func (ms *MapSpec) Compatible(m *Map) error {
 		diffs = append(diffs, fmt.Sprintf("MaxEntries: %d changed to %d", m.maxEntries, ms.MaxEntries))
 	}
 
-	// BPF_F_RDONLY_PROG is set unconditionally for devmaps. Explicitly allow this
-	// mismatch.
-	if !((ms.Type == DevMap || ms.Type == DevMapHash) && m.flags^ms.Flags == sys.BPF_F_RDONLY_PROG) &&
-		m.flags != ms.Flags {
-		diffs = append(diffs, fmt.Sprintf("Flags: %d changed to %d", m.flags, ms.Flags))
+	flags := ms.Flags
+	if ms.Type == DevMap || ms.Type == DevMapHash {
+		// As of 0cdbb4b09a06 ("devmap: Allow map lookups from eBPF")
+		// BPF_F_RDONLY_PROG is set unconditionally for devmaps. Explicitly
+		// allow this mismatch.
+		flags |= (m.flags & sys.BPF_F_RDONLY_PROG)
+	}
+
+	if m.flags != flags {
+		diffs = append(diffs, fmt.Sprintf("Flags: %d changed to %d", m.flags, flags))
 	}
 
 	if len(diffs) == 0 {
@@ -477,7 +482,7 @@ func (spec *MapSpec) createMap(inner *sys.FD) (_ *Map, err error) {
 
 	p, sysMapType := platform.DecodeConstant(spec.Type)
 	if p != platform.Native {
-		return nil, fmt.Errorf("map type %s: %w", spec.Type, internal.ErrNotSupportedOnOS)
+		return nil, fmt.Errorf("map type %s (%s): %w", spec.Type, p, internal.ErrNotSupportedOnOS)
 	}
 
 	attr := sys.MapCreateAttr{
@@ -534,6 +539,14 @@ func (spec *MapSpec) createMap(inner *sys.FD) (_ *Map, err error) {
 }
 
 func handleMapCreateError(attr sys.MapCreateAttr, spec *MapSpec, err error) error {
+	if platform.IsWindows {
+		if errors.Is(err, unix.EINVAL) && attr.MapFlags != 0 {
+			return fmt.Errorf("map create: flags: %w", internal.ErrNotSupportedOnOS)
+		}
+
+		return err
+	}
+
 	if errors.Is(err, unix.EPERM) {
 		return fmt.Errorf("map create: %w (MEMLOCK may be too low, consider rlimit.RemoveMemlock)", err)
 	}
@@ -1004,7 +1017,7 @@ func (m *Map) nextKey(key interface{}, nextKeyOut sys.Pointer) error {
 	if err = sys.MapGetNextKey(&attr); err != nil {
 		// Kernels 4.4.131 and earlier return EFAULT instead of a pointer to the
 		// first map element when a nil key pointer is specified.
-		if key == nil && errors.Is(err, unix.EFAULT) {
+		if platform.IsLinux && key == nil && errors.Is(err, unix.EFAULT) {
 			var guessKey []byte
 			guessKey, err = m.guessNonExistentKey()
 			if err != nil {
diff --git a/prog.go b/prog.go
index 6ef28e6..0eb7dfb 100644
--- a/prog.go
+++ b/prog.go
@@ -282,7 +282,7 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
 
 	p, progType := platform.DecodeConstant(spec.Type)
 	if p != platform.Native {
-		return nil, fmt.Errorf("program type %s: %w", spec.Type, internal.ErrNotSupportedOnOS)
+		return nil, fmt.Errorf("program type %s (%s): %w", spec.Type, p, internal.ErrNotSupportedOnOS)
 	}
 
 	attr := &sys.ProgLoadAttr{
@@ -414,6 +414,10 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
 		}
 	}
 
+	if platform.IsWindows && opts.LogLevel != 0 {
+		return nil, fmt.Errorf("log level: %w", internal.ErrNotSupportedOnOS)
+	}
+
 	// The caller requested a specific verifier log level. Set up the log buffer
 	// so that there is a chance of loading the program in a single shot.
 	logSize := internal.Between(opts.LogSizeStart, minVerifierLogSize, maxVerifierLogSize)
@@ -520,7 +524,7 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
 //
 // You should not use fd after calling this function.
 //
-// Requires at least Linux 4.10.
+// Requires at least Linux 4.10. Returns an error on Windows.
 func NewProgramFromFD(fd int) (*Program, error) {
 	f, err := sys.NewFD(fd)
 	if err != nil {
@@ -764,6 +768,10 @@ func (p *Program) Benchmark(in []byte, repeat int, reset func()) (uint32, time.D
 }
 
 var haveProgRun = internal.NewFeatureTest("BPF_PROG_RUN", func() error {
+	if platform.IsWindows {
+		return nil
+	}
+
 	prog, err := NewProgram(&ProgramSpec{
 		// SocketFilter does not require privileges on newer kernels.
 		Type: SocketFilter,
@@ -805,7 +813,7 @@ var haveProgRun = internal.NewFeatureTest("BPF_PROG_RUN", func() error {
 	}
 
 	return err
-}, "4.12")
+}, "4.12", "windows:0.20")
 
 func (p *Program) run(opts *RunOptions) (uint32, time.Duration, error) {
 	if uint(len(opts.Data)) > math.MaxUint32 {
diff --git a/syscalls.go b/syscalls.go
index f057478..32b6d72 100644
--- a/syscalls.go
+++ b/syscalls.go
@@ -11,6 +11,7 @@ import (
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/linux"
+	"github.com/cilium/ebpf/internal/platform"
 	"github.com/cilium/ebpf/internal/sys"
 	"github.com/cilium/ebpf/internal/tracefs"
 	"github.com/cilium/ebpf/internal/unix"
@@ -62,6 +63,11 @@ func progLoad(insns asm.Instructions, typ ProgramType, license string) (*sys.FD,
 }
 
 var haveNestedMaps = internal.NewFeatureTest("nested maps", func() error {
+	if platform.IsWindows {
+		// We only support efW versions which have this feature, no need to probe.
+		return nil
+	}
+
 	_, err := sys.MapCreate(&sys.MapCreateAttr{
 		MapType:    sys.MapType(ArrayOfMaps),
 		KeySize:    4,
@@ -77,7 +83,7 @@ var haveNestedMaps = internal.NewFeatureTest("nested maps", func() error {
 		return nil
 	}
 	return err
-}, "4.12")
+}, "4.12", "windows:0.20.0")
 
 var haveMapMutabilityModifiers = internal.NewFeatureTest("read- and write-only maps", func() error {
 	// This checks BPF_F_RDONLY_PROG and BPF_F_WRONLY_PROG. Since
@@ -171,6 +177,11 @@ func wrapMapError(err error) error {
 }
 
 var haveObjName = internal.NewFeatureTest("object names", func() error {
+	if platform.IsWindows {
+		// We only support efW versions which have this feature, no need to probe.
+		return nil
+	}
+
 	attr := sys.MapCreateAttr{
 		MapType:    sys.MapType(Array),
 		KeySize:    4,
@@ -194,9 +205,14 @@ var haveObjName = internal.NewFeatureTest("object names", func() error {
 
 	_ = fd.Close()
 	return nil
-}, "4.15")
+}, "4.15", "windows:0.20.0")
 
 var objNameAllowsDot = internal.NewFeatureTest("dot in object names", func() error {
+	if platform.IsWindows {
+		// We only support efW versions which have this feature, no need to probe.
+		return nil
+	}
+
 	if err := haveObjName(); err != nil {
 		return err
 	}
@@ -225,7 +241,7 @@ var objNameAllowsDot = internal.NewFeatureTest("dot in object names", func() err
 
 	_ = fd.Close()
 	return nil
-}, "5.2")
+}, "5.2", "windows:0.20.0")
 
 var haveBatchAPI = internal.NewFeatureTest("map batch api", func() error {
 	var maxEntries uint32 = 2
diff --git a/types.go b/types.go
index fb09b33..ae70e6c 100644
--- a/types.go
+++ b/types.go
@@ -105,6 +105,23 @@ const (
 	Arena
 )
 
+// Map types (Windows).
+const (
+	WindowsHash MapType = MapType(platform.WindowsTag | iota + 1)
+	WindowsArray
+	WindowsProgramArray
+	WindowsPerCPUHash
+	WindowsPerCPUArray
+	WindowsHashOfMaps
+	WindowsArrayOfMaps
+	WindowsLRUHash
+	WindowsLPMTrie
+	WindowsQueue
+	WindowsLRUCPUHash
+	WindowsStack
+	WindowsRingBuf
+)
+
 // MapTypeForPlatform returns a platform specific map type.
 //
 // Use this if the library doesn't provide a constant yet.
@@ -114,7 +131,14 @@ func MapTypeForPlatform(plat string, typ uint32) (MapType, error) {
 
 // hasPerCPUValue returns true if the Map stores a value per CPU.
 func (mt MapType) hasPerCPUValue() bool {
-	return mt == PerCPUHash || mt == PerCPUArray || mt == LRUCPUHash || mt == PerCPUCGroupStorage
+	switch mt {
+	case PerCPUHash, PerCPUArray, LRUCPUHash, PerCPUCGroupStorage:
+		return true
+	case WindowsPerCPUHash, WindowsPerCPUArray, WindowsLRUCPUHash:
+		return true
+	default:
+		return false
+	}
 }
 
 // canStoreMapOrProgram returns true if the Map stores references to another Map
@@ -126,13 +150,13 @@ func (mt MapType) canStoreMapOrProgram() bool {
 // canStoreMap returns true if the map type accepts a map fd
 // for update and returns a map id for lookup.
 func (mt MapType) canStoreMap() bool {
-	return mt == ArrayOfMaps || mt == HashOfMaps
+	return mt == ArrayOfMaps || mt == HashOfMaps || mt == WindowsArrayOfMaps || mt == WindowsHashOfMaps
 }
 
 // canStoreProgram returns true if the map type accepts a program fd
 // for update and returns a program id for lookup.
 func (mt MapType) canStoreProgram() bool {
-	return mt == ProgramArray
+	return mt == ProgramArray || mt == WindowsProgramArray
 }
 
 // canHaveValueSize returns true if the map type supports setting a value size.
@@ -190,6 +214,18 @@ const (
 	Netfilter             = ProgramType(sys.BPF_PROG_TYPE_NETFILTER)
 )
 
+// eBPF program types (Windows).
+//
+// See https://github.com/microsoft/ebpf-for-windows/blob/main/include/ebpf_structs.h#L170
+const (
+	WindowsXDP ProgramType = ProgramType(platform.WindowsTag) | (iota + 1)
+	WindowsBind
+	WindowsCGroupSockAddr
+	WindowsSockOps
+	WindowsXDPTest ProgramType = ProgramType(platform.WindowsTag) | 998
+	WindowsSample  ProgramType = ProgramType(platform.WindowsTag) | 999
+)
+
 // ProgramTypeForPlatform returns a platform specific program type.
 //
 // Use this if the library doesn't provide a constant yet.
@@ -207,6 +243,7 @@ type AttachType uint32
 // AttachNone is an alias for AttachCGroupInetIngress for readability reasons.
 const AttachNone AttachType = 0
 
+// Attach types (Linux).
 const (
 	AttachCGroupInetIngress          = AttachType(sys.BPF_CGROUP_INET_INGRESS)
 	AttachCGroupInetEgress           = AttachType(sys.BPF_CGROUP_INET_EGRESS)
@@ -267,6 +304,21 @@ const (
 	AttachNetkitPeer                 = AttachType(sys.BPF_NETKIT_PEER)
 )
 
+// Attach types (Windows).
+//
+// See https://github.com/microsoft/ebpf-for-windows/blob/main/include/ebpf_structs.h#L260
+const (
+	AttachWindowsXDP = AttachType(platform.WindowsTag | iota + 1)
+	AttachWindowsBind
+	AttachWindowsCGroupInet4Connect
+	AttachWindowsCGroupInet6Connect
+	AttachWindowsCgroupInet4RecvAccept
+	AttachWindowsCgroupInet6RecvAccept
+	AttachWindowsCGroupSockOps
+	AttachWindowsSample
+	AttachWindowsXDPTest
+)
+
 // AttachTypeForPlatform returns a platform specific attach type.
 //
 // Use this if the library doesn't provide a constant yet.
diff --git a/types_string.go b/types_string.go
index f066851..efcd6a7 100644
--- a/types_string.go
+++ b/types_string.go
@@ -42,17 +42,41 @@ func _() {
 	_ = x[UserRingbuf-31]
 	_ = x[CgroupStorage-32]
 	_ = x[Arena-33]
+	_ = x[WindowsHash-268435457]
+	_ = x[WindowsArray-268435458]
+	_ = x[WindowsProgramArray-268435459]
+	_ = x[WindowsPerCPUHash-268435460]
+	_ = x[WindowsPerCPUArray-268435461]
+	_ = x[WindowsHashOfMaps-268435462]
+	_ = x[WindowsArrayOfMaps-268435463]
+	_ = x[WindowsLRUHash-268435464]
+	_ = x[WindowsLPMTrie-268435465]
+	_ = x[WindowsQueue-268435466]
+	_ = x[WindowsLRUCPUHash-268435467]
+	_ = x[WindowsStack-268435468]
+	_ = x[WindowsRingBuf-268435469]
 }
 
-const _MapType_name = "UnspecifiedMapHashArrayProgramArrayPerfEventArrayPerCPUHashPerCPUArrayStackTraceCGroupArrayLRUHashLRUCPUHashLPMTrieArrayOfMapsHashOfMapsDevMapSockMapCPUMapXSKMapSockHashCGroupStorageReusePortSockArrayPerCPUCGroupStorageQueueStackSkStorageDevMapHashStructOpsMapRingBufInodeStorageTaskStorageBloomFilterUserRingbufCgroupStorageArena"
+const (
+	_MapType_name_0 = "UnspecifiedMapHashArrayProgramArrayPerfEventArrayPerCPUHashPerCPUArrayStackTraceCGroupArrayLRUHashLRUCPUHashLPMTrieArrayOfMapsHashOfMapsDevMapSockMapCPUMapXSKMapSockHashCGroupStorageReusePortSockArrayPerCPUCGroupStorageQueueStackSkStorageDevMapHashStructOpsMapRingBufInodeStorageTaskStorageBloomFilterUserRingbufCgroupStorageArena"
+	_MapType_name_1 = "WindowsHashWindowsArrayWindowsProgramArrayWindowsPerCPUHashWindowsPerCPUArrayWindowsHashOfMapsWindowsArrayOfMapsWindowsLRUHashWindowsLPMTrieWindowsQueueWindowsLRUCPUHashWindowsStackWindowsRingBuf"
+)
 
-var _MapType_index = [...]uint16{0, 14, 18, 23, 35, 49, 59, 70, 80, 91, 98, 108, 115, 126, 136, 142, 149, 155, 161, 169, 182, 200, 219, 224, 229, 238, 248, 260, 267, 279, 290, 301, 312, 325, 330}
+var (
+	_MapType_index_0 = [...]uint16{0, 14, 18, 23, 35, 49, 59, 70, 80, 91, 98, 108, 115, 126, 136, 142, 149, 155, 161, 169, 182, 200, 219, 224, 229, 238, 248, 260, 267, 279, 290, 301, 312, 325, 330}
+	_MapType_index_1 = [...]uint8{0, 11, 23, 42, 59, 77, 94, 112, 126, 140, 152, 169, 181, 195}
+)
 
 func (i MapType) String() string {
-	if i >= MapType(len(_MapType_index)-1) {
+	switch {
+	case i <= 33:
+		return _MapType_name_0[_MapType_index_0[i]:_MapType_index_0[i+1]]
+	case 268435457 <= i && i <= 268435469:
+		i -= 268435457
+		return _MapType_name_1[_MapType_index_1[i]:_MapType_index_1[i+1]]
+	default:
 		return "MapType(" + strconv.FormatInt(int64(i), 10) + ")"
 	}
-	return _MapType_name[_MapType_index[i]:_MapType_index[i+1]]
 }
 func _() {
 	// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -91,17 +115,39 @@ func _() {
 	_ = x[SkLookup-30]
 	_ = x[Syscall-31]
 	_ = x[Netfilter-32]
+	_ = x[WindowsXDP-268435457]
+	_ = x[WindowsBind-268435458]
+	_ = x[WindowsCGroupSockAddr-268435459]
+	_ = x[WindowsSockOps-268435460]
+	_ = x[WindowsXDPTest-268436454]
+	_ = x[WindowsSample-268436455]
 }
 
-const _ProgramType_name = "UnspecifiedProgramSocketFilterKprobeSchedCLSSchedACTTracePointXDPPerfEventCGroupSKBCGroupSockLWTInLWTOutLWTXmitSockOpsSkSKBCGroupDeviceSkMsgRawTracepointCGroupSockAddrLWTSeg6LocalLircMode2SkReuseportFlowDissectorCGroupSysctlRawTracepointWritableCGroupSockoptTracingStructOpsExtensionLSMSkLookupSyscallNetfilter"
+const (
+	_ProgramType_name_0 = "UnspecifiedProgramSocketFilterKprobeSchedCLSSchedACTTracePointXDPPerfEventCGroupSKBCGroupSockLWTInLWTOutLWTXmitSockOpsSkSKBCGroupDeviceSkMsgRawTracepointCGroupSockAddrLWTSeg6LocalLircMode2SkReuseportFlowDissectorCGroupSysctlRawTracepointWritableCGroupSockoptTracingStructOpsExtensionLSMSkLookupSyscallNetfilter"
+	_ProgramType_name_1 = "WindowsXDPWindowsBindWindowsCGroupSockAddrWindowsSockOps"
+	_ProgramType_name_2 = "WindowsXDPTestWindowsSample"
+)
 
-var _ProgramType_index = [...]uint16{0, 18, 30, 36, 44, 52, 62, 65, 74, 83, 93, 98, 104, 111, 118, 123, 135, 140, 153, 167, 179, 188, 199, 212, 224, 245, 258, 265, 274, 283, 286, 294, 301, 310}
+var (
+	_ProgramType_index_0 = [...]uint16{0, 18, 30, 36, 44, 52, 62, 65, 74, 83, 93, 98, 104, 111, 118, 123, 135, 140, 153, 167, 179, 188, 199, 212, 224, 245, 258, 265, 274, 283, 286, 294, 301, 310}
+	_ProgramType_index_1 = [...]uint8{0, 10, 21, 42, 56}
+	_ProgramType_index_2 = [...]uint8{0, 14, 27}
+)
 
 func (i ProgramType) String() string {
-	if i >= ProgramType(len(_ProgramType_index)-1) {
+	switch {
+	case i <= 32:
+		return _ProgramType_name_0[_ProgramType_index_0[i]:_ProgramType_index_0[i+1]]
+	case 268435457 <= i && i <= 268435460:
+		i -= 268435457
+		return _ProgramType_name_1[_ProgramType_index_1[i]:_ProgramType_index_1[i+1]]
+	case 268436454 <= i && i <= 268436455:
+		i -= 268436454
+		return _ProgramType_name_2[_ProgramType_index_2[i]:_ProgramType_index_2[i+1]]
+	default:
 		return "ProgramType(" + strconv.FormatInt(int64(i), 10) + ")"
 	}
-	return _ProgramType_name[_ProgramType_index[i]:_ProgramType_index[i+1]]
 }
 func _() {
 	// An "invalid array index" compiler error signifies that the constant values have changed.


Test patch:
diff --git a/collection_test.go b/collection_test.go
index 593cfe4..b30f073 100644
--- a/collection_test.go
+++ b/collection_test.go
@@ -104,8 +104,10 @@ func TestCollectionSpecCopy(t *testing.T) {
 	qt.Assert(t, testutils.IsDeepCopy(cs.Copy(), cs))
 }
 
-func TestCollectionSpecRewriteMaps(t *testing.T) {
-	insns := asm.Instructions{
+// Load key "0" from a map called "test-map" and return the value.
+var loadKeyFromMapProgramSpec = &ProgramSpec{
+	Type: SocketFilter,
+	Instructions: asm.Instructions{
 		// R1 map
 		asm.LoadMapPtr(asm.R1, 0).WithReference("test-map"),
 		// R2 key
@@ -114,11 +116,16 @@ func TestCollectionSpecRewriteMaps(t *testing.T) {
 		asm.StoreImm(asm.R2, 0, 0, asm.Word),
 		// Lookup map[0]
 		asm.FnMapLookupElem.Call(),
-		asm.JEq.Imm(asm.R0, 0, "ret"),
+		asm.JEq.Imm(asm.R0, 0, "error"),
 		asm.LoadMem(asm.R0, asm.R0, 0, asm.Word),
+		asm.Ja.Label("ret"),
+		// Windows doesn't allow directly using R0 result from FnMapLookupElem.
+		asm.Mov.Imm(asm.R0, 0).WithSymbol("error"),
 		asm.Return().WithSymbol("ret"),
-	}
+	},
+}
 
+func TestCollectionSpecRewriteMaps(t *testing.T) {
 	cs := &CollectionSpec{
 		Maps: map[string]*MapSpec{
 			"test-map": {
@@ -129,11 +136,7 @@ func TestCollectionSpecRewriteMaps(t *testing.T) {
 			},
 		},
 		Programs: map[string]*ProgramSpec{
-			"test-prog": {
-				Type:         SocketFilter,
-				Instructions: insns,
-				License:      "MIT",
-			},
+			"test-prog": loadKeyFromMapProgramSpec.Copy(),
 		},
 	}
 
@@ -170,20 +173,6 @@ func TestCollectionSpecRewriteMaps(t *testing.T) {
 }
 
 func TestCollectionSpecMapReplacements(t *testing.T) {
-	insns := asm.Instructions{
-		// R1 map
-		asm.LoadMapPtr(asm.R1, 0).WithReference("test-map"),
-		// R2 key
-		asm.Mov.Reg(asm.R2, asm.R10),
-		asm.Add.Imm(asm.R2, -4),
-		asm.StoreImm(asm.R2, 0, 0, asm.Word),
-		// Lookup map[0]
-		asm.FnMapLookupElem.Call(),
-		asm.JEq.Imm(asm.R0, 0, "ret"),
-		asm.LoadMem(asm.R0, asm.R0, 0, asm.Word),
-		asm.Return().WithSymbol("ret"),
-	}
-
 	cs := &CollectionSpec{
 		Maps: map[string]*MapSpec{
 			"test-map": {
@@ -194,11 +183,7 @@ func TestCollectionSpecMapReplacements(t *testing.T) {
 			},
 		},
 		Programs: map[string]*ProgramSpec{
-			"test-prog": {
-				Type:         SocketFilter,
-				Instructions: insns,
-				License:      "MIT",
-			},
+			"test-prog": loadKeyFromMapProgramSpec.Copy(),
 		},
 	}
 
@@ -655,6 +640,8 @@ func BenchmarkNewCollection(b *testing.B) {
 		m.Pinning = PinNone
 	}
 
+	spec = fixupCollectionSpec(spec)
+
 	b.ReportAllocs()
 	b.ResetTimer()
 
@@ -674,6 +661,8 @@ func BenchmarkNewCollectionManyProgs(b *testing.B) {
 		b.Fatal(err)
 	}
 
+	spec = fixupCollectionSpec(spec)
+
 	b.ReportAllocs()
 	b.ResetTimer()
 
@@ -781,10 +770,4 @@ func ExampleCollectionSpec_LoadAndAssign() {
 	}
 	defer objs.Program.Close()
 	defer objs.Map.Close()
-
-	fmt.Println(objs.Program.Type())
-	fmt.Println(objs.Map.Type())
-
-	// Output: SocketFilter
-	// Array
 }
diff --git a/elf_reader_test.go b/elf_reader_test.go
index 6dbf51d..c81435b 100644
--- a/elf_reader_test.go
+++ b/elf_reader_test.go
@@ -257,7 +257,7 @@ func TestLoadCollectionSpec(t *testing.T) {
 		}
 
 		have.Maps["array_of_hash_map"].InnerMap = have.Maps["hash_map"]
-		coll, err := NewCollectionWithOptions(have, CollectionOptions{
+		coll, err := newCollection(t, have, &CollectionOptions{
 			Maps: MapOptions{
 				PinPath: testutils.TempBPFFS(t),
 			},
@@ -270,7 +270,6 @@ func TestLoadCollectionSpec(t *testing.T) {
 		if err != nil {
 			t.Fatal(err)
 		}
-		defer coll.Close()
 
 		ret, _, err := coll.Programs["xdp_prog"].Test(internal.EmptyBPFContext)
 		if err != nil {
@@ -304,11 +303,7 @@ func TestDataSections(t *testing.T) {
 		Program *Program `ebpf:"data_sections"`
 	}
 
-	err = coll.LoadAndAssign(&obj, nil)
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
+	mustLoadAndAssign(t, coll, &obj, nil)
 	defer obj.Program.Close()
 
 	ret, _, err := obj.Program.Test(internal.EmptyBPFContext)
@@ -342,11 +337,7 @@ func TestInlineASMConstant(t *testing.T) {
 		Program *Program `ebpf:"asm_relocation"`
 	}
 
-	err = coll.LoadAndAssign(&obj, nil)
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
+	mustLoadAndAssign(t, coll, &obj, nil)
 	obj.Program.Close()
 }
 
@@ -369,12 +360,8 @@ func TestFreezeRodata(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	err = spec.LoadAndAssign(&obj, nil)
-	testutils.SkipIfNotSupported(t, err)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer obj.Program.Close()
+	mustLoadAndAssign(t, spec, &obj, nil)
+	obj.Program.Close()
 }
 
 func TestCollectionSpecDetach(t *testing.T) {
@@ -447,12 +434,11 @@ func TestLoadInitializedBTFMap(t *testing.T) {
 				t.Skipf("Skipping %s collection", coll.ByteOrder)
 			}
 
-			tmp, err := NewCollection(coll)
+			_, err := newCollection(t, coll, nil)
 			testutils.SkipIfNotSupported(t, err)
 			if err != nil {
 				t.Fatal("NewCollection failed:", err)
 			}
-			tmp.Close()
 		})
 
 		t.Run("prog_array", func(t *testing.T) {
@@ -554,12 +540,11 @@ func TestStringSection(t *testing.T) {
 		t.Fatal("Read only data maps should have the prog-read-only flag set")
 	}
 
-	coll, err := NewCollection(spec)
+	coll, err := newCollection(t, spec, nil)
 	testutils.SkipIfNotSupported(t, err)
 	if err != nil {
 		t.Fatalf("new collection: %s", err)
 	}
-	defer coll.Close()
 
 	prog := coll.Programs["filter"]
 	if prog == nil {
@@ -621,14 +606,18 @@ func TestTailCall(t *testing.T) {
 	var obj struct {
 		TailMain  *Program `ebpf:"tail_main"`
 		ProgArray *Map     `ebpf:"prog_array_init"`
+		// Windows evicts programs from the tail call array when the last
+		// user space reference is closed. This is not the case on Linux.
+		Tail *Program `ebpf:"tail_1"`
 	}
 
-	err = spec.LoadAndAssign(&obj, nil)
+	err = loadAndAssign(t, spec, &obj, nil)
 	testutils.SkipIfNotSupported(t, err)
 	if err != nil {
 		t.Fatal(err)
 	}
 	defer obj.TailMain.Close()
+	defer obj.Tail.Close()
 	defer obj.ProgArray.Close()
 
 	ret, _, err := obj.TailMain.Test(internal.EmptyBPFContext)
@@ -842,7 +831,7 @@ func TestSubprogRelocation(t *testing.T) {
 		HashMap *Map     `ebpf:"hash_map"`
 	}
 
-	err = spec.LoadAndAssign(&obj, nil)
+	err = loadAndAssign(t, spec, &obj, nil)
 	testutils.SkipIfNotSupported(t, err)
 	if err != nil {
 		t.Fatal(err)
@@ -877,12 +866,10 @@ func TestUnassignedProgArray(t *testing.T) {
 		// ProgArray *Map     `ebpf:"prog_array_init"`
 	}
 
-	err = spec.LoadAndAssign(&obj, nil)
+	err = loadAndAssign(t, spec, &obj, nil)
 	testutils.SkipIfNotSupported(t, err)
-	if err == nil {
-		obj.TailMain.Close()
-		t.Fatal("Expecting LoadAndAssign to return error")
-	}
+	defer obj.TailMain.Close()
+	qt.Assert(t, qt.IsNotNil(err))
 }
 
 func TestIPRoute2Compat(t *testing.T) {
diff --git a/helpers_test.go b/helpers_test.go
index d6271b9..30ea660 100644
--- a/helpers_test.go
+++ b/helpers_test.go
@@ -9,10 +9,15 @@ import (
 
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/btf"
+	"github.com/cilium/ebpf/internal/platform"
 	"github.com/cilium/ebpf/internal/testutils"
 )
 
 var haveTestmod = sync.OnceValues(func() (bool, error) {
+	if platform.IsWindows {
+		return false, nil
+	}
+
 	// See https://github.com/torvalds/linux/commit/290248a5b7d829871b3ea3c62578613a580a1744
 	testmod, err := btf.FindHandle(func(info *btf.HandleInfo) bool {
 		return info.IsModule() && info.Name == "bpf_testmod"
@@ -42,6 +47,8 @@ func requireTestmod(tb testing.TB) {
 func newMap(tb testing.TB, spec *MapSpec, opts *MapOptions) (*Map, error) {
 	tb.Helper()
 
+	spec = fixupMapSpec(spec)
+
 	if opts == nil {
 		opts = new(MapOptions)
 	}
@@ -100,6 +107,8 @@ func newProgram(tb testing.TB, spec *ProgramSpec, opts *ProgramOptions) (*Progra
 		opts = new(ProgramOptions)
 	}
 
+	spec = fixupProgramSpec(spec)
+
 	prog, err := NewProgramWithOptions(spec, *opts)
 	testutils.SkipIfNotSupportedOnOS(tb, err)
 	if err != nil {
@@ -111,6 +120,8 @@ func newProgram(tb testing.TB, spec *ProgramSpec, opts *ProgramOptions) (*Progra
 }
 
 func mustNewProgram(tb testing.TB, spec *ProgramSpec, opts *ProgramOptions) *Program {
+	tb.Helper()
+
 	prog, err := newProgram(tb, spec, opts)
 	qt.Assert(tb, qt.IsNil(err))
 	return prog
@@ -149,11 +160,14 @@ func createBasicProgram(tb testing.TB) *Program {
 func newCollection(tb testing.TB, spec *CollectionSpec, opts *CollectionOptions) (*Collection, error) {
 	tb.Helper()
 
+	spec = fixupCollectionSpec(spec)
+
 	if opts == nil {
 		opts = new(CollectionOptions)
 	}
 
 	c, err := NewCollectionWithOptions(spec, *opts)
+	testutils.SkipIfNotSupportedOnOS(tb, err)
 	if err != nil {
 		return nil, err
 	}
@@ -171,6 +185,7 @@ func mustNewCollection(tb testing.TB, spec *CollectionSpec, opts *CollectionOpti
 
 func loadAndAssign(tb testing.TB, spec *CollectionSpec, to any, opts *CollectionOptions) error {
 	tb.Helper()
+	spec = fixupCollectionSpec(spec)
 	err := spec.LoadAndAssign(to, opts)
 	testutils.SkipIfNotSupported(tb, err)
 	return err
@@ -179,3 +194,117 @@ func loadAndAssign(tb testing.TB, spec *CollectionSpec, to any, opts *Collection
 func mustLoadAndAssign(tb testing.TB, spec *CollectionSpec, to any, opts *CollectionOptions) {
 	qt.Assert(tb, qt.IsNil(loadAndAssign(tb, spec, to, opts)))
 }
+
+// The functions below translate Linux types to their Windows equivalents, if
+// possible. This allows running most tests on Windows without modification.
+
+func fixupMapType(typ MapType) MapType {
+	if !platform.IsWindows {
+		return typ
+	}
+
+	switch typ {
+	case Array:
+		return WindowsArray
+	case Hash:
+		return WindowsHash
+	case ProgramArray:
+		return WindowsProgramArray
+	case PerCPUHash:
+		return WindowsPerCPUHash
+	case PerCPUArray:
+		return WindowsPerCPUArray
+	case LRUHash:
+		return WindowsLRUHash
+	case LRUCPUHash:
+		return WindowsLRUCPUHash
+	case ArrayOfMaps:
+		return WindowsArrayOfMaps
+	case HashOfMaps:
+		return WindowsHashOfMaps
+	case LPMTrie:
+		return WindowsLPMTrie
+	case Queue:
+		return WindowsQueue
+	case Stack:
+		return WindowsStack
+	case RingBuf:
+		return WindowsRingBuf
+	default:
+		return typ
+	}
+}
+
+func fixupMapSpec(spec *MapSpec) *MapSpec {
+	if !platform.IsWindows {
+		return spec
+	}
+
+	spec = spec.Copy()
+	spec.Type = fixupMapType(spec.Type)
+	if spec.InnerMap != nil {
+		spec.InnerMap.Type = fixupMapType(spec.InnerMap.Type)
+	}
+
+	return spec
+}
+
+func fixupProgramType(typ ProgramType) ProgramType {
+	if !platform.IsWindows {
+		return typ
+	}
+
+	switch typ {
+	case SocketFilter:
+		return WindowsXDPTest
+	case XDP:
+		return WindowsXDPTest
+	default:
+		return typ
+	}
+}
+
+func fixupProgramSpec(spec *ProgramSpec) *ProgramSpec {
+	if !platform.IsWindows {
+		return spec
+	}
+
+	spec = spec.Copy()
+	spec.Type = fixupProgramType(spec.Type)
+
+	for i, ins := range spec.Instructions {
+		if ins.IsBuiltinCall() {
+			switch asm.BuiltinFunc(ins.Constant) {
+			case asm.FnMapUpdateElem:
+				spec.Instructions[i].Constant = int64(asm.WindowsFnMapUpdateElem)
+			case asm.FnMapLookupElem:
+				spec.Instructions[i].Constant = int64(asm.WindowsFnMapLookupElem)
+			case asm.FnTailCall:
+				spec.Instructions[i].Constant = int64(asm.WindowsFnTailCall)
+			}
+		}
+	}
+
+	return spec
+}
+
+func fixupCollectionSpec(spec *CollectionSpec) *CollectionSpec {
+	if !platform.IsWindows {
+		return spec
+	}
+
+	spec = spec.Copy()
+	for name := range spec.Maps {
+		spec.Maps[name] = fixupMapSpec(spec.Maps[name])
+	}
+
+	for name := range spec.Programs {
+		spec.Programs[name] = fixupProgramSpec(spec.Programs[name])
+	}
+
+	for _, varSpec := range spec.Variables {
+		varSpec.m = spec.Maps[varSpec.m.Name]
+	}
+
+	return spec
+}
diff --git a/info_test.go b/info_test.go
index 89f151d..55d5feb 100644
--- a/info_test.go
+++ b/info_test.go
@@ -85,27 +85,30 @@ func TestMapInfoFromProcOuterMap(t *testing.T) {
 	qt.Assert(t, qt.Equals(info.MaxEntries, 2))
 }
 
-func validateProgInfo(t *testing.T, info *ProgramInfo) {
+func validateProgInfo(t *testing.T, spec *ProgramSpec, info *ProgramInfo) {
 	t.Helper()
 
-	qt.Assert(t, qt.Equals(info.Type, SocketFilter))
-	qt.Assert(t, qt.Equals(info.Tag, "d7edec644f05498d"))
+	qt.Assert(t, qt.Equals(info.Type, spec.Type))
+	if info.Tag != "" {
+		qt.Assert(t, qt.Equals(info.Tag, "d7edec644f05498d"))
+	}
 }
 
 func TestProgramInfo(t *testing.T) {
-	prog := createBasicProgram(t)
+	spec := fixupProgramSpec(basicProgramSpec)
+	prog := mustNewProgram(t, spec, nil)
 
 	info, err := newProgramInfoFromFd(prog.fd)
 	testutils.SkipIfNotSupported(t, err)
 	qt.Assert(t, qt.IsNil(err))
 
-	validateProgInfo(t, info)
+	validateProgInfo(t, spec, info)
 
 	id, ok := info.ID()
 	qt.Assert(t, qt.IsTrue(ok))
 	qt.Assert(t, qt.Not(qt.Equals(id, 0)))
 
-	if testutils.IsVersionLessThan(t, "4.15") {
+	if testutils.IsVersionLessThan(t, "4.15", "windows:0.20") {
 		qt.Assert(t, qt.Equals(info.Name, ""))
 	} else {
 		qt.Assert(t, qt.Equals(info.Name, "test"))
@@ -155,13 +158,14 @@ func TestProgramInfo(t *testing.T) {
 }
 
 func TestProgramInfoProc(t *testing.T) {
-	prog := createBasicProgram(t)
+	spec := fixupProgramSpec(basicProgramSpec)
+	prog := mustNewProgram(t, spec, nil)
 
 	info, err := newProgramInfoFromProc(prog.fd)
 	testutils.SkipIfNotSupported(t, err)
 	qt.Assert(t, qt.IsNil(err))
 
-	validateProgInfo(t, info)
+	validateProgInfo(t, spec, info)
 }
 
 func TestProgramInfoBTF(t *testing.T) {
@@ -234,7 +238,7 @@ func TestProgramInfoMapIDs(t *testing.T) {
 
 	ids, ok := info.MapIDs()
 	switch {
-	case testutils.IsVersionLessThan(t, "4.15"):
+	case testutils.IsVersionLessThan(t, "4.15", "windows:0.20"):
 		qt.Assert(t, qt.IsFalse(ok))
 		qt.Assert(t, qt.HasLen(ids, 0))
 
@@ -259,7 +263,7 @@ func TestProgramInfoMapIDsNoMaps(t *testing.T) {
 
 	ids, ok := info.MapIDs()
 	switch {
-	case testutils.IsVersionLessThan(t, "4.15"):
+	case testutils.IsVersionLessThan(t, "4.15", "windows:0.20"):
 		qt.Assert(t, qt.IsFalse(ok))
 		qt.Assert(t, qt.HasLen(ids, 0))
 
@@ -331,6 +335,7 @@ func TestStats(t *testing.T) {
 	}
 
 	if err := testStats(prog); err != nil {
+		testutils.SkipIfNotSupportedOnOS(t, err)
 		t.Error(err)
 	}
 }
@@ -343,6 +348,7 @@ func BenchmarkStats(b *testing.B) {
 
 	for n := 0; n < b.N; n++ {
 		if err := testStats(prog); err != nil {
+			testutils.SkipIfNotSupportedOnOS(b, err)
 			b.Fatal(fmt.Errorf("iter %d: %w", n, err))
 		}
 	}
@@ -363,19 +369,19 @@ func testStats(prog *Program) error {
 
 	stats, err := EnableStats(uint32(sys.BPF_STATS_RUN_TIME))
 	if err != nil {
-		return fmt.Errorf("failed to enable stats: %v", err)
+		return fmt.Errorf("failed to enable stats: %w", err)
 	}
 	defer stats.Close()
 
 	// Program execution with runtime statistics enabled.
 	// Should increase both runtime and run counter.
 	if _, _, err := prog.Test(in); err != nil {
-		return fmt.Errorf("failed to trigger program: %v", err)
+		return fmt.Errorf("failed to trigger program: %w", err)
 	}
 
 	pi, err := prog.Info()
 	if err != nil {
-		return fmt.Errorf("failed to get ProgramInfo: %v", err)
+		return fmt.Errorf("failed to get ProgramInfo: %w", err)
 	}
 
 	rc, ok := pi.RunCount()
@@ -399,18 +405,18 @@ func testStats(prog *Program) error {
 	lt := rt
 
 	if err := stats.Close(); err != nil {
-		return fmt.Errorf("failed to disable statistics: %v", err)
+		return fmt.Errorf("failed to disable statistics: %w", err)
 	}
 
 	// Second program execution, with runtime statistics gathering disabled.
 	// Total runtime and run counters are not expected to increase.
 	if _, _, err := prog.Test(in); err != nil {
-		return fmt.Errorf("failed to trigger program: %v", err)
+		return fmt.Errorf("failed to trigger program: %w", err)
 	}
 
 	pi, err = prog.Info()
 	if err != nil {
-		return fmt.Errorf("failed to get ProgramInfo: %v", err)
+		return fmt.Errorf("failed to get ProgramInfo: %w", err)
 	}
 
 	rc, ok = pi.RunCount()
@@ -448,7 +454,7 @@ func TestProgInfoExtBTF(t *testing.T) {
 		Main *Program `ebpf:"xdp_prog"`
 	}
 
-	err = spec.LoadAndAssign(&obj, nil)
+	err = loadAndAssign(t, spec, &obj, nil)
 	testutils.SkipIfNotSupported(t, err)
 	if err != nil {
 		t.Fatal(err)
diff --git a/internal/platform/constants_test.go b/internal/platform/constants_test.go
index 5782469..473ba04 100644
--- a/internal/platform/constants_test.go
+++ b/internal/platform/constants_test.go
@@ -10,6 +10,7 @@ func TestConstant(t *testing.T) {
 	const maxConstant = ^uint32(platformMask)
 	for _, plat := range []string{
 		Linux,
+		Windows,
 	} {
 		t.Run(plat, func(t *testing.T) {
 			c, err := EncodeConstant[uint32](plat, maxConstant)
diff --git a/map_test.go b/map_test.go
index ac7122c..9bb7c02 100644
--- a/map_test.go
+++ b/map_test.go
@@ -7,6 +7,7 @@ import (
 	"math"
 	"os"
 	"path/filepath"
+	"runtime"
 	"slices"
 	"sort"
 	"testing"
@@ -17,6 +18,7 @@ import (
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/btf"
 	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/platform"
 	"github.com/cilium/ebpf/internal/sys"
 	"github.com/cilium/ebpf/internal/testutils"
 	"github.com/cilium/ebpf/internal/unix"
@@ -102,10 +104,6 @@ func TestMapSpecCopy(t *testing.T) {
 }
 
 func TestMapBatch(t *testing.T) {
-	if err := haveBatchAPI(); err != nil {
-		t.Skipf("batch api not available: %v", err)
-	}
-
 	contents := []uint32{
 		42, 4242, 23, 2323,
 	}
@@ -138,6 +136,7 @@ func TestMapBatch(t *testing.T) {
 			m := createMap(t, typ, uint32(len(contents)))
 			keys, values, _ := keysAndValuesForMap(m, contents)
 			count, err := m.BatchUpdate(keys, values, nil)
+			testutils.SkipIfNotSupported(t, err)
 			qt.Assert(t, qt.IsNil(err))
 			qt.Assert(t, qt.Equals(count, len(contents)))
 
@@ -162,6 +161,7 @@ func TestMapBatch(t *testing.T) {
 			m := createMap(t, typ, uint32(len(contents)))
 			keys, values, stride := keysAndValuesForMap(m, contents)
 			count, err := m.BatchUpdate(keys, values, nil)
+			testutils.SkipIfNotSupported(t, err)
 			qt.Assert(t, qt.IsNil(err))
 			qt.Assert(t, qt.Equals(count, len(contents)))
 
@@ -228,10 +228,12 @@ func TestMapLookupKeyNotFoundAllocations(t *testing.T) {
 	m := createMap(t, Array, 2)
 	defer m.Close()
 	var key, out uint32 = 3, 0
+	var err error
 
 	allocs := testing.AllocsPerRun(5, func() {
-		_ = m.Lookup(&key, &out)
+		err = m.Lookup(&key, &out)
 	})
+	qt.Assert(t, qt.ErrorIs(err, ErrKeyNotExist))
 	qt.Assert(t, qt.Equals(allocs, float64(0)))
 }
 
@@ -294,17 +296,11 @@ func TestMapClose(t *testing.T) {
 
 func TestBatchMapWithLock(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.13", "MAP BATCH BPF_F_LOCK")
-	file := testutils.NativeFile(t, "testdata/map_spin_lock-%s.elf")
-	spec, err := LoadCollectionSpec(file)
-	if err != nil {
-		t.Fatal("Can't parse ELF:", err)
-	}
 
-	coll, err := NewCollection(spec)
-	if err != nil {
-		t.Fatal("Can't parse ELF:", err)
-	}
-	defer coll.Close()
+	spec, err := LoadCollectionSpec(testutils.NativeFile(t, "testdata/map_spin_lock-%s.elf"))
+	qt.Assert(t, qt.IsNil(err))
+
+	coll := mustNewCollection(t, spec, nil)
 
 	type spinLockValue struct {
 		Cnt     uint32
@@ -319,6 +315,7 @@ func TestBatchMapWithLock(t *testing.T) {
 	keys := []uint32{0, 1}
 	values := []spinLockValue{{Cnt: 42}, {Cnt: 4242}}
 	count, err := m.BatchUpdate(keys, values, &BatchOptions{ElemFlags: uint64(UpdateLock)})
+	testutils.SkipIfNotSupportedOnOS(t, err)
 	if err != nil {
 		t.Fatalf("BatchUpdate: %v", err)
 	}
@@ -351,17 +348,11 @@ func TestBatchMapWithLock(t *testing.T) {
 
 func TestMapWithLock(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.13", "MAP BPF_F_LOCK")
-	file := testutils.NativeFile(t, "testdata/map_spin_lock-%s.elf")
-	spec, err := LoadCollectionSpec(file)
-	if err != nil {
-		t.Fatal("Can't parse ELF:", err)
-	}
 
-	coll, err := NewCollection(spec)
-	if err != nil {
-		t.Fatal("Can't parse ELF:", err)
-	}
-	defer coll.Close()
+	spec, err := LoadCollectionSpec(testutils.NativeFile(t, "testdata/map_spin_lock-%s.elf"))
+	qt.Assert(t, qt.IsNil(err))
+
+	coll := mustNewCollection(t, spec, nil)
 
 	type spinLockValue struct {
 		Cnt     uint32
@@ -376,6 +367,9 @@ func TestMapWithLock(t *testing.T) {
 	key := uint32(1)
 	value := spinLockValue{Cnt: 5}
 	err = m.Update(key, value, UpdateLock)
+	if platform.IsWindows && errors.Is(err, unix.EINVAL) {
+		t.Skip("Windows doesn't support UpdateLock")
+	}
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -458,11 +452,7 @@ func TestMapPin(t *testing.T) {
 func TestNestedMapPin(t *testing.T) {
 	m := createMapInMap(t, ArrayOfMaps, Array)
 
-	tmp, err := os.MkdirTemp("/sys/fs/bpf", "ebpf-test")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tmp)
+	tmp := testutils.TempBPFFS(t)
 
 	path := filepath.Join(tmp, "nested")
 	if err := m.Pin(path); err != nil {
@@ -470,7 +460,7 @@ func TestNestedMapPin(t *testing.T) {
 	}
 	m.Close()
 
-	m, err = LoadPinnedMap(path, nil)
+	m, err := LoadPinnedMap(path, nil)
 	testutils.SkipIfNotSupported(t, err)
 	if err != nil {
 		t.Fatal(err)
@@ -653,6 +643,9 @@ func TestMapLoadPinnedWithOptions(t *testing.T) {
 		array, err := LoadPinnedMap(path, &LoadPinOptions{
 			ReadOnly: true,
 		})
+		if platform.IsWindows && errors.Is(err, unix.EINVAL) {
+			t.Skip("Windows doesn't support file_flags in OBJ_GET")
+		}
 		testutils.SkipIfNotSupported(t, err)
 		if err != nil {
 			t.Fatal("Can't load map:", err)
@@ -668,6 +661,9 @@ func TestMapLoadPinnedWithOptions(t *testing.T) {
 		array, err := LoadPinnedMap(path, &LoadPinOptions{
 			WriteOnly: true,
 		})
+		if platform.IsWindows && errors.Is(err, unix.EINVAL) {
+			t.Skip("Windows doesn't support file_flags in OBJ_GET")
+		}
 		testutils.SkipIfNotSupported(t, err)
 		if err != nil {
 			t.Fatal("Can't load map:", err)
@@ -805,9 +801,8 @@ func TestNewMapInMapFromFD(t *testing.T) {
 
 	// Do not copy this, use Clone instead.
 	another, err := NewMapFromFD(testutils.DupFD(t, nested.FD()))
-	if err != nil {
-		t.Fatal("Can't create a new nested map from an FD")
-	}
+	testutils.SkipIfNotSupportedOnOS(t, err)
+	qt.Assert(t, qt.IsNil(err))
 	another.Close()
 }
 
@@ -1023,6 +1018,10 @@ func TestMapIterateHashKeyOneByteFull(t *testing.T) {
 }
 
 func TestMapGuessNonExistentKey(t *testing.T) {
+	if !platform.IsLinux {
+		t.Skip("No need to test linux quirk on", runtime.GOOS)
+	}
+
 	tests := []struct {
 		name    string
 		mapType MapType
@@ -1253,8 +1252,6 @@ func TestCgroupPerCPUStorageMarshaling(t *testing.T) {
 	}
 	testutils.SkipOnOldKernel(t, "5.9", "per-CPU CGoup storage with write from user space support")
 
-	cgroup := testutils.CreateCgroup(t)
-
 	arr := mustNewMap(t, &MapSpec{
 		Type:      PerCPUCGroupStorage,
 		KeySize:   uint32(unsafe.Sizeof(bpfCgroupStorageKey{})),
@@ -1274,6 +1271,8 @@ func TestCgroupPerCPUStorageMarshaling(t *testing.T) {
 		},
 	}, nil)
 
+	cgroup := testutils.CreateCgroup(t)
+
 	progAttachAttrs := sys.ProgAttachAttr{
 		TargetFdOrIfindex: uint32(cgroup.Fd()),
 		AttachBpfFd:       uint32(prog.FD()),
@@ -1573,8 +1572,6 @@ func TestMapPinning(t *testing.T) {
 }
 
 func TestMapHandle(t *testing.T) {
-	testutils.SkipOnOldKernel(t, "4.18", "btf_id in map info")
-
 	kv := &btf.Int{Size: 4}
 	m := mustNewMap(t, &MapSpec{
 		Type:       Hash,
@@ -1586,6 +1583,7 @@ func TestMapHandle(t *testing.T) {
 	}, nil)
 
 	h, err := m.Handle()
+	testutils.SkipIfNotSupported(t, err)
 	qt.Assert(t, qt.IsNil(err))
 	qt.Assert(t, qt.IsNotNil(h))
 	defer h.Close()
@@ -2070,9 +2068,6 @@ func ExampleMap_perCPU() {
 	if err := entries.Err(); err != nil {
 		panic(err)
 	}
-	// Output:
-	// Value of key 0 on all CPUs: 4
-	// Value of key 1 on all CPUs: 5
 }
 
 // It is possible to use unsafe.Pointer to avoid marshalling
@@ -2106,7 +2101,6 @@ func ExampleMap_zeroCopy() {
 	}
 
 	fmt.Printf("The value is: %d\n", value)
-	// Output: The value is: 23
 }
 
 func ExampleMap_NextKey() {
@@ -2142,7 +2136,6 @@ func ExampleMap_NextKey() {
 	// Order of keys is non-deterministic due to randomized map seed
 	sort.Strings(keys)
 	fmt.Printf("Keys are %v\n", keys)
-	// Output: Keys are [hello world]
 }
 
 // ExampleMap_Iterate demonstrates how to iterate over all entries
@@ -2182,10 +2175,6 @@ func ExampleMap_Iterate() {
 	for k, v := range values {
 		fmt.Printf("key: %s, value: %d\n", k, v)
 	}
-
-	// Unordered output:
-	// key: hello, value: 21
-	// key: world, value: 42
 }
 
 // It is possible to iterate nested maps and program arrays by
@@ -2256,8 +2245,4 @@ func ExampleMap_Iterate_nestedMapsAndProgramArrays() {
 	if err := entries.Err(); err != nil {
 		panic(fmt.Sprint("Iterator encountered an error:", err))
 	}
-	// Output:
-	// outerKey: 0
-	//	innerKey 0 innerValue 1
-	// 	innerKey 1 innerValue 2
 }
diff --git a/prog_linux_test.go b/prog_linux_test.go
index dcc1990..392867b 100644
--- a/prog_linux_test.go
+++ b/prog_linux_test.go
@@ -1,11 +1,16 @@
 package ebpf
 
 import (
+	"fmt"
 	"math"
 	"runtime"
+	"slices"
 	"testing"
 	"time"
 
+	"github.com/go-quicktest/qt"
+
+	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/testutils"
 	"github.com/cilium/ebpf/internal/unix"
@@ -82,3 +87,51 @@ func TestProgramTestRunInterrupt(t *testing.T) {
 		}
 	}
 }
+
+func TestProgramVerifierLogLinux(t *testing.T) {
+	check := func(t *testing.T, err error) {
+		t.Helper()
+
+		var ve *internal.VerifierError
+		qt.Assert(t, qt.ErrorAs(err, &ve))
+
+		loglen := len(fmt.Sprintf("%+v", ve))
+		qt.Assert(t, qt.IsTrue(loglen > minVerifierLogSize),
+			qt.Commentf("Log buffer didn't grow past minimum, got %d bytes", loglen))
+	}
+
+	// Generate a base program of sufficient size whose verifier log does not fit
+	// in the minimum buffer size. Stay under 4096 insn limit of older kernels.
+	var base asm.Instructions
+	for i := 0; i < 4093; i++ {
+		base = append(base, asm.Mov.Reg(asm.R0, asm.R1))
+	}
+
+	// Touch R10 (read-only frame pointer) to reliably force a verifier error.
+	invalid := slices.Clone(base)
+	invalid = append(invalid, asm.Mov.Reg(asm.R10, asm.R0))
+	invalid = append(invalid, asm.Return())
+
+	valid := slices.Clone(base)
+	valid = append(valid, asm.Return())
+
+	// Start out with testing against the invalid program.
+	spec := &ProgramSpec{
+		Type:         SocketFilter,
+		License:      "MIT",
+		Instructions: invalid,
+	}
+
+	_, err := newProgram(t, spec, nil)
+	check(t, err)
+
+	// Run tests against a valid program from here on out.
+	spec.Instructions = valid
+
+	// Explicitly request verifier log for a valid program and a start size.
+	prog := mustNewProgram(t, spec, &ProgramOptions{
+		LogLevel:     LogLevelInstruction,
+		LogSizeStart: minVerifierLogSize * 2,
+	})
+	qt.Assert(t, qt.IsTrue(len(prog.VerifierLog) > minVerifierLogSize))
+}
diff --git a/prog_test.go b/prog_test.go
index 924ec4a..07a3027 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -8,7 +8,7 @@ import (
 	"math"
 	"os"
 	"path/filepath"
-	"slices"
+	"runtime"
 	"strings"
 	"syscall"
 	"testing"
@@ -18,6 +18,7 @@ import (
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/btf"
 	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/platform"
 	"github.com/cilium/ebpf/internal/sys"
 	"github.com/cilium/ebpf/internal/testutils"
 	"github.com/cilium/ebpf/internal/unix"
@@ -50,6 +51,16 @@ func TestProgramRun(t *testing.T) {
 		asm.Return(),
 	)
 
+	if platform.IsWindows {
+		// Windows uses an incompatible context for XDP. Pointers are
+		// 64 bit.
+		// See https://github.com/microsoft/ebpf-for-windows/issues/3873
+		// r2 = *(r1+8)
+		ins[0] = asm.LoadMem(asm.R2, asm.R1, 8, asm.DWord)
+		// r1 = *(r1+0)
+		ins[1] = asm.LoadMem(asm.R1, asm.R1, 0, asm.DWord)
+	}
+
 	t.Log(ins)
 
 	prog := mustNewProgram(t, &ProgramSpec{
@@ -89,15 +100,23 @@ func TestProgramRunWithOptions(t *testing.T) {
 	prog := createProgram(t, XDP, int64(sys.XDP_ABORTED))
 
 	buf := internal.EmptyBPFContext
-	xdp := sys.XdpMd{
-		Data:    0,
-		DataEnd: uint32(len(buf)),
+	var in, out any
+	if platform.IsWindows {
+		type winXdpMd struct {
+			Data, DataEnd, DataMeta uint64
+			Ifindex                 uint32
+		}
+		in = &winXdpMd{Data: 0, DataEnd: uint64(len(buf))}
+		out = &winXdpMd{}
+	} else {
+		in = &sys.XdpMd{Data: 0, DataEnd: uint32(len(buf))}
+		out = &sys.XdpMd{}
 	}
-	xdpOut := sys.XdpMd{}
+
 	opts := RunOptions{
 		Data:       buf,
-		Context:    xdp,
-		ContextOut: &xdpOut,
+		Context:    in,
+		ContextOut: out,
 	}
 	ret, err := prog.Run(&opts)
 	testutils.SkipIfNotSupported(t, err)
@@ -109,9 +128,7 @@ func TestProgramRunWithOptions(t *testing.T) {
 		t.Error("Expected return value to be 0, got", ret)
 	}
 
-	if xdp != xdpOut {
-		t.Errorf("Expect xdp (%+v) == xdpOut (%+v)", xdp, xdpOut)
-	}
+	qt.Assert(t, qt.DeepEquals(out, in))
 }
 
 func TestProgramRunRawTracepoint(t *testing.T) {
@@ -164,7 +181,8 @@ func TestProgramClose(t *testing.T) {
 }
 
 func TestProgramPin(t *testing.T) {
-	prog := createBasicProgram(t)
+	spec := fixupProgramSpec(basicProgramSpec)
+	prog := mustNewProgram(t, spec, nil)
 
 	tmp := testutils.TempBPFFS(t)
 
@@ -185,18 +203,12 @@ func TestProgramPin(t *testing.T) {
 	}
 	defer prog.Close()
 
-	if prog.Type() != SocketFilter {
-		t.Error("Expected pinned program to have type SocketFilter, but got", prog.Type())
-	}
+	qt.Assert(t, qt.Equals(prog.Type(), spec.Type))
 
 	if haveObjName() == nil {
-		if prog.name != "test" {
-			t.Errorf("Expected program to have object name 'test', got '%s'", prog.name)
-		}
+		qt.Assert(t, qt.Equals(prog.name, "test"))
 	} else {
-		if prog.name != "program" {
-			t.Errorf("Expected program to have file name 'program', got '%s'", prog.name)
-		}
+		qt.Assert(t, qt.Equals(prog.name, "program"))
 	}
 
 	if !prog.IsPinned() {
@@ -266,9 +278,19 @@ func TestProgramVerifierOutputOnError(t *testing.T) {
 		t.Fatal("NewProgram does return an unwrapped VerifierError")
 	}
 
-	if !strings.Contains(ve.Error(), "R0 !read_ok") {
-		t.Logf("%+v", ve)
-		t.Error("Missing verifier log in error summary")
+	switch {
+	case platform.IsLinux:
+		if !strings.Contains(ve.Error(), "R0 !read_ok") {
+			t.Logf("%+v", ve)
+			t.Error("Missing verifier log in error summary")
+		}
+	case platform.IsWindows:
+		if !strings.Contains(ve.Error(), "r0.type == number") {
+			t.Logf("%+v", ve)
+			t.Error("Missing verifier log in error summary")
+		}
+	default:
+		t.Error("Unsupported platform", runtime.GOOS)
 	}
 }
 
@@ -292,26 +314,23 @@ func TestProgramVerifierLog(t *testing.T) {
 
 		var ve *internal.VerifierError
 		qt.Assert(t, qt.ErrorAs(err, &ve))
-
-		loglen := len(fmt.Sprintf("%+v", ve))
-		qt.Assert(t, qt.IsTrue(loglen > minVerifierLogSize),
-			qt.Commentf("Log buffer didn't grow past minimum, got %d bytes", loglen))
-	}
-
-	// Generate a base program of sufficient size whose verifier log does not fit
-	// in the minimum buffer size. Stay under 4096 insn limit of older kernels.
-	var base asm.Instructions
-	for i := 0; i < 4093; i++ {
-		base = append(base, asm.Mov.Reg(asm.R0, asm.R1))
+		loglen := 0
+		for _, line := range ve.Log {
+			loglen += len(line)
+		}
+		qt.Assert(t, qt.IsTrue(loglen > 0))
 	}
 
 	// Touch R10 (read-only frame pointer) to reliably force a verifier error.
-	invalid := slices.Clone(base)
-	invalid = append(invalid, asm.Mov.Reg(asm.R10, asm.R0))
-	invalid = append(invalid, asm.Return())
+	invalid := asm.Instructions{
+		asm.Mov.Reg(asm.R10, asm.R0),
+		asm.Return(),
+	}
 
-	valid := slices.Clone(base)
-	valid = append(valid, asm.Return())
+	valid := asm.Instructions{
+		asm.Mov.Imm(asm.R0, 0),
+		asm.Return(),
+	}
 
 	// Start out with testing against the invalid program.
 	spec := &ProgramSpec{
@@ -324,12 +343,6 @@ func TestProgramVerifierLog(t *testing.T) {
 	_, err := newProgram(t, spec, nil)
 	check(t, err)
 
-	// Explicitly request a verifier log for an invalid program.
-	_, err = newProgram(t, spec, &ProgramOptions{
-		LogLevel: LogLevelInstruction,
-	})
-	check(t, err)
-
 	// Disabling the verifier log should result in a VerifierError without a log.
 	_, err = newProgram(t, spec, &ProgramOptions{
 		LogDisabled: true,
@@ -338,6 +351,12 @@ func TestProgramVerifierLog(t *testing.T) {
 	qt.Assert(t, qt.ErrorAs(err, &ve))
 	qt.Assert(t, qt.HasLen(ve.Log, 0))
 
+	// Explicitly request a verifier log for an invalid program.
+	_, err = newProgram(t, spec, &ProgramOptions{
+		LogLevel: LogLevelInstruction,
+	})
+	check(t, err)
+
 	// Run tests against a valid program from here on out.
 	spec.Instructions = valid
 
@@ -346,19 +365,11 @@ func TestProgramVerifierLog(t *testing.T) {
 	prog := mustNewProgram(t, spec, nil)
 	qt.Assert(t, qt.HasLen(prog.VerifierLog, 0))
 
-	// Explicitly request verifier log for a valid program. If a log is requested
-	// and the buffer is too small, ENOSPC occurs even for valid programs.
+	// Explicitly request verifier log for a valid program.
 	prog = mustNewProgram(t, spec, &ProgramOptions{
 		LogLevel: LogLevelInstruction,
 	})
-	qt.Assert(t, qt.IsTrue(len(prog.VerifierLog) > minVerifierLogSize))
-
-	// Repeat the previous test with a larger starting buffer size.
-	prog = mustNewProgram(t, spec, &ProgramOptions{
-		LogLevel:     LogLevelInstruction,
-		LogSizeStart: minVerifierLogSize * 2,
-	})
-	qt.Assert(t, qt.IsTrue(len(prog.VerifierLog) > minVerifierLogSize))
+	qt.Assert(t, qt.Not(qt.HasLen(prog.VerifierLog, 0)))
 }
 
 func TestProgramWithUnsatisfiedMap(t *testing.T) {
@@ -459,7 +470,8 @@ func TestProgramMarshaling(t *testing.T) {
 }
 
 func TestProgramFromFD(t *testing.T) {
-	prog := createBasicProgram(t)
+	spec := fixupProgramSpec(basicProgramSpec)
+	prog := mustNewProgram(t, spec, nil)
 
 	// If you're thinking about copying this, don't. Use
 	// Clone() instead.
@@ -475,9 +487,7 @@ func TestProgramFromFD(t *testing.T) {
 		t.Errorf("Expected program to have name test, got '%s'", prog2.name)
 	}
 
-	if prog2.typ != SocketFilter {
-		t.Errorf("Expected program to have type SocketFilter, got '%s'", prog2.typ)
-	}
+	qt.Assert(t, qt.Equals(prog2.Type(), spec.Type))
 }
 
 func TestHaveProgTestRun(t *testing.T) {
@@ -600,7 +610,7 @@ func TestProgramSpecTag(t *testing.T) {
 		t.Fatal("Can't calculate tag:", err)
 	}
 
-	if tag != info.Tag {
+	if info.Tag != "" && tag != info.Tag {
 		t.Errorf("Calculated tag %s doesn't match kernel tag %s", tag, info.Tag)
 	}
 }
@@ -744,6 +754,7 @@ func TestProgramInstructions(t *testing.T) {
 	}
 
 	insns, err := pi.Instructions()
+	testutils.SkipIfNotSupportedOnOS(t, err)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/variable_test.go b/variable_test.go
index 76e854d..8efa66e 100644
--- a/variable_test.go
+++ b/variable_test.go
@@ -170,7 +170,8 @@ func TestVariableFallback(t *testing.T) {
 	obj := struct {
 		Data *Variable `ebpf:"var_data"`
 	}{}
-	qt.Assert(t, qt.IsNil(spec.LoadAndAssign(&obj, nil)))
+
+	mustLoadAndAssign(t, spec, &obj, nil)
 
 	// Expect either success or ErrNotSupported on all systems.
 	u32 := uint32(0)