4

It was brought to my attention that my binary will crash on the CPU without SSE support, with exception code 0xC000001D (STATUS_ILLEGAL_INSTRUCTION), despite I'm compiling with option /arch:IA32. And I have been able to track down the exact place where it crashes: Wherever _snprintf_s() is called for the first time, it will crash. The crash is inside of ucrtbase.dll, not my own code.

Now, the interesting part is that, when I make a "fully static" build with compiler option /MT, so to avoid explicity dependency on ucrtbase.dll, the resulting binary works just fine! But, as soon as I compile the exactly some code as "shared" build, with option /MD, it will crash again in ucrtbase.dll.

So it would appear that "static" version of the UCRT still can work on the CPU without SSE support, but the "shared" (DLL) version can not. This inconsistency would clearly seem like a bug to me!

Any thoughts?


Build environment:

  • Windows 10 v1803
  • Visual Studio 2017.8 (v15.8.1)
  • Windows SDK v10.0.17134.12
  • Toolset: v141_xp
  • Compiler option: /arch:IA32

Test machine (used for compat-testing only):

  • CPU: Pentium II
  • OS: Windows XP with Service-Pack 3

Note: Redist DLLs (ucrtbase.dll +api-ms-win-*.dll) for the setup of the "shared" build have been copied straight from C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x86 directory! To the best of my knowledge, this is the latest available version of these DLLs (v10.0.17134.12).


Even this minimal test program will reproduce the crash:

#include <stdio.h>

int main()
{
    char buffer[128];
    _snprintf_s(buffer, 128, _TRUNCATE, "Hello %s!\n", "World!");
    fputs(buffer, stdout);
    getc(stdin);
    return 0;
}
MuldeR
  • 577
  • 1
  • 4
  • 17
  • I have tracked down with debugger. It crashes inside of the implementation of `_snprintf_s()`, inside of system DLL `ucrtbase.dll`. The code is STATUS_ILLEGAL_INSTRUCTION, so almost certainly **not** a problem in my code, but use of SSE instruction in pre-existing code inside system DLL `ucrtbase.dll`. Even minimal test application with *nothing* but `main()` calling into `_snprintf_s()` will reproduce the crash! And only on CPU *without* SSE; otherwise it works fine! – MuldeR Aug 26 '18 at 11:41
  • 1
    Did you try with [standard `snprintf()`](https://en.cppreference.com/w/cpp/io/c/fprintf)? It is available if you can use C++11. – Geezer Aug 26 '18 at 12:06
  • 1
    You should report this to Microsoft as well. – Khouri Giordano Aug 26 '18 at 12:21
  • 1
    @KhouriGiordano They’ll probably tell OP that Windows XP support has ended in 2014 and will do nothing. As of today, all supported versions of Windows (i.e. 8.1 and 10) require SSE. – Soonts Aug 26 '18 at 12:30
  • 2
    @SkepticalEmpiricist: Call to `snprintf()` results in the same crash as the call to `_snprintf_s()`. Looking at the callstack, it seems that they both end up in same internal routine named `_stdio_common_vsprintf` inside of **`ucrtbase.dll`** – where it crashes. – MuldeR Aug 26 '18 at 12:37
  • @Soonts: Nonetheless, Windows XP is still an officially supported "target" system for MSVC in VS2017. They even have a separate **`v141_xp`** toolset for exactly the purpose - which I am using. Also, the "Redist" UCRT DLL's (in Windows 10 SDK) are provided exactly for the purpose of supporting these "legacy" systems. Newer Windows versions come with the UCRT integrated. Also: If latest UCRT version stopped supporting *non*-SSE CPUs, then why only the DLL version is effected and **not** the "static" lib? This doesn't look intentionally to me. It looks like a bug to me... – MuldeR Aug 26 '18 at 12:40
  • @MuldeR I think they have `v141_xp` toolset because there’re couple of XP-based OSes that are still supported, Windows Embedded POSReady 2009 and Windows Embedded Standard 2009. You’re using neither of them. – Soonts Aug 26 '18 at 12:45
  • @Soonts: I don't agree. I think toolset **`v141_xp`** exists to target Windows XP *in general*. The separate **`_xp`** toolsets where introduced in VS2012, long before XP became "end of life". But anyway: That's **not** the point, I think! If UCRT "officially" stopped supporting *non*-SSE CPUs, then (1) this needs to be made clear by MS somewhere and (2) the DLL version and the "static" lib should be effected equally. The way it is now, it seems like an oversight, effecting only in the DLL version... – MuldeR Aug 26 '18 at 12:51
  • @MuldeR The way it’s now, it seems MS no longer cares what happens when your CPU doesn’t support SSE. “the DLL version and the static lib should be effected in the same way” read the source code in `ucrt\stdio\output.cpp` under Windows SDK and you’ll see template C++ implementation of that thing, in common_vsprintf / common_vsnprintf_s / common_vsprintf_s template functions. Probably because of these templates, the static library is affected by `/arch:IA32` in your project. – Soonts Aug 26 '18 at 13:23
  • @Soonts: Indeed, in MSVC's `stdio.h` header file, **`_snprintf_s()`** is defined as "inline" code. But that's really just a minimal "wrapper" function, that will call the actual function, which is `__stdio_common_vsnprintf_s()`, from the UCRT. And *that* function is **not** "inlined" but rather located inside **`ucrtbase.dll`** or **`libucrt.lib`**, depending on which variant ("static" or "DLL") of the UCRT is linked in. It is also `__stdio_common_vsnprintf_s()` where the crash occurs, in the "DLL" version. – MuldeR Aug 26 '18 at 14:38

1 Answers1

3

Update:

After some more debugging and fiddling around, I made a very interesting observation: The UCRT "Redist" DLLs contained within the latest vcredist_x86.exe (Microsoft Visual C++ 2017 Redistributable installer), i.e. the version that ships with VS2017.8 (v14.15.26706), are actually quite different from those UCRT "Redist" DLLs that can be found in the Redist\ucrt\DLLs\x86 directory of the latest Windows SDK (v10.0.17134.12):

  • ucrtbase.dll from latest Windows SDK
    v10.0.17134.12

  • ucrtbase.dll from latest Visual C++ 2017 Redistributable installer:
    v10.0.10586.15

Indeed, I can confirm now that the application compiled with latest Visual C++ 2017 (v14.15.26706) and using the /MD option works correctly on the non-SSE CPU, as long as we use the "old" ucrtbase.dll version, as extracted from the latest vcredist_x86.exe installer.

I'm a bit concerned using such an old version of the UCRT, if the Windows SDK already provides a much newer version. But, apparently, that is what Microsoft is doing themselves with the Visual C++ 2017 Redistributable installer. So, I guess, it is what we are supposed to use...


Just in case anybody at Microsoft is reading this:

Things could have been way less confusing and error-prone for software developers, if, in the Redist\ucrt directory of the Windows SDK, there were separate sub-folders for each "incarnation" of the UCRT – the latest "cutting edge" version of the UCRT and the "compatible" version of the UCRT that we are actually supposed to redistribute. If then somebody at Microsoft spent three minutes for writing a little README file that tells us which "incarnation" of the CRT we should pick and put it at Redist\ucrt\README.txt, it would be almost impossible to do wrong...

MuldeR
  • 577
  • 1
  • 4
  • 17
  • Extracting files from the VS redistributable is not quite straightforward. Refer to this [answer](https://stackoverflow.com/a/55490616/731081) for procedure utilizing WiX toolset – sonyisda1 Aug 02 '19 at 15:22