12. Windows toolchain reference: MSVC, MSBuild, vcpkg, WinDbg, and Linux side by side¶
Time: ~2 hours Prerequisites: 02, 03, 04, 07
1. In plain words¶
You already use most of these tools at work. This module does two things. First, it fills the
gaps: the parts of the Windows toolchain people use daily without knowing what is underneath
(what /MD really selects, why LNK2038 happens, how a PDB is matched to a crash dump). Second, it
lines everything up against the Linux tools from modules 01 to 11, one row per concept, so that in
an interview you can answer "how would you do X on Linux?" by saying "on Windows I did it with Y;
on Linux it is Z, and here is the one way they differ".
That comparison is your selling point. Many Linux C++ candidates have never seen a crash dump from a customer machine; many Windows developers have never written a CMakeLists.txt. You will be able to talk about both precisely.
Analogy: two cities with the same services under different names. Both have a post office, a hospital and a bank. If you know where each is in one city and the name it goes by in the other, you can work in either one.
2. The real thing¶
2.1 The pieces of the MSVC toolchain¶
| Piece | File | Linux equivalent |
|---|---|---|
| Compiler driver | cl.exe |
g++ / clang++ |
| Linker | link.exe |
ld (GNU ld, gold, lld, mold), usually called through g++ |
| Static library tool | lib.exe |
ar |
| Binary inspector | dumpbin.exe (= link /dump) |
objdump, readelf, nm, ldd |
| Name demangler | undname.exe |
c++filt |
| Build engine | MSBuild (.sln, .vcxproj) |
Make, Ninja |
| Meta-build | CMake (also generates .sln) |
CMake |
| Package manager | vcpkg, NuGet, Conan | vcpkg, Conan, apt install lib*-dev |
| Platform headers and libs | Windows SDK (windows.h, kernel32.lib) |
glibc headers, /usr/include, /usr/lib |
| C runtime | UCRT (ucrtbase.dll) + vcruntime140.dll |
glibc (libc.so.6) |
| C++ standard library | MSVC STL (msvcp140.dll) |
libstdc++ (libstdc++.so.6) or libc++ |
| Debugger | Visual Studio debugger, WinDbg | GDB, LLDB |
| Debug info | .pdb files, separate from the binary |
DWARF sections inside the ELF, or split .debug files |
The compiler is not on PATH by default. The Developer Command Prompt (or
vcvars64.bat, or Enter-VsDevShell in PowerShell) sets PATH, INCLUDE, LIB and LIBPATH
to the right MSVC and SDK versions. CI tasks and CMake's Visual Studio generator do this for you.
On Linux the compiler is simply in /usr/bin.
2.2 cl.exe flags against GCC flags¶
| Purpose | MSVC | GCC / Clang |
|---|---|---|
| Standard | /std:c++17 |
-std=c++17 |
| Strict conformance | /permissive- (on by default in new VS projects) |
default; -pedantic |
Correct __cplusplus |
/Zc:__cplusplus |
always correct |
| Exceptions | /EHsc |
on by default |
| No optimisation | /Od |
-O0 |
| Optimise | /O2 (speed), /O1 (size) |
-O2, -Os |
| Whole program / LTO | /GL + /LTCG |
-flto |
| Debug info | /Zi (PDB), /Z7 (in .obj) |
-g |
| Linker debug info | /DEBUG (/DEBUG:FULL) |
part of -g |
| Warnings | /W4 |
-Wall -Wextra |
| All warnings | /Wall (very noisy) |
Clang -Weverything |
| Warnings as errors | /WX |
-Werror |
| Static analysis | /analyze |
clang-tidy, -fanalyzer (GCC) |
| Define | /DNAME=1 |
-DNAME=1 |
| Include dir | /I path |
-I path |
| AddressSanitizer | /fsanitize=address (VS 2019 16.9+) |
-fsanitize=address |
| UBSan, TSan, MSan | not available in MSVC | -fsanitize=undefined,thread,memory |
| Stack protector | /GS (default on) |
-fstack-protector-strong |
| Control-flow guard | /guard:cf |
-fcf-protection |
| Source charset | /utf-8 |
UTF-8 by default |
| Parallel compile | /MP |
make -j, Ninja (default parallel) |
| Remove unused code | /OPT:REF /OPT:ICF (linker) |
-ffunction-sections -Wl,--gc-sections, --icf (lld/gold) |
The runtime library switch has no direct GCC counterpart and is the source of most MSVC link errors:
| Flag | Meaning | CMake CMAKE_MSVC_RUNTIME_LIBRARY |
|---|---|---|
/MD |
release CRT and STL as DLLs | MultiThreadedDLL (CMake default) |
/MDd |
debug CRT as DLLs (checked iterators, debug heap) | MultiThreadedDebugDLL |
/MT |
release CRT linked statically into the module | MultiThreaded |
/MTd |
debug CRT statically | MultiThreadedDebug |
Every object file and static library linked into one module must use the same choice. Mixing
gives LNK2038: mismatch detected for 'RuntimeLibrary' or for '_ITERATOR_DEBUG_LEVEL' (Debug
and Release objects mixed). On Linux there is one glibc and one libstdc++ for the whole process,
so this class of error does not exist; the nearest equivalent is mixing _GLIBCXX_DEBUG and
normal objects, or the libstdc++ dual ABI (_GLIBCXX_USE_CXX11_ABI).
2.3 Libraries: .lib, .dll and the import library¶
| Concept | Windows | Linux |
|---|---|---|
| Object file | .obj (COFF) |
.o (ELF) |
| Static library | .lib (archive of .obj) |
.a (archive of .o) |
| Shared library | .dll |
.so |
| What you link against | the import library .lib that comes with the DLL |
the .so itself |
| Executable format | PE/COFF (.exe) |
ELF (no extension) |
| Exported by default | nothing: __declspec(dllexport) or a .def file |
everything with default visibility; -fvisibility=hidden to opt out |
| Run-time search | app directory, system directories, PATH (simplified) |
RPATH/RUNPATH, LD_LIBRARY_PATH, ld.so.cache, /lib, /usr/lib |
| Embedded search path | none (use app directory, SetDllDirectory, side-by-side manifests) |
-Wl,-rpath,'$ORIGIN/../lib' |
| Load at run time | LoadLibraryW / GetProcAddress / FreeLibrary |
dlopen / dlsym / dlclose |
| List dependencies | dumpbin /dependents app.exe, Dependencies tool |
ldd app, readelf -d app |
| List exports | dumpbin /exports x.dll |
nm -D --defined-only x.so |
| Position independent | DLLs are relocated (ASLR) using a relocation table | -fPIC required for .so |
The portable export macro every cross-platform library has:
// include/tcpkv/export.hpp - CMake's GenerateExportHeader produces the same thing for you.
#pragma once
#if defined(_WIN32)
# if defined(TCPKV_BUILDING_DLL)
# define TCPKV_API __declspec(dllexport) // compiling the DLL itself
# else
# define TCPKV_API __declspec(dllimport) // compiling a consumer of the DLL
# endif
#else
# define TCPKV_API __attribute__((visibility("default"))) // with -fvisibility=hidden
#endif
class TCPKV_API Store { /* ... */ };
Link errors, translated:
| MSVC | GNU ld | Meaning |
|---|---|---|
LNK2019: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z) referenced in function main |
undefined reference to 'add(int, int)' |
declared, used, never defined in what you linked |
LNK2001: unresolved external symbol |
same | same, reference not inside a function (e.g. a vtable or data) |
LNK2005: "x" already defined in a.obj |
multiple definition of 'x' |
ODR violation the linker can see (module 02) |
LNK1104: cannot open file 'foo.lib' |
cannot find -lfoo |
library path or name wrong |
LNK2038: mismatch detected for 'RuntimeLibrary' |
(no equivalent) | /MT and /MD objects mixed |
LNK4098: defaultlib 'LIBCMT' conflicts with use of other libs |
(no equivalent) | two CRTs requested; same root cause |
The three causes of an unresolved symbol are the same on both platforms: the definition is
missing from the link, the names differ (C vs C++ linkage, a different signature, __cdecl vs
__stdcall on 32-bit), or, for Windows only, the function is in a DLL that does not export it.
dumpbin /symbols foo.obj and undname play the role of nm -C.
2.4 MSBuild and CMake on Windows¶
MSBuild reads XML project files: a .sln lists .vcxproj projects; each .vcxproj lists
sources and per-configuration settings (Debug|x64, Release|x64), usually inheriting from
.props property sheets. From a Developer prompt:
msbuild tcpkv.sln -p:Configuration=Release -p:Platform=x64 -m -v:minimal
msbuild tcpkv.vcxproj -t:Rebuild -p:Configuration=Debug -p:Platform=x64
In Azure Pipelines, the VSBuild@1 or MSBuild@1 task runs the same command and VSTest@2 runs
tests (module 09).
CMake on Windows has two families of generators:
:: Visual Studio generator: multi-config. Produces a .sln you can open; config chosen at build time.
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release --parallel
ctest --test-dir build -C Release --output-on-failure
:: Ninja + MSVC: single-config, faster, the same commands as Linux. Run from a Developer prompt.
cmake -S . -B build/ninja -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build/ninja
Visual Studio's "Open Folder" reads CMakePresets.json directly, so the presets from module 04
work on Windows with a few extra entries:
{
"name": "windows-msvc",
"displayName": "MSVC x64 (Ninja)",
"inherits": "base",
"condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" },
"architecture": { "value": "x64", "strategy": "external" },
"cacheVariables": {
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl",
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
}
}
Portable CMake for both compilers keeps compiler-specific flags behind generator expressions, as
cmake/CompilerWarnings.cmake in tcpkv does:
target_compile_options(tcpkv_core PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX /permissive- /Zc:__cplusplus /utf-8>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic -Werror>)
set_property(TARGET tcpkv_core PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL") # /MD or /MDd
tcpkv itself uses POSIX sockets and would need a Winsock port to build on Windows: WSAStartup
once, SOCKET (an unsigned handle, compare with INVALID_SOCKET not < 0), closesocket instead
of close, WSAGetLastError() instead of errno, ioctlsocket(FIONBIO) instead of fcntl, no
SIGPIPE (send returns WSAECONNRESET), and ws2_32.lib in the link. That is a good
exercise (section 6).
2.5 vcpkg¶
vcpkg is Microsoft's source-based C and C++ package manager. It works on Windows, Linux and macOS, and integrates with CMake through a toolchain file.
Manifest mode (recommended): a vcpkg.json in the project root declares dependencies, and
CMake installs them during configure into build/vcpkg_installed.
{
"name": "tcpkv",
"version": "0.1.0",
"dependencies": [ "gtest", "fmt", "libpq", "hiredis" ],
"builtin-baseline": "<commit sha of the vcpkg registry, pins every port version>"
}
git clone https://github.com/microsoft/vcpkg.git ~/vcpkg && ~/vcpkg/bootstrap-vcpkg.sh # .bat on Windows
export VCPKG_ROOT=~/vcpkg
cmake -S . -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
Triplets name the target platform and linkage: x64-windows (DLLs, /MD),
x64-windows-static (static libs, /MT), x64-windows-static-md (static libs, /MD),
x64-linux (static by default). Choose with -DVCPKG_TARGET_TRIPLET=.... Pick the triplet that
matches your runtime library choice or you are back at LNK2038.
Binary caching stores built packages (locally under %LOCALAPPDATA%\vcpkg\archives or
~/.cache/vcpkg/archives, or in NuGet feeds / cloud storage) keyed by a hash of the port, its
dependencies, the triplet and the compiler. In CI, caching that directory turns a 20-minute
dependency build into seconds.
Classic mode (vcpkg install fmt:x64-windows into a global tree) is what older projects use;
it cannot pin versions per project. Conan is the other major choice: binary packages, a Python
recipe language, more flexible, more to learn. On Linux, apt install libfmt-dev is the third
choice, fine for tools, poor for reproducible builds (versions follow the distro).
2.6 Visual Studio debugger: the features worth knowing¶
- Conditional breakpoints: right-click a breakpoint, Conditions:
key == "acct42"or hit count>= 1000. GDB:break store.cpp:40 if ...,ignore. - Tracepoints (Actions: "Log a message", continue execution):
Store::set key={key} tid=$TID. printf debugging without rebuilding. GDB:dprintf. - Data breakpoints (Debug > New Breakpoint > Data Breakpoint): break when 1, 2, 4 or 8 bytes
at an address change. Uses CPU debug registers, so at most four (x86-64). The tool for "who is
corrupting this field". GDB:
watch -l. - Threads, Parallel Stacks, Parallel Watch windows: all threads' stacks merged into a graph;
freeze and thaw threads to force an interleaving. GDB:
info threads,thread apply all bt,set scheduler-locking on. - Exception Settings (Ctrl+Alt+E): break on first-chance C++ exceptions or Win32 exceptions
such as
0xC0000005access violation, before any handler runs. GDB:catch throw,handle SIGSEGV stop. - Watch format specifiers:
value,x(hex),ptr,10(show a pointer as a 10-element array),str,s8(UTF-8 string),var,!(raw, bypass visualizers). GDB:p/x,p *ptr@10. - Natvis files (
.natvis) define how your types display in the debugger; GDB's equivalent is Python pretty-printers. - Symbols: Tools > Options > Debugging > Symbols. Add the Microsoft symbol server and your own symbol store. "Just My Code" hides library frames; turn it off when a crash is inside the CRT.
- Diagnostic Tools: CPU usage, memory snapshots with diffing (heap growth between two points). Linux: perf, heaptrack, Massif (module 07).
- Open a dump file: File > Open > a
.dmp, then "Debug with Native Only". Set the symbol and source paths first. Linux:gdb ./tcpkv core.1234. - Attach to Process (Ctrl+Alt+P), including remote debugging with
msvsmon.exe. Linux:gdb -p PID,gdbserver. Visual Studio can also debug Linux processes over SSH and WSL with its CMake support, which is worth knowing if you keep the IDE.
2.7 WinDbg (WinDbg Preview, now just "WinDbg")¶
The modern WinDbg from the Microsoft Store or winget install Microsoft.WinDbg has the same
engine as classic WinDbg with a better UI, a data-model command dx, JavaScript extensions and
Time Travel Debugging. The deep material, including dump analysis walkthroughs, is in
05_Memory_Debugging_RCA/07_windbg_and_windows_crash_dumps.md. Command reference with GDB
equivalents:
| Task | WinDbg | GDB |
|---|---|---|
| Use Microsoft public symbols | .symfix then .reload |
debuginfod (set debuginfod enabled on) |
| Add own symbols | .sympath+ C:\build\pdb |
set debug-file-directory, symbol-file |
| Automatic triage | !analyze -v |
(none built in; bt + reading) |
| Switch to the exception context | .ecxr |
(core already stops at the signal) |
| Show exception record | .exr -1 |
p $_siginfo |
| Current stack | k, kb, kp (with params), kn (frame numbers) |
bt, bt full |
| All threads' stacks | ~*k |
thread apply all bt |
| Switch thread | ~3s |
thread 3 |
| Select frame, locals | .frame 2, dv |
frame 2, info locals |
| Display a type | dt tcpkv!Store 0x1234 |
ptype, p *(Store*)0x1234 |
| Loaded modules | lm, lmvm tcpkv |
info sharedlibrary |
| Critical sections / locks | !locks, !cs -l |
info threads + p mutex (__owner field) |
| Heap state | !heap -s, !heap -p -a ADDR (with page heap) |
ASan, Valgrind, malloc_info |
| Memory map | !address -summary |
info proc mappings, /proc/PID/maps |
| CPU time per thread | !runaway |
top -H -p PID |
| Write a dump | .dump /ma C:\dumps\x.dmp |
gcore PID / generate-core-file |
| Record and replay | Time Travel Debugging (TTD) | rr record / rr replay, GDB record |
Page heap (gflags /p /enable tcpkv.exe /full, or Application Verifier) puts every
allocation at the end of a page followed by a guard page, so a heap overrun faults at the
offending instruction instead of corrupting memory silently. It is the Windows ancestor of what
ASan does with redzones.
2.8 ProcDump and Windows Error Reporting¶
ProcDump (Sysinternals) writes dumps of a running process when a condition triggers:
procdump -accepteula -ma -e tcpkv.exe C:\dumps :: full dump on unhandled exception
procdump -ma -e 1 -f C0000005 tcpkv.exe C:\dumps :: on first-chance access violation
procdump -ma -h tcpkv.exe C:\dumps :: hung window (GUI apps)
procdump -ma -c 90 -s 10 -n 3 tcpkv.exe C:\dumps :: CPU >= 90 % for 10 s, 3 dumps
procdump -ma -m 2048 tcpkv.exe C:\dumps :: commit memory >= 2048 MB
procdump -ma -w tcpkv.exe C:\dumps :: wait for the process to start
procdump -ma -i C:\dumps :: install as the system post-mortem debugger
For a service on a customer machine, the zero-install alternative is WER LocalDumps:
registry key HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\tcpkv.exe
with DumpFolder, DumpCount and DumpType = 2 (full). Every crash then leaves a .dmp.
Linux equivalents: ulimit -c unlimited plus /proc/sys/kernel/core_pattern, or
systemd-coredump with coredumpctl list / coredumpctl gdb; gcore PID for a dump of a live
process. The "dump when CPU is high" trigger has no standard tool; a script with top -b and
gcore does it, and perf record is usually the better answer for CPU problems anyway.
2.9 dumpbin and the rest of the inspection kit¶
dumpbin /dependents tcpkv.exe :: DLLs it imports (ldd, readelf -d)
dumpbin /imports tcpkv.exe :: every imported function (nm -D --undefined-only)
dumpbin /exports tcpkv_core.dll :: exported functions (nm -D --defined-only)
dumpbin /headers tcpkv.exe :: PE headers, machine, subsystem, debug directory with PDB path (readelf -h -l)
dumpbin /symbols store.obj :: COFF symbol table of an object (nm store.o)
dumpbin /linkermember tcpkv_core.lib :: symbols indexed in a static library (nm -s libx.a)
dumpbin /disasm /section:.text x.obj :: disassembly (objdump -d)
dumpbin /pdbpath tcpkv.exe :: which PDB the binary expects (readelf -n: build-id)
undname ?add@@YAHHH@Z :: int __cdecl add(int,int) (c++filt _Z3addii)
Other tools and their Linux counterparts:
| Windows | Linux | Use |
|---|---|---|
| Dependencies (lucasg), Dependency Walker | ldd, lddtree |
DLL / .so resolution |
| Process Explorer | htop, /proc, lsof |
processes, handles, loaded modules |
handle.exe, Resource Monitor |
lsof -p PID, ls -l /proc/PID/fd |
open files and sockets |
| Process Monitor | strace, inotifywait, fatrace |
file, registry, process activity |
TCPView, netstat -ano |
ss -tanp |
sockets per process |
| ETW, WPR + WPA, PerfView | perf, bpftrace, flame graphs |
CPU and system tracing |
| Application Verifier, page heap | ASan, Valgrind | heap and handle misuse |
| Event Viewer | journalctl |
system and service logs |
Services (sc.exe, services.msc) |
systemctl |
service management |
| Task Manager | top, htop |
overview |
2.10 The interview table¶
The one-page comparison to know cold:
| Area | Windows | Linux |
|---|---|---|
| Compiler | cl.exe (MSVC), also clang-cl |
gcc/g++, clang/clang++ |
| Build system | MSBuild .sln/.vcxproj |
Make, Ninja; CMake generates for both |
| Warnings | /W4 /WX |
-Wall -Wextra -Werror |
| Optimised with symbols | /O2 /Zi + /DEBUG /OPT:REF /OPT:ICF |
-O2 -g (RelWithDebInfo) |
| Binary format | PE/COFF | ELF |
| Debug info | separate .pdb, matched by GUID + age |
DWARF in the binary or split, matched by build-id |
| Static library | .lib |
.a |
| Shared library | .dll + import .lib; nothing exported by default |
.so; everything exported by default |
| Runtime library choice | /MD vs /MT; one CRT heap per module with /MT |
one glibc per process |
| Library search | app dir, system dirs, PATH |
rpath, LD_LIBRARY_PATH, ld.so.cache |
| Data model | LLP64: long is 32-bit |
LP64: long is 64-bit |
wchar_t |
16-bit, UTF-16 APIs (CreateFileW) |
32-bit; APIs take UTF-8 char* |
| x64 calling convention | args in RCX, RDX, R8, R9; 32-byte shadow space; no red zone | args in RDI, RSI, RDX, RCX, R8, R9; 128-byte red zone |
| Name mangling | ?add@@YAHHH@Z (undname) |
_Z3addii (c++filt) |
| Crash mechanism | SEH exception (0xC0000005) |
signal (SIGSEGV) |
| Crash dump | minidump / full dump .dmp (WER, ProcDump) |
core file (core_pattern, systemd-coredump) |
| Debugger | Visual Studio, WinDbg | GDB, LLDB |
| Memory errors | ASan (/fsanitize=address), page heap, App Verifier |
ASan, UBSan, TSan, MSan, Valgrind |
| Profiler | VS Profiler, ETW/WPA, PerfView | perf, Valgrind callgrind |
| Syscall tracing | Process Monitor, ETW | strace, perf trace |
| Handles / fds | HANDLE, CloseHandle |
int fd, close |
| Last error | GetLastError(), WSAGetLastError() |
errno |
| Process creation | CreateProcessW |
fork + exec, posix_spawn |
| Threads | CreateThread, _beginthreadex, std::thread |
pthread_create, std::thread |
| Locks | CRITICAL_SECTION, SRWLOCK |
pthread_mutex_t (futex) |
| Scalable I/O | IOCP (completion-based) | epoll (readiness), io_uring (completion) |
| Services | Service Control Manager | systemd |
| Logs | Event Log, ETW | journald, syslog |
| Memory commit | committed at allocation, fails early | overcommit, OOM killer later |
| Package manager | vcpkg, NuGet, Conan | vcpkg, Conan, apt |
| Shell | PowerShell, cmd | bash |
| CI | Azure Pipelines (VSBuild@1, VSTest@2) |
GitHub Actions, Azure Pipelines on Linux agents |
3. Code¶
No new files in code/; the examples are inline because they need a Windows machine. To try the
Windows side of tcpkv's build:
- Install Visual Studio 2022 Community with the "Desktop development with C++" workload (it includes CMake, Ninja, vcpkg and the Windows SDK).
- Open a "Developer PowerShell for VS 2022" in a clone of your tcpkv repository on the Windows
filesystem (not
\\wsl$: builds over the 9P bridge are slow, the same reason module 01 gave for/mnt/cin the other direction). - Build the protocol library and tests only (the server needs the Winsock port):
cmake -S . -B build/vs -G "Visual Studio 17 2022" -A x64
cmake --build build/vs --config Debug --target tcpkv_tests
ctest --test-dir build/vs -C Debug --output-on-failure
dumpbin /dependents build\vs\tests\Debug\tcpkv_tests.exe # expect MSVCP140D.dll, VCRUNTIME140D.dll, ucrtbased.dll
The TCPKV_WERROR option in tcpkv's cmake/CompilerWarnings.cmake already maps to /WX for
MSVC, and the warning set to /W4 /permissive-. MSVC and GCC do not flag exactly the same
constructs, so expect some warnings GCC did not give (C4267 size_t to int, C4996 "unsafe"
CRT functions, for example). Fixing them is exercise 2.
A minimal Winsock initialisation for the port, showing the differences in one place:
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <winsock2.h>
# include <ws2tcpip.h>
# pragma comment(lib, "ws2_32.lib") // or target_link_libraries(... ws2_32)
using socket_t = SOCKET; // unsigned: never compare with < 0
constexpr socket_t kInvalidSocket = INVALID_SOCKET;
inline int last_socket_error() { return WSAGetLastError(); }
inline void close_socket(socket_t s) { closesocket(s); }
struct WinsockInit { // RAII: one per process, before any socket call
WinsockInit() { WSADATA d; if (WSAStartup(MAKEWORD(2, 2), &d) != 0) std::abort(); }
~WinsockInit() { WSACleanup(); }
};
#else
# include <sys/socket.h>
# include <unistd.h>
# include <cerrno>
using socket_t = int;
constexpr socket_t kInvalidSocket = -1;
inline int last_socket_error() { return errno; }
inline void close_socket(socket_t s) { ::close(s); }
struct WinsockInit {}; // nothing to do on POSIX
#endif
4. Under the hood (senior-level)¶
PE vs ELF loading. A PE image has an import directory: for each DLL, a list of
functions, and an Import Address Table (IAT) the loader fills with addresses at load time
(all at once, unless delay-loaded). Calls go through call [IAT slot]. ELF uses the GOT
(global offset table) for data and function addresses and the PLT (procedure linkage table)
stubs for calls, traditionally resolved lazily on first call; -Wl,-z,now (full RELRO) resolves
at load time like Windows. Both then make the table read-only. ELF symbol lookup is by name across
all loaded objects in order (symbol interposition: LD_PRELOAD can replace malloc for the
whole process). Windows lookup is two-level: each import names the DLL it comes from, so two DLLs
can export the same name without conflict and there is no global interposition.
Default visibility and its consequences. Because ELF exports everything by default and
resolves by global name, an .so built without -fvisibility=hidden has a huge dynamic symbol
table, slower loading, and calls to its own exported functions go through the PLT (they could be
interposed). Windows DLLs export only what is marked, so this problem does not arise; the cost is
the dllexport/dllimport macro dance.
One heap or many. With /MT, each DLL and the EXE carry their own copy of the CRT, each with
its own heap. Memory allocated by malloc in one module and freed by free in another goes to the
wrong heap: corruption or a debug assertion. The same applies to FILE* and to std::string
crossing DLL boundaries. The rules on Windows: free memory in the module that allocated it, or use
/MD everywhere, or pass only C types across DLL boundaries. On Linux one libc.so.6 serves the
whole process and the problem does not exist (unless you statically link libstdc++ into several
.so files, which recreates a milder version of it).
PDB vs DWARF matching. The linker writes a CodeView debug directory entry (RSDS) into the PE
containing the PDB's GUID, an age, and the PDB path. The debugger only loads a PDB whose GUID and
age match exactly; that is why you must archive the PDB of every shipped build (a symbol store,
symstore.exe add, served over HTTP). Linux's equivalent is the build-id note
(readelf -n), a hash of the binary's contents; GDB finds separate debug files by build-id under
/usr/lib/debug/.build-id/ or through a debuginfod server. CI archiving the unstripped
binary per commit (module 09) is the low-tech version.
SEH vs signals. A Windows access violation is a structured exception: the kernel builds an
exception record and context, and user-mode dispatch walks registered handlers (table-based on
x64, from the .pdata section), which __try/__except and C++ catch(...) with /EHa can catch,
first chance then second chance. A Linux SIGSEGV is a signal delivered to a handler with a
ucontext_t; C++ exceptions are a separate mechanism (Itanium ABI unwinding with .eh_frame)
and cannot catch it. Both end with a dump/core if unhandled. "Catching" access violations to keep
a server running is a bad idea on both: the process state is unknown.
LLP64 vs LP64. Windows kept long at 32 bits on 64-bit platforms for source compatibility
with Win32 structs; Linux made long 64-bit. Code that stores a pointer or a size_t in a
long works on Linux and truncates on Windows; code that assumes long is 32-bit for a file
format works on Windows and breaks on Linux. Use int64_t, uint32_t, size_t, intptr_t.
Calling conventions matter when reading crash dumps without full symbols: on Windows x64
the first four integer arguments are in RCX, RDX, R8, R9, and the caller reserves 32 bytes of
shadow space; on Linux x86-64 (System V) the first six are in RDI, RSI, RDX, RCX, R8, R9, and
leaf functions may use a 128-byte red zone below RSP. Knowing this lets you recover a this
pointer from a register in a release-build dump on either platform.
5. In production¶
- Your experience, framed. "Our core-banking application server is built with MSBuild in Azure Pipelines, crashes at customer sites arrive as WER or ProcDump dumps, and I analyse them in WinDbg with our symbol store." Then: "On Linux the same pipeline is CMake and Ninja in GitHub Actions, crashes are cores collected by systemd-coredump, and I analyse them in GDB with the archived unstripped binary or debuginfod." That pair of sentences answers half the Linux questions an interviewer will ask a Windows developer.
- Missing redistributable. A build that runs on every developer machine fails on a clean server
with "VCRUNTIME140.dll was not found". Developer machines have Visual Studio. Ship the VC++
redistributable with the installer, or use
/MTfor a self-contained exe. The Linux version is the "cannot open shared object file" from module 09's 3 AM story: same root cause, different runtime. - Symbols you did not keep. A customer sends a dump from build 4.2.17; nobody archived its PDB.
Rebuilding the same source does not help: the new PDB has a different GUID. You are reduced to
reading raw assembly and register values. Every release pipeline must publish PDBs (or
unstripped ELF /
.debugfiles) to a symbol store keyed by build, automatically. - Porting pitfalls in mixed teams.
longsize,wchar_tsize, path separators and drive letters, case-insensitive file names (an#include "Store.hpp"that works on Windows fails on Linux), CRLF in shell scripts,SOCKETvsint,closesocket, missingSIGPIPEhandling when code moves from Windows to Linux,localtime_svslocaltime_r,_stricmpvsstrcasecmp. CI on both platforms catches them on the commit that introduces them. - ASan on MSVC is real and useful but narrower: no UBSan, TSan or LeakSanitizer in MSVC, and
it requires matching runtime libraries. Teams with Windows-only code often keep a Linux or
clang-clCI job just to get the other sanitizers. - 3 AM: the Windows service hangs at a customer. Before restarting it, take
procdump -ma tcpkv.exe(it does not kill the process), then restart. In WinDbg,~*kshows 40 threads blocked inRtlEnterCriticalSection;!locksnames the owning thread, whose stack is inside a logging call waiting on another lock held by thread 12: a lock-order inversion. On Linux the identical procedure isgcore PID(orgdb -batch -x scripts/dump_stacks.gdb -p PID),systemctl restart, thenthread apply all btand the mutex__ownerfield.
6. Try it¶
- (easy) On Windows, build any small C++ program in a Developer prompt with
cl /std:c++17 /W4 /EHsc /Zi /O2 hello.cpp, then rundumpbin /headers,/dependentsand/pdbpathon it. On WSL, do the same withg++ -O2 -gandreadelf -h,ldd,readelf -n. Done when you have a two-column table of what each command told you. - (medium) Build tcpkv's
tcpkv_coreandtcpkv_testswith the Visual Studio generator as in section 3, at/W4 /WX. Fix every warning in a way that also compiles cleanly with GCC-Werror. Done when both platforms build the tests with warnings as errors and all tests pass. - (medium) Reproduce LNK2038: build a static library with
/MTand an executable with/MDthat links it. Then fix it by aligningCMAKE_MSVC_RUNTIME_LIBRARY. Done when you can explain why Linux has no equivalent error. - (medium) Add a
vcpkg.jsonwithfmtto a copy of tcpkv, configure with the vcpkg toolchain file on both Windows and WSL, and usefmt::formatinlog_line. Done when both builds succeed from a clean checkout and you can name the triplet used on each. - (hard) Port
src/main.cppto Windows using thesocket_tshim from section 3: Winsock init,closesocket,WSAGetLastError,SetConsoleCtrlHandlerfor Ctrl-C instead ofsigaction. Done when the samescripts/tcpkv_client.py --selftest(run from Windows Python) passes against the Windows build and the Linux build. - (hard) Crash the Windows build deliberately (null dereference on a
CRASHcommand), capture a dump withprocdump -ma -e, open it in WinDbg, and run.symfix; .reload; !analyze -v; .ecxr; kp. Then do the equivalent on WSL withcoredumpctl gdb. Done when you have both stack traces pointing at the same source line and can list each command's counterpart.
7. Interview questions¶
Q: How does linking against a DLL differ from linking against a shared object?
On Windows you link against an import library (.lib) that tells the linker which DLL exports
each symbol; at run time the loader fills the Import Address Table. Only explicitly exported
symbols (__declspec(dllexport) or a .def file) are visible. On Linux you link against the
.so itself, everything with default visibility is exported, calls go through PLT/GOT, and the
loader searches rpath, LD_LIBRARY_PATH and the cache instead of the application directory.
Q: What does LNK2038 "mismatch detected for RuntimeLibrary" mean?
Objects compiled with different C runtime options (/MT vs /MD, or Debug vs Release) are being
linked into one module. Each choice selects a different CRT and STL build with different object
layouts and heaps. Fix by using one runtime library for everything, in CMake with
CMAKE_MSVC_RUNTIME_LIBRARY, and choosing the matching vcpkg triplet.
Q: What are /MD and /MT and which do you ship?
/MD links the CRT and STL as DLLs shared by all modules (one heap, requires the VC++
redistributable on the target); /MT links them statically into each module (self-contained, but
each DLL gets its own heap, so memory and CRT objects must not cross DLL boundaries). For a product
with several DLLs, /MD plus a redistributable is the usual choice; for a single tool exe, /MT.
Q: How do you get a crash dump from a production Windows service, and the Linux equivalent?
Configure WER LocalDumps in the registry for the exe, or run ProcDump with -ma -e; for a hang,
take procdump -ma on demand. Analyse in WinDbg with the archived PDB: !analyze -v, .ecxr,
kp, ~*k. On Linux: ulimit -c/LimitCORE plus systemd-coredump or core_pattern, gcore
for live processes, and GDB with the unstripped binary or debuginfod.
Q: Why might code that works on Windows break on Linux (or the reverse) even with the same compiler family?
Data model (long is 32-bit on Windows, 64-bit on Linux), wchar_t size, file-name case
sensitivity, path separators, line endings, default symbol visibility, SIGPIPE on socket writes,
different socket handle types and error retrieval, and APIs with _s/_r variants. Fixed-width
integer types, CI on both platforms and portable wrappers catch these.
Q: What is vcpkg manifest mode and what is a triplet?
Manifest mode declares a project's dependencies in vcpkg.json and pins versions with a
builtin-baseline; CMake's vcpkg toolchain file installs them per project at configure time.
A triplet names the target architecture, OS and linkage (x64-windows, x64-windows-static,
x64-linux), and must match the runtime-library choice of the project.
Q: Compare WinDbg and GDB for a hang.
Both attach to a live process or open a dump. WinDbg: ~*k for all stacks, !locks to find
critical-section owners, !runaway for CPU per thread. GDB: thread apply all bt, the mutex's
__owner field for the owning thread's LWP, top -H for CPU per thread. The diagnosis is
identical: find who holds the lock everyone waits for and what it is waiting for.
8. Recap¶
- MSVC =
cl+link+lib+ MSBuild; Linux =g+++ld+ar+ Make/Ninja; CMake drives both. /W4 /WX /permissive- /EHsc /std:c++17is the MSVC mirror of-Wall -Wextra -Werror -std=c++17./MDvs/MTmust match across a module; LNK2038 is the symptom; Linux has one runtime per process.- DLLs export nothing by default and link through import libraries;
.soexports everything and links directly. - LNK2019/LNK2001 = undefined reference; LNK2005 = multiple definition;
dumpbin/undname=nm/c++filt. - vcpkg manifest mode + toolchain file + matching triplet; cache binaries in CI.
- VS debugger: conditional breakpoints, tracepoints, data breakpoints, Parallel Stacks, exception settings.
- WinDbg:
.symfix; .reload; !analyze -v; .ecxr; kp; ~*k; !locks; ProcDump-ma -e; WER LocalDumps. - PDB matched by GUID + age, ELF debug info by build-id: archive symbols for every shipped build.
- LLP64 vs LP64,
wchar_t2 vs 4 bytes, SEH vs signals, IOCP vs epoll: the differences interviewers probe.