Big-Endian Testing with QEMU
hanshq.net
hanshq.net
But Hans's post uses user-mode emulation with qemu-mips, which avoids having to set up a whole big-endian system in QEMU. It is a very interesting approach I was unaware of. I'm pretty sure qemu-mips was available back in 2010, but I'm not sure if the gcc-mips-linux-gnu cross-compiler was readily available back then. I suspect my PPC-based solution might have been the only convenient way to solve this problem at the time.
Thanks for sharing it here. It was nice to go down memory lane and also learn a new way to solve the same problem.
What you should do instead is write all your code so it is little-endian only, as the only relevant big-endian architecture is s390x, and if someone wants to run your code on s390x, they can afford a support contract.
The adjacent POWER architecture is also still relevant - but as you say, they too can afford a support contract.
However if designing a new network protocol, choosing big endian is insanity. Use little endian, skip the macros, and just add
#ifndef LITTLE_ENDIAN
#error
Or the like to a header somewhere.Adding other architectures to your build system also tends to reveal nasty bugs in general, e.g. you were unknowingly triggering UB on all architectures but on the one you commonly use it causes silent data corruption whereas one with a different memory layout results in a much more conspicuous segfault.
FWIW I doing hobby-stuff for Amiga's (68k big-endian) but that's just that, hobby stuff.
If the data stream encodes values with byte order B, then the algorithm to decode the value on computer with byte order C should be about B, not about the relationship between B and C.
One cannot just ignore the big/little data interchange problem MacOS[1], Java, TCP/IP, Jpeg etc...The point (for me) is not that your code runs on a s390, it is that you abstract your personal local implementation details from the data interchange formats. And unfortunately almost all of the processors are little, and many of the popular and unavoidable externalization are big...
[0] https://commandcenter.blogspot.com/2012/04/byte-order-fallac... [1] https://github.com/apple/darwin-xnu/blob/main/EXTERNAL_HEADE...
Their x86 changeover moved the CPU's to little-endian and Aarch64 continues solidifies that tradition.
Same with Java, there's probably a strong influence from SPARC's and with PPC, 68k and SPARC being relevant back in the 90s it wasn't a bold choice.
But all of this is more or less legacy at this point, I have little reason to believe that the types of code I write will ever end up on a s390 or any other big-endian platform unless something truly revolutionizes the computing landscape since x86, aarch64, risc-v and so on run little now.
Most CPUs (including x86-64) have variants of the load and store instructions that reverse the byte order (e.g. MOVBE in x86-64). The remaining CPUs have byte reversal instructions for registers, so a reversed byte order load or store can be simulated by a sequence of 2 instructions.
So the little-endian types and the big-endian data types must be handled identically by a compiler, except that the load and store instructions use different encodings.
The structures used in a data-exchange format must be declared with the correct types and that should take care of everything.
Any decent programming language must provide means for the user to define such data types, when they are not provided by the base language.
The traditional UNIX conversion functions are the wrong way to handle endianness differences. An optimizing compiler must be able to recognize them as special cases in order to be able to optimize them away from the machine code.
A program that is written using only data types with known endianness can be compiled for either little-endian targets or big-endian targets and it will work identically.
All the problems that have ever existed in handling endianness have been caused by programming languages where the endianness of the base data types was left undefined, for fear that recompiling a program for a target of different endianness could result in a slower program.
This fear is obsolete today.
Just treat the data on disk / on the wire as if it were in some encoded format. Parse on load. Encode back out to the expected format when you save it. Within your program, just use your language's native int formats.
For example, in C I use something like this:
uint32_t read_be_u32(uint8_t data[4]) {
return ((uint32_t)data[0] << 24) |
((uint32_t)data[1] << 16) |
((uint32_t)data[2] << 8) |
((uint32_t)data[3]);
}
... And the equivalent for little endian data. Modern optimizers will happily turn that into the right instructions - either a noop or bswap - as appropriate depending on the target architecture.You can do the same thing in Rust, Go, or any other language. No special type definitions or macros necessary.
The vast majority of modern network protocols use little endian byte ordering. Most Linux filesystems use little endian for their on-disk binary representations.
There is absolutely no good reason for networking protocols to be defined to use big endian. It's an antiquated arbitrary idea: just do what makes sense.
Use these functions to avoid ifdef noise: https://man7.org/linux/man-pages/man3/endian.3.html
You should actually use format-swapping loads/stores (i.e deserialization/serialization).
This is because your computer can not compute on values of non-native endianness. As such, the value is logically converted back and forth on every operation. Of course, a competent optimizer can elide these conversions, but such actions fundamentally lack machine sympathy.
The better model is viewing the endianness as a serialization format and converting at the boundaries of your compute engine. This ensures you only need to care about endianness when serializing and deserializing wire formats and that you have no accidental mixing of formats in your internals; everything has been parsed to native before any computation occurs.
Essentially, non-native endianness should only exist in memory and preferably only memory filled in by the outside world before being parsed.
These days it's bi, actually :) Although I don't see any CPU designer actually implementing that feature, except maybe MIPS (who have stopped working on their own ISA, and now want all their locked-in customers to switch to RISC-V without worrying about endianness bugs)
ARM works the same way. And SPARC is the opposite, instructions are always big-endian, but data can be switched to little-endian.
This argument is pretty silly: visualizations can always be changed. For some time I have been thinking that hexdumps on little-endian systems ought to be written right-to-left: in fact, when I once decided to include such a right-to-left dumper in my own software, it took me very little time for me to get used to, and I immediately started regretting I don't have it available everywhere.
> it’s easier to draw pictures as a matrix of bytes that are transmitted from left to right and top to bottom.
There are many reasons for big endian... but that is not one of them :)
> But processors are all over the map
That's not true anymore, big endian is dead. Upstream Linux is refusing to support big endian riscv at all, and is making serious noises about ripping out the existing big endian aarch64 support because the companies that ship the hardware that needs it don't work upstream.
(1) for JPG for embedded TIFF metadata which can have both.
[2] https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.ht...
The only question that matters: Do your customers / users want to run it on big-endian hardware? And for 99% of programmers, the answer is no, because their customers have never knowingly been in the same room as a big-endian CPU.
The second sentence, weather your customers know if they have been in the same room with a big-endian system (CPU alone doesn't matter) is irrelevant when the point is to write correct code. Many of then aren't interested in this or other details and that is ok as they are not responsible for the implementation.
Changing the endianness either direction did have show bugs to me several times, that could be fixed, and it was worth it for that alone.
Or you can just be a nice person and make your code endian-agnostic. ;-)
In fact, I'd be surprised if you made a big endian arch and then ran a browser on it if some large number of websites would fail because they used typedarrays and aren't endian aware.
The solution is not to ask every programmer in the universe to write endian aware code. The solution is to standardize on little endian
It costs nothing other than having separate instructions for the different endian types.
The reason for this is that on the transistor level it takes exactly zero transistors to implement a byte swap since all you are changing is in which order the wires are connected.
Forcing software to deal with the pain of big endian support in exchange for saving a nonexistent cost in hardware is such a bad trade that it's on the same level of stupidity as not applying a clear coat on a car and then seeing them rust and expecting the owner of the car to thoroughly wax the car frequently to prevent the inevitable formation of rust.
Of course that’s not what people will do. They’ll write code and not have any idea which parts have a dependency on endianness. It won’t be given a thought during their design or testing and when they need to make it work on a different architecture, it will needlessly be a giant pain in the ass.
I was at IBM when we gave up on big endian for Power. Too much new code assumed LE, and we switched, despite the insane engineering effort (though TBH, that effort had the side effect of retaining some absolutely first-class engineers a few more years).
On Linux it's really as simple as installing QEMU binfmt and doing:
GOARCH=s390x go testI contend it's almost never important and almost nobody writing user software should bother with this. Certainly, people who didn't already know they needed big-endian should not start caring now because they read an article online. There are countless rare machines that your code doesn't run on--what's so special about big endian? The world is little endian now. Big endian chips aren't coming back. You are spending your own time on an effort that will never pay off. If big endian is really needed, IBM will pay you to write the s390x port and they will provide the machine.
One difference is that when your endian-oblivious code runs on a BE system, it can be subtly wrong in a way that's hard to diagnose, which is a whole lot worse than not working at all.
You're also the most likely person to try to run your code on an 18 bit machine.
> [fixes] specific to VMS (a.k.a. OpenVMS),
> For conformity with DECSYSTEM-20 Kermit ...
> running on a real Sun3, compiled with a non-ANSII compiler (Sun cc 1.22)
> this is fatal in HP-UX 10 with the bundled compiler
> OpenWatcom 1.9 compiler
> OS/2 builds
> making sure that all functions are declared in both ANSI format and K&R format (so C-Kermit can built on both new and old computers)
Oooooh! A clang complaint: 'Clang also complains about perfectly legal compound IF statements and/or complex IF conditions, and wants to have parens and/or brackets galore added for clarity. These statements were written by programmers who understood the rules of precedence of arithmetic and logical operators, and the code has been working correctly for decades.'
As of the fourth Beta, DECnet support has been re-enabled. To make LAT or CTERM connections you must have a licensed copy of Pathworks32 installed.
SSH is now supported on 32bit ARM devices (Windows RT) for the first time
REXX support has been extended to x86 systems running Windows XP or newer. This was previously an OS/2-only feature.
No legacy telnet encryption (no longer useful, but may return in a future release anyway)
For context:
The first new Kermit release for Windows in TWENTY-TWO YEARS
Yes, it's called Kermit 95 once again! K95 for short. 2025 is its 40th anniversary.
Granted, I still work on a fair number of big endian systems even though my daily drivers (ppc64le, Apple silicon) are little.
How come you're running ppc64le as a daily driver?
Many of the tests I did back in the 1990s seem pointless now. Do you have checks for non-IEEE 754 math?
It is _all_ non-IEEE 754 math.
That it isn't compliant is a compiler guarantee, in the current state of things.
You may as well have an `assert(1)`.
I don't support the full range of platforms that C supports. I assume 8 bit chars. I assume good hardware support for 754. I assume the compiler's documentation is correct when it says it map "double" to "binary64" and uses native operations. I assume if someone else compiles my code with non-754 flags, like fused multiply and add, then it's not a problem I need to worry about.
For that matter, my code doesn't deal with NaNs or inf (other than input rejection tests) so I don't even need fully conformant 754.
You wrote "I generally include various static asserts about basic platform assumptions."
I pointed out "There's platform and there's platform.", and mentioned that I assume POSIX.
So of course I don't test for CHAR_BIT as something other than 8.
If you want to support non-POSIX platform, go for it! But adding tests for every single one of the places where the C spec allows implementation defined behavior and where all the compilers I used have the same implementation defined behavior and have done so for years or even decades, seems quixotic to me so I'm not doing to do it.
And I doubt you have tests for every single one of those implementation-defined platform assumptions, because there are so many of them, and maintaining those tests when you don't have access to a platform with, say, 18-bit integers to test those tests, seems like it will end up with flawed tests.
No? I don't over generalize for features I don't use. I test to confirm the presence of the assumptions that I depend on. I want my code to fail to compile if my assumptions don't hold.
I don't recall if I verify CHAR_BIT or not but it wouldn't surprise me if I did.
I can't support IEEE 754, so its simply irrelevant - so long as I know I cannot support it, and behaviour differs.
Even for "regular" architectures this turns out to be important for FP data types. Long double is an f128 on Emscripten but an f80 on x86_64 Clang, where f128 is provided as __float128. The last time I updated my code (admittedly quite a while ago) Clang version 17 did not (yet?) implement std::numeric_limits support for f128.
Honestly there's no good reason not to test these sorts of assumptions when implementing low level utility functions because it's the sort of stuff you write once and then reuse everywhere forever.
Huh. https://en.cppreference.com/w/c/23.html says the "Old feature-test macro" __STDC_IEC_559__ was deprecated in C23, in favor of __STDC_IEC_60559_BFP__ .
Its one of the caveats of the C-family that developers are supposed to be aware of, but often aren't. It doesn't support IEEE 754 fully. There is a standard to do so, but no one has actually implemented it.
Of course in my case what I'm actually concerned with is the behavior surrounding inf and NaN. Thankfully I've never been forced to write code that relied on subtle precision or rounding differences. If it ever comes up I'd hope to keep it to a platform independent fixed point library.
But, for example, LLVM does not fully support IEEE 754 [0].
And nor does GCC - who list it as unsupported, despite defining the macro and having partial support. [1]
The biggest caveat is in Annex F of the C standard:
> The C functions in the following table correspond to mathematical operations recommended by IEC 60559. However, correct rounding, which IEC 60559 specifies for its operations, is not required for the C functions in the table.
The C++ standard [2] barely covers support, but if a type supports any of the properties of ISO 60559, then it gets is_iec559 - even if that support is _incomplete_.
This paper [3] is a much deeper dive - but the current state for C++ is worse than C. Its underspecified.
> When built with version 18.1.0 of the clang C++ compiler, without specifying any compiler options, the output is:
> distance: 0.0999999
> proj_vector_y: -0.0799999
> Worse, if -march=skylake is passed to the clang C++ compiler, the output is:
> distance: 0.1
> proj_vector_y: -0.08
[0] https://github.com/llvm/llvm-project/issues/17379
[1] https://www.gnu.org/software/gcc/projects/c-status.html
[2] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/n49...
u32::from_le_bytes
u32::from_ne_bytes the n stands for native
But for everything else, it's fine to assume little-endian.
You sound like some sort of purist, so sure, if you really want to be explicit and support both endiannesses in your software when needed, go for it. But as general advice to random programmers: don't bother.
My modern choice is just to make clear to BE users I don't support them, and while I will accept patches I'll make no attempt to bugfix for them, because every time I try to get a BE VM running a modern linux it takes a whole afternoon.
It's also increasingly hard to test. Particularly when you have large expensive testsuites which run incredibly slowly on this simulated machines.
presented at Embedded Linux Conf
Do they do this? I thought they swapped this as well.
The most common is to start from the most significant digit and read left-to-right until the last two digits, which you then read right-to-left.
A less common alternative is to read right-to-left starting from the least significant digit.
Now that I think about it though, I've only seen the latter way used for the year in a date.
Most (all?) Western languages say out their numbers in big endian, as do East Asian languages like Chinese, Japanese and Korean. It is only natural that we write down our numbers in big endian, it can be argued that the mistake was making little endian CPUs.
Big endian: byte N has value 256^(L-N), bit n has value 2^n or 2^(l-n) depending on the architecture (some effectively have little bit-endian but big byte-endian) and where L and l are the byte and bit size of the whole integer respectively.
Design hardware or even write arbitrary precision routines, and you'll quickly realise that "big endian is backwards, little endian is logical".
Language is messy, some more than others [1].
Yeah, I know there are exceptions, but on average most human languages are big endian.
Of course the endianness only matters to C programmers who take endless pleasure in casting raw data from external sources into structs.
https://gist.github.com/siraben/cb0eb96b820a50e11218f0152f2e...
Nice article! But pity it does not elaborate on how...
Eh, is it? There aren't any big endian systems left that matter for anyone that isn't doing super niche stuff. Unless you are writing a really foundation library that you want to work everywhere (like libc, zlib, libpng etc.) you can safely just assume everything is little endian. I usually just put a static_assert that the system is little endian for C++.