During research for my AMD Geode projects, I found an amazing saga based around a CPU instruction. Nobody else has written this up from what I can see, so here's my take.
Background[edit | edit source]
The AMD Geode LX800 CPU is an incredibly strange CPU. Despite being branded AMD, it's a descendant of the Cyrix MediaGX with a ton of features stapled on.
It implements the following instruction sets:
- Pentium (i586)
- Pentium Pro (i686)
- Extended MMX
- Enhanced 3DNow!
- Extra AMD Geode instructions
Notably the machine does not support PAE (Physical Address Extension). This may be why the CPUID identifies as family 5 (Pentium), not family 6 (Pentium Pro). This is all according to my reading of the AMD Geode LX Processors Data Book.
In contrast, some CPUs like the VIA C3 marked their CPU as family 6 without implementing the instruction set specified in the Pentium Pro Family Developer's Manual Volume 2. Most notably the 'CMOV' instruction.
But in theory, you could run an i686 system on the Geode without PAE. In practice it's a bit more complicated.
Undocumented i686 instructions[edit | edit source]
In 1995 Intel released the Pentium Pro and its developer manual documenting the entire instruction set.
By 1997 Christian Ludloff had created a map of 2 byte x86 opcodes. This confirmed Intel's documentation, but included a few unknown opcodes: 0F 34, 0F 35, as well as 0F 18 through 0F 1F.
In 1997 those first two opcodes 0F 34 and 0F 35 were determined to be Pentium II SYSENTER and SYSEXIT instructions despite Intel only documenting this later in 1999. It turns out the instructions were available on the Pentium Pro but broken. See SYSENTER, Where Are you? for a good summary of this situation.
Hilariously enough, the Geode LX800 identifies as a Pentium but supports the SYSENTER and SYSEXIT instructions introduced in the Pentium Pro and finally made useful in the Pentium II. Weird.
In 1998 Christian Ludloff documented in his updated map of 2 byte x86 opcodes that the 0F 18 through 0F 1F range of opcodes were hinting NOPs. The first being the 0F 18 opcode which maps to PREFETCHh instructions. I believe this information was documented first in the Intel Architecture Optimization Reference Manual.
Later in 2003 Christian Ludloff clarified in an email thread Undocumented opcodes (HINT_NOP) that these hinting NOPs were declared by Intel in their 1995 patent US5701442. The idea behind this patent from my reading is that you can encode a program written in another ISA as a series of opcodes that are run as NOPs on older machines and the new ISA on a newer machine.
I'm not sure why, but third party x86 CPUs aside from AMD didn't implement these NOPs. Perhaps Intel kept this patent close to their heart? Or maybe it's just not worth spending silicon and research on NOPs that nobody used?
The birth of multi-byte NOP[edit | edit source]
In 2006 Intel released an updated IA-32 Intel Architecture Software Developer's Manual. In addition to the standard one byte NOP op code, the multi-byte opcode was documented. This NOP could be from 2 to 9 bytes long, much longer than a single byte NOP. This is a pretty useful instruction for aligning code and data in memory. The only thing weird about this instruction is that it was marked as available on Pentium Pro and newer machines despite being documented in 2006. It turned out that Intel had recycled one of their hinting NOPs (0F 1F) as a new instruction.
Someone pointed this out on the Intel forum in the thread Multi-byte NOP opcode made official. They had tested and verified the feature and even pointed out that it works on AMD processors despite this instruction not being documented anywhere. They asked a few hard hitting questions to Intel:
- Why was this opcode secret?
- Why does it work on AMD CPUs?
- Why does AMD recommend an opcode of 66 66 66 90 for multi-byte NOPs?
Intel got back to them with a 'this information is Intel Confidential and would require an NDA to discuss' reply. NOPs are definitely serious business.
I'm going to just go out and guess that AMD recommended the 66 90 opcode series because their CPUs optimized it and it worked on older machines. While with Intel their solution seems to be to recycle their trash.
In 2007 Symantec wrote a blog post x86 Fetch-Decode Anomalies showing that Intel's hinting NOP opcode they assigned to the multi-byte NOP actually will attempt to fetch memory (and even page fault) if you instruct it to. This isn't a problem in practice, but it gives more evidence this opcode isn't strictly a NOP.
Linux fallout[edit | edit source]
In 2006 PATCH: Add "nop memory" for i386/x86-64 was committed to the GNU Assembler. It added support for the 'nopl' and 'nopw' assembly instructions that map to multi-byte NOP code.
In 2007 x86: multi-byte single instruction NOPs was committed to Linux. This added a set of 'P6 NOPs' that used the multi-byte NOP opcodes and used them for i686 or newer x86 CPUs. Which type of NOPs to use were decided at runtime, so running an i686 kernel on an i586 machine would not cause any issues with this. Strangely on 64-bit systems the NOPs were only used if your CPU vendor was Intel.
In 2008 the Debian bugs Linux 2.6.24 fails to boot on MS Virtual PC 2007 and -686 build uses long noops, that are unsupported by Transmeta Crusoe, immediate crash on boot were reported, as well as the Linux. After a lot of discussion, the following CPUs were reported to not support multi-byte NOPs:
- VIA C3 Nehemiah
- Transmeta Crusoe TM5800
- Microsoft Virtual PC 2007
- QEMU 0.9.1
- AMD Geode LX800
Interestingly enough the TM5800 reports its CPUID as family 5 like the LX800 does. But for the TM5800 the kernel promoted its status to i686 by changing the reported family at runtime.
The LX800 reports its family as 5, so shouldn't have failed due to the patch. I looked up other reports from the time and found the bug report 2.6.24-rc8 hangs at mfgpt-timer which seems to fit better, especially since the reporter didn't post any logs.
In parallel [BUG] x86 kenel won't boot under Virtual PC was reported to the Linux mailing list. This has a bit of a more focused discussion about how to address this problem. To summarize the discussion:
- GNU Assembler shouldn't generate multi-byte NOPs if not all i686 CPUs support it
- This is all too much effort for NOPs
A flurry of fixes appeared in Linux 2.6.25 to stop the bleeding:
- x86: do not promote TM3x00/TM5x00 to i686-class
- x86: require family >= 6 if we are using P6 NOPs
- x86: don't use P6_NOPs if compiling with CONFIG_X86_GENERIC
This decided Transmeta Crusoe CPUs weren't i686, and limited use of multi-byte NOPs to i686 non-generic kernels.
A little while later in Linux 2.6.27 this amazing chain of patches happened:
- x86: add NOPL as a synthetic CPU feature bit
- x86: disable static NOPLs on 32 bits
- x86: prevent binutils from being "smart" and generating NOPLs for us
- x86: completely disable NOPL on 32 bits
Instead of only adding multi-byte NOPs to i686 and better machines during the kernel build, the plan was to only use multi-byte NOPs at runtime based on whether the CPU supported running multi-byte NOPs.
Unfortunately the developers found the GNU Assembler would add multi-byte NOPs all the time for i686 and Virtual PC would freeze during detection of multi-byte NOP support. In the end they just threw their arms up and said 'no multi-byte NOPs at all on 32-bit x86.
As a result, the 'nopl' flag found in /proc/cpuinfo which would have shown if the CPU supported multi-byte NOPs is always hidden on 32-bit x86 and always shown on 64-bit x86, making it effectively useless.
GNU Assembler confusion[edit | edit source]
In 2008 in the midst of the kernel fallout the GNU Assembler bug i386 NOPs must be derived from march not mtune.
When compiling code you can specify which CPU family to support and which specific CPU to optimize for. The kernel developers found that the GNU Assembler wouldn't add multi-byte NOPs if you targeted the i686 family, instead it only added them if you targeted the i686 CPU (the Pentium Pro). This was confusing for a few reasons:
- Multi-byte NOPs weren't used on all CPUs that supported them by default
- Optimizing for a specific CPU could break compatibility with the family
Reading the bug report, you can see two schools of thought on what the problem is here and how to fix it.
The bug reporters believed:
- Multi-byte NOPs are not part of the i686 family
- Optimizing for the i686 CPU is adding Pentium Pro-only instructions
- The i686 architecture should not emit multi-byte NOPs
The GNU Assembler developers believed:
- Multi-byte NOPs are indeed part of the i686 family
- Optimizing for the i686 CPU is using i686 instructions
- Developers should build against the i586 architecture if they need wider compatibility
Something important to note here is that most software projects didn't ask GNU Assembler to target the i686 CPU, so this bug didn't really affect many projects in practice. A workaround was to optimize for the 'generic32' CPU which didn't use the nopl instructions.
glibc fallout[edit | edit source]
In 2010 Pass -mtune=i686 to assembler when compiling for i686 was committed to glibc. This told GNU Assembler to optimize for i686 CPUs (Pentium Pro), and as I mentioned in the previous section, this used multi-byte NOPs.
A month later the Arch Linux bug Update to glibc 2.12-2 on VIA C3 Nehemia makes system unusable and Fedora bug glibc not compatible with AMD Geode LX were reported. glibc being a core component of most GNU systems meant updating completely crashed people's machines. Oops.
Unlike the Linux and GNU Assembler discussions, the Arch Linux and Fedora discussions were from the perspective of people building and packaging software. Finding out what was broken was a little tricky.
- Was it GNU Assembler for adding nopls to code?
- Was it glibc for tuning for i686 CPUs?
- Was it the Linux distros for running i686 binaries on non-i686 CPUs?
Things were a little tricky for Fedora here as they explicitly supported the AMD Geode LX800 as it was used in millions of laptops for the One Laptop per Child project. While the LX800 isn't i686, it ran i686 binaries fine. They would have to support not just i686 but i586 too for their entire distribution just to support this laptop.
Around this time the GNU Assembler committed Don't generate multi-byte NOPs for i686. This patch restricted generating multi-byte NOPs to Intel and AMD CPUs. Strangely enough the i586 AMD K6-2 CPU was marked as supporting multi-byte NOPs, which was fixed in the 2013 commit Remove CpuNop from CPU_K6_2_FLAGS.
After a few months of discussion and without a new GNU Assembler release, Arch and Fedora decided to just revert glibc's change. This at least fixed things and made i686 builds of their distributions run on CPUs they supported.
Kernel emulation[edit | edit source]
In 2010 a kernel developer proposed the patch AMD Geode NOPL emulation for kernel 2.6.36-rc2. This patch would trap the unknown instruction exception non-i686 CPUs would generate, emulate it, then return back to the program. This was a bit controversial.
Arguments for the patch:
- Distributions aren't going to care long term
- Proprietary software isn't easily fixable
- A similar patch was used to emulate CMOV instructions on i586 CPUs
Arguments against the patch:
- NOP isn't supposed to spend thousands of CPU cycles jumping to the kernel and back
- With the GNU Assembler fix distributions can avoid adding multi-byte NOPs
- That patch wasn't accepted in to Linux
A bit later someone started the mailing list thread Promoting Crusoe and Geode Processors to i686 Status which took a look at the overall situation for those two CPUs. It argued that both CPUs supported the full i686 instruction set and that NOPL was not standard i686. As far as I can tell not much was done in response to this.
In 2021 the patch x86: add NOPL and CMOV emulation was proposed to the kernel again. As most 32-bit x86 distributions compiled for the i686 architecture this would let i586 or better CPUs run modern day 32-bit Linux distributions. This is especially useful for CPUs still manufactured and used today like Vortex86 CPUs. As it turns out, old machines don't just disappear. They just run out of date software.
Unfortunately a few days later the author followed up with some bad news. The Pentium Pro introduced conditional floating point operations and when used on systems that don't support them they silently fail instead of throwing an unknown instruction exception. This makes it effectively impossible to fully emulate the i686 instructions on i586 systems.
LLVM fallout[edit | edit source]
In 2010 r96988 was committed to LLVM. It made the compiler unconditionally output multi-byte NOPs for 32-bit and 64-bit x86 code. This happened regardless if the target architecture supported it, so output could break on systems that weren't even supposed to support multi-byte NOPs, like i586 or i386.
In 2011 someone reported 9.0 RC1/Clang / illegal instruction (Signal 4) in gengtype while building cc_tools on i586. to the FreeBSD mailing lists and in 2012 clang crashes on Geode was reported to the FreeBSD bug tracker.
After the first bug report, X86AsmBackend::WriteNopData uses long nops unconditionally was filed upstream to LLVM.
Later in 2012 LLVM r164132 was committed, adding a 'geode' CPU target to LLVM that didn't use multi-byte NOPs. This meant building for i686 without using multi-byte NOPs required building for Geode CPUs. Not very useful for generic i686 releases or for i586 and older machines that weren't supposed to support multi-byte NOPs.
In 2014 LLVM r195679 was committed to flat out avoid using multi-byte NOPs on i686, i586 and specific non-Intel and non-AMD CPU models that didn't support multi-byte NOPs.
Emulators[edit | edit source]
It's not just hardware that implements the i686 instruction set, software emulators can too. So which emulators support multi-byte NOPs?
In 2006 Bochs r7216 was committed, adding support for the multi-byte NOP opcode as long as Bochs was compiled to emulate an i686 or newer. Later in 2007 Bochs r7973 was committed, marking 0F 19 through 0F 1E as multi-byte NOPs based on AMD documentation. They didn't link to the documentation but it makes sense to me.
In 2006 QEMU r2145 was committed and made all hinting NOPs execute as multi-byte NOPs. This made it in to QEMU 0.9.0 which makes the Debian bug report reporting QEMU 0.9.1 as crashing due to NOPs surprising. Furthermore these NOPs are available on every emulated x86 CPU, 32-bit or 64-bit, regardless of whether it should have it or not.
In 2007 VirtualBox r2422 imported QEMU's i386 interpreter and gained multi-byte NOP support.
In 2020 Add hintable NOPs for Pentium Pro and II. was committed to PCem.
In 2022 src/cpu: Implement hinting NOPs was merged to DOSBox-X, the only DOSBox variant that supports Pentium Pro and newer CPUs.
Intel CET[edit | edit source]
In 2016 Intel announced Control-flow Enforcement Technology and released the Intel CET specification. These CPU extensions run not just in 64-bit mode but in 32-bit mode. While management for the shadow stack uses new instructions, the ENDBRANCH instruction intended to be compiled in to user space code re-uses the hinting NOP 0F 1E.
Unlike the multi-byte NOP there's no indication in the specifications that these instructions are limited to Pentium Pro or newer CPUs.
In 2017 Add support for Intel CET instructions was committed to the GNU Assembler.
Later in 2017 Update x86 backend to enable Intel CET. was committed to GNU GCC.
Even later in 2017 LLVM r318995 added support for CET. As far as I can still this doesn't limit the use of these CET instructions.
In 2021 gcc generates endbr32 invalid opcode on -march=i486 was reported to GCC. The next day x86: Error on -fcf-protection with incompatible target was committed to GNU GCC. This patch limits CET to architectures with CMOV. That's a safe bet, but seems like it would break on the Geode LX800 and other i686-compatibles that lack multi-byte NOPs.
In 2022 i586-unknown-linux-gnu target generates binaries containing Intel CET opcodes which are illegal on i586 processors was reported to the Rust bug tracker. A day or so later Gentoo committed dev-lang/rust: pass -fcf-protection=none on i586 despite Rust not being available on i586 yet. It's unclear how much things will break if someone gets an actual i686 build of Rust going.
Rust uses LLVM so this might indicate that LLVM doesn't check if an architecture supports CET before adding its instructions.
As of early 2022 Intel CET support is not in the kernel yet.
Conclusions[edit | edit source]
I have a few takeaways from this slow motion train wreck:
- Intel's documentation only applies to Intel CPUs
- Developers don't really question retroactive additions to instruction sets
- To some i686 is the Pentium Pro
- To others i686 is a baseline for various 32-bit x86 processors
Something else to just tack on here is that I spent a non-trivial amount of time trying to dig up old copies of Intel web pages and documentation. By the way Intel: When you make a new revision of a document you don't have to destroy the old ones.