MSVC Toolchain and Windows Native Build

The Windows native build stack end to end: cl.exe and link.exe, the developer environment, MSBuild versus CMake, the CRT variants and the mismatched-runtime crash, DLL exports, PDBs, and pinning a toolset in CI.

Overview

A full second toolchain

Windows native C++ is a full second toolchain, not a porting detail. "Strong knowledge of MSVC libraries" means debugging a build with no Unix tooling: cl.exe compiles, link.exe links, the standard library is Microsoft's, symbols live in a separate file, and nothing is exported from a DLL unless you say so.

  • The toolchain does not know where it is

MSVC depends on PATH, INCLUDE, LIB, LIBPATH and many more variables, set by a batch file generated for your specific installation.

  • VsDevCmd.bat — The general one
  • vcvars32, vcvars64, cross-tools variants — Thin wrappers
  • vcvarsall.bat — Parameterized: host and target architecture, optional Windows SDK version, optional toolset via -vcvars_ver, optional Spectre-mitigated libraries

> [!WARNING] Microsoft warns against mixing vcvars files from different Visual Studio versions in one shell, and against copying a vcvarsall.bat between machines.

  • The C runtime

Most Windows-specific production incidents come from here. Since Visual Studio 2015 the CRT has three pieces.

  • Universal CRT — A Windows component, shipped through the Windows SDK
  • vcruntime — Compiler-version-specific: exception handling, RTTI, runtime checks
  • Startup code — Always statically linked

/MT, /MTd, /MD and /MDd select the import libraries. Every EXE and DLL chooses independently, so one process can hold several CRTs, each with its own heap and static state.

  • Export control is inverted
  • Linux — Every non-static symbol exported — Visibility attributes or a version script to hide
  • Windows — Nothing exported — __declspec(dllexport) or a .def file to export

Linking a DLL produces the DLL plus an import library (.lib) that contains no code. Confusing the two meanings of .lib is a reliable source of confusion in mixed teams.

  • Debug information lives in PDBs
  • /Z7 — Full debug info embedded in the object files
  • /Zi — A separate PDB
  • /ZI — Edit-and-Continue PDB; forces /Gy and /FC, disables most optimization pragmas

> [!IMPORTANT] A release build with no archived PDB produces minidumps nobody can symbolize. That is a process failure, not a technical one, and it is very common.

  • Versioning changed recently

Through VS 2022 the compiler was versioned 14.xx, with binary compatibility across v140 to v143. From VS 2026, the MSVC package is identified by its own toolset version.

> [!TIP] In CI, locate toolchains with vswhere and pin explicitly, rather than trusting whatever the hosted agent image installed last week.

What interviewers probe

  • Crash on free only in release — CRT mismatch
  • DLL will not load — Dependencies, exports
  • Stack trace with no function names — Missing or mismatched PDB
  • Works in Visual Studio, fails in the pipeline — Environment and toolset pinning

Naming the mechanism separates someone who has shipped on Windows from someone who has only cross-compiled for it.

Key concepts

Developer command prompt and vcvarsall
MSVC requires PATH, INCLUDE, LIB, LIBPATH and around twenty other variables that the installer-generated batch files set. VsDevCmd.bat is the general entry point; vcvarsall.bat takes an architecture argument (x64, x86_amd64, amd64_arm64 and so on), an optional Windows SDK version, an optional -vcvars_ver toolset selector and an optional spectre flag. With no arguments it configures x86 native, not x64.
The three-part CRT
Universal CRT (ucrtbase.dll, a Windows component delivered through the Windows SDK), vcruntime (compiler-version-specific: exception handling, RTTI, runtime checks), and the startup code, which is always statically linked. /MD selects the import libraries msvcrt.lib, ucrt.lib and vcruntime.lib; /MT selects libcmt.lib, libucrt.lib and libvcruntime.lib. With no option at all, the linker uses the static set.
Mismatched runtime
Two images in one process using different CRTs, or the same CRT in debug and release form. Each has its own allocator, its own internal layouts and its own static state, so memory, FILE handles, locales and C++ objects cannot safely cross between them. MSVC emits detect-mismatch records so many cases fail at link with LNK2038 naming RuntimeLibrary or _ITERATOR_DEBUG_LEVEL.
dllexport, dllimport and .def
Nothing leaves a Windows DLL unless exported. declspec(dllexport) adds the export directive at compile time and handles C++ decorated names, which is why it is preferred; declspec(dllimport) on the consumer side lets the compiler generate a direct indirect call. A .def file is the alternative and the only route to ordinals, NONAME and PRIVATE. Using both together is allowed.
Import library versus static library
Linking a DLL emits a .lib containing stubs and a name table that bind to the DLL at load time. A static library is a .lib containing real object code copied into the consumer. Identical extension, entirely different artifact, and a routine source of confusion.
PDB and debug information format
/Z7 keeps full symbolic debug info inside each .obj and produces no compiler PDB. /Zi produces a separate PDB and implies /DEBUG. /ZI is /Zi plus Edit and Continue, forces /Gy and /FC, and disables optimize pragmas. The linker produces the final PDB. Binary and PDB are matched by name, GUID and age, so a rebuild invalidates old PDBs.
/permissive- and conformance
Turns off Microsoft language extensions and sets /Zc:referenceBinding, /Zc:strictStrings, /Zc:rvalueCast and /Zc:ternary to conforming behaviour; from VS 2022 17.6 it also sets /Zc:lambda and /Zc:externConstexpr. It is implied by /std:c++20 and /std:c++latest, and is required for C++20 modules. It enables two-phase name lookup, which is what breaks most legacy template code on first contact.
Toolset version, Visual Studio version, Windows SDK
Three independent axes. Through VS 2022 the platform toolset carried a v-number tied to the IDE (v140 for 2015 through v143 for 2022) with binary compatibility maintained across that range; from VS 2026 the MSVC version is decoupled and identified by the toolset version itself. The Windows SDK versions separately and supplies the UCRT headers and libraries. Pin all three in CI.

When to use it

  • Adding a Windows target to a codebase that has only ever been built with GCC or Clang on Linux
  • Debugging a crash that only reproduces in a release build on Windows and points at a free or a destructor
  • Setting up a Windows build agent and needing the toolset and SDK to be pinned rather than whatever the image happens to ship
  • Shipping a native DLL that third parties link against, where the export surface and the CRT choice become a public contract
  • Symbolising production minidumps and discovering the PDBs were never archived
  • Evaluating clang-cl to get Clang diagnostics and sanitizers while staying on the MSVC ABI and standard library

Interview questions

  • A DLL crashes inside free() when the host EXE passes it an object to delete. Walk through the cause and the fixes.
  • How do you produce, ship and use PDBs so that a production crash is actually debuggable?
  • You are setting up Windows CI for a C++ project. How do you pin and locate the toolchain, and what are the failure modes?
  • Explain __declspec(dllexport)/dllimport versus a .def file, and how you would structure exports for a library with external consumers.
  • What does /permissive- change, why would you adopt it, and what breaks first?
  • An application fails to start on a customer machine with a missing-DLL or 0xc000007b error. How do you diagnose it?

Sign up free to read the full topic

More DevOps topics

Start free on Cariara · Coding problems · System design studio