- Updated: March 21, 2026
- 12 min read
C Bit‑Field Pitfalls: Understanding Struct Alignment and Portable Code
Skip to content HomeAbout Wanted List OS/2 History OS/2 Beginnings OS/2 1.0 OS/2 1.1 OS/2 1.2 and 1.3 OS/2 16-bit Server OS/2 2.0 OS/2 2.1 and 2.11 OS/2 Warp OS/2 Warp, PowerPC Edition OS/2 Warp 4 OS/2 Timeline OS/2 Library OS/2 1.x SDK OS/2 1.x Programming OS/2 2.0 Technical Library OS/2 Videos, 1987 DOS History DOS Beginnings DOS 1.0 and 1.1 DOS 2.0 and 2.1 DOS 3.0, 3.1, and 3.2 DOS 3.3 DOS 4.0 DOS Library NetWare History NetWare Timeline NetWare Library Windows History Windows Library PC UNIX History Solaris 2.1 for x86 ← DOS Memory Management Bitfield Pitfalls Posted on March 20, 2026 by Michal Necasek Some time ago I ran into a bug that had been dormant for some time. The problem involved expressions where one of the operands is a bit-field. To demonstrate the problem, I will present a reduced example: #include #include typedef struct { uint32_t uf1 : 12; uint32_t uf2 : 12; uint32_t uf3 : 8; } BF; int main( void ) { BF bf; uint64_t u1, u2; bf.uf1 = 0x7ff; bf.uf2 = ~bf.uf1; u1 = bf.uf1 << 20; u2 = bf.uf2 << 20; printf( "u1: %016" PRIX64 "\n", u1 ); printf( "u2: %016" PRIX64 "\n", u2 ); return( 0 ); } The troublesome behavior is demonstrated by the lines performing the left shift.We take a 12-bit wide bit-field, shift it left by 20 bits so that the high bit of the bit-field lines up with the high bit of uint32_t, and then convert the result to uint64_t. The contents of u1 will be predictable. The contents of u2 perhaps not so much. Or more specifically, the resulting value of u2 depends entirely on who you ask. First off, the problematic behavior shows up on platforms where int is 32 bits wide… which is only almost everything these days.It notably includes both 32-bit and 64-bit x86 platforms. According to Microsoft, the result is as follows: u1: 000000007FF00000u2: 0000000080000000 According to GNU C and clang, the result is something else: u1: 000000007FF00000u2: FFFFFFFF80000000 Needless to say, that’s a fairly major difference. Why oh Why? It’s apparent that Microsoft considers the result of the shift operation to be an unsigned integer, which is zero-extended to 64 bits.On the other hand, gcc and clang consider the result of the shift operation to be a signed integer, which is then sign-extended to 64 bits. To understand what is happening, one needs to answer a seemingly trivial question: What is the type of a bit-field? Note “seemingly”. The problem is that the C language standard (in particular talking about C99 here) provides at best unclear and at worst contradictory answers.First let’s see how bit-fields are defined: A bit-field shall have a type that is a qualified or unqualified version of _Bool, signed int, unsigned int, or some other implementation-defined type. C99 section 6.7.2.1 paragraph 4 That seems pretty unambiguous, doesn’t it? In our case, uint32_t maps to unsigned int, so according to 6.7.2.1 the bit field shall have the type unsigned int.Further down there’s the following: A bit-field is interpreted as a signed or unsigned integer type consisting of the specified number of bits. […] C99 section 6.7.2.1 paragraph 9 Note that the text says how a bit-field is interpreted, not what its type is, which may or may not be significant. Now we have to look at the part of the C standard which defines how types are promoted in expressions.The following may be used in an expression wherever an int or unsigned int may be used:— An object or expression with an integer type whose integer conversion rank is less than or equal to the rank of int and unsigned int.— A bit-field of type _Bool, int, signed int, or unsigned int.If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions.All other types are unchanged by the integer promotions. C99 section 6.3.1.1 paragraph 2 The term “original type” does a lot of work there. There is no ambiguity when, say, converting uint16_t to int (on a platform where int has 32 bits or more). The int type can clearly represent all values of the original type (typically uint16_t is equivalent to unsigned short), therefore the value is converted to (signed) int. But with bit-fields, it comes down to what precisely the “original type” is.Note that the wording of the Standard was changed in C11, likely to address exactly this kind of problem: If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. C11 section 6.3.1.1 paragraph 2 (excerpt) Bit-fields are explicitly called out and according to the new wording, it is much clearer that a bit-field such as unsigned int uf1 : 31; should be promoted to (signed) int on a platform where int is 32 bits wide. Two Schools of Thought In older C standards, things were genuinely unclear. Some compiler writers looked at a declaration such as unsigned int uf2 : 12; and said, okay, the “original” type of the uf2 bit-field is clearly unsigned int.Therefore, when evaluating an expression, the type of the bit-field remains unsigned. That is the thinking notably followed by Microsoft’s compilers. Other compiler writers looked at the same declaration and said, alright, we have an integer with twelve (unsigned) bits, which means that a signed integer can represent all values of the original type. Therefore, when evaluating an expression, the type of uf2 gets promoted to (signed) int.That line of thought is followed by gcc and clang, among others. Known Problem Needless to say, others have run into this issue in the past. An early mention of a closely related problem (bit-field initialization) is defect report #120 filed against C89 in 1993. The committee response states: Subclause 6.5.2.1 states “A bit-field is interpreted as an integral type consisting of the specified number of bits.” Thus the type of object1.bit and object2.bit can be informally described as unsigned int : 1. It is apparent that at least to some committee members, the clause “a bit-field is interpreted as an integral type consisting of the specified number of bits” meant that the type of a bit-field is an integral type consisting of the specified number of bits. The issue was further discussed by Joseph Myers in 2007 in WG14 document N1260, well after the C99 standard was published.Clearly even C11 did not fully resolve the problems and further discussion with even more bit-field pitfalls followed in 2022 in WG14 document N2958. The document notes that existing implementation do not always agree–precisely because using bit-fields can trigger several poorly specified edge cases. Compiler Survey To get a better sense of the situation, I tested a number of current and historic common PC compilers, trying to check how they deal with the problematic code I ran into.Note that for older 32-bit compilers that offer no long long type or similar (such as Microsoft’s __int64 type), there is no equivalent way to trigger the problem because int is the largest integer type already. But there is a good alternative: Converting a bit-field to double. That triggers the same process of first promoting the bit-field to either int or unsigned int and then to double. The equivalent problem does exist for 16-bit compilers.The long type is wider than int, therefore analogous situation can be triggered e.g. like this: /* For 16-bit C compilers */#include typedef struct { unsigned uf1 : 6; unsigned uf2 : 6; unsigned uf3 : 4;} BF;int main( void ){ BF bf; unsigned long u1, u2; bf.uf1 = 0x1f; bf.uf2 = ~bf.uf1; u1 = bf.uf1 << 10; u2 = bf.uf2 << 10; printf( "u1: %08lX\n", u1 ); printf( "u2: %08lX\n", u2 ); return( 0 );} All 16-bit compilers I tried produced the same result: u1: 00007C00u2: 00008000 In other words, the bit-field type always remained unsigned during promotions. The 16-bit compilers tested included: Microsoft C 5.0, 6.0, and Visual C++ 1.52; various versions of Watcom C; Borland C++ 3.1 and 4.0; Digital Mars 8.38. The situation was more interesting for 32-bit compilers.For compilers that do not support long long, I used a variant with the double type: /* For 32-bit C compilers */#include typedef struct { unsigned uf1 : 12; unsigned uf2 : 12; unsigned uf3 : 8;} BF;int main( void ){ BF bf; double d1, d2; unsigned u1, u2; bf.uf1 = 0x7ff; bf.uf2 = ~bf.uf1; d1 = bf.uf1 << 20; d2 = bf.uf2 << 20; u1 = bf.uf1 << 20; u2 = bf.uf2 << 20; printf( "d1 (u1): %lf (%X)\n", d1, u1 ); printf( "d2 (u1): %lf (%X)\n", d2, u2 ); return( 0 );} Most DOS/Windows compilers keep the existing behavior and produce the following: d1 (u1): 2146435072.000000 (7FF00000)d2 (u1): 2147483648.000000 (80000000) That includes: Borland C++ 5.5.1, Microsoft C/C++ from version 9.0 (Visual C++ 2.0, 1994) to at least version 19.41 (2025); numerous versions of Watcom C; Digital Mars 8.38. A notable exception was IBM’s compiler. VisualAge C++ 3.5 on Windows, as well as VisualAge C++ 3.0 on OS/2, produce the following: d1 (u1): 2146435072.000000 (7FF00000)d2 (u1): -2147483648.000000 (80000000) Since the VisualAge C++ 3.5 compiler also supports 64-bit integers, and even has inttype.h, I was able to test the first example as well: u1: 000000007FF00000u2: FFFFFFFF80000000 IBM clearly considers the bit-field width and an unsigned bit-field narrower than int gets promoted to a signed type. MingW gcc 3.4.5 produces the same result as VisualAge C++, that is, unsigned bit-fields get promoted to int if their width allows it. Current versions of gcc and clang behave the same as the old gcc 3.4.5. This may cause trouble when writing portable code, because Microsoft and non-Microsoft compilers deliver different results. By my reading of the current C standards, bit-field types should be considered to have the specified width in addition to the underlying type (int, unsigned int, etc.).That affects integer promotions. Microsoft’s compilers do match Microsoft’s own documentation which explicitly states the following: Bit fields have the same semantics as the integer type. A bit field is used in expressions in exactly the same way as a variable of the same base type would be used. It doesn’t matter how many bits are in the bit field.Microsoft may be unwilling to change the existing behavior because it would lead to the worst kind of “quiet change”–code that previously was and still is conforming suddenly produces different results. Summary Although bit-fields have been part of the C language since the 1970s, their precise semantics were not fully defined in the ANSI C89 standard, and the C99 revision didn’t bring much improvement either.Although C11 and later revisions did bring improvements, as late as 2022, some unclarities still remained—although to be fair, some of those were brought about by expanding the capabilities of bit-fields, such as allowing wider integer types to be used as the basis of bit-fields. The practical consequence is that when using bit-fields in some unusual contexts (and a few not so unusual), different compilers produce different results.Since no warnings are produced, this may lead to unpleasant surprises for programmers or end users. This entry was posted in C, Development, Standards. Bookmark the permalink. ← DOS Memory Management One Response to Bitfield Pitfalls random lurker says: March 21, 2026 at 6:53 am That’s actually hilarious. Could be a neat trick for IOCCC or the Underhanded C Contest. Leave a ReplyYour email address will not be published.Required fields are marked *Comment * Name * Email * Website Δ This site uses Akismet to reduce spam. Learn how your comment data is processed.Blog Comments Archives March 2026 February 2026 January 2026 December 2025 November 2025 October 2025 September 2025 August 2025 July 2025 June 2025 May 2025 April 2025 March 2025 February 2025 January 2025 December 2024 November 2024 October 2024 September 2024 August 2024 July 2024 June 2024 May 2024 April 2024 March 2024 February 2024 January 2024 October 2023 September 2023 August 2023 July 2023 June 2023 May 2023 April 2023 March 2023 January 2023 December 2022 November 2022 October 2022 September 2022 July 2022 June 2022 May 2022 April 2022 March 2022 February 2022 January 2022 December 2021 November 2021 October 2021 September 2021 August 2021 July 2021 June 2021 May 2021 April 2021 March 2021 February 2021 January 2021 December 2020 November 2020 October 2020 September 2020 August 2020 July 2020 June 2020 May 2020 April 2020 March 2020 February 2020 January 2020 December 2019 November 2019 October 2019 September 2019 August 2019 July 2019 June 2019 May 2019 April 2019 March 2019 February 2019 January 2019 December 2018 November 2018 October 2018 August 2018 July 2018 June 2018 May 2018 April 2018 March 2018 February 2018 January 2018 December 2017 November 2017 October 2017 August 2017 July 2017 June 2017 May 2017 April 2017 March 2017 February 2017 January 2017 December 2016 November 2016 October 2016 September 2016 August 2016 July 2016 June 2016 May 2016 April 2016 March 2016 February 2016 January 2016 December 2015 November 2015 October 2015 September 2015 August 2015 July 2015 June 2015 May 2015 April 2015 March 2015 February 2015 January 2015 December 2014 November 2014 October 2014 September 2014 August 2014 July 2014 June 2014 May 2014 April 2014 March 2014 February 2014 January 2014 December 2013 November 2013 October 2013 September 2013 August 2013 July 2013 June 2013 May 2013 April 2013 March 2013 February 2013 January 2013 December 2012 November 2012 October 2012 September 2012 August 2012 July 2012 June 2012 May 2012 April 2012 March 2012 February 2012 January 2012 December 2011 November 2011 October 2011 September 2011 August 2011 July 2011 June 2011 May 2011 April 2011 March 2011 January 2011 November 2010 October 2010 August 2010 July 2010 Categories 286 386 386MAX 3Com 3Dfx 486 8086/8088 Adaptec AGP AMD AMD64 Apple Archiving Assembler ATi BIOS Books Borland BSD Bugs BusLogic C C&T CD-ROM Cirrus Logic CompactFlash Compaq Compression Computing History Conner Corrections CP/M Creative Labs Crystal Semi Cyrix DDR RAM Debugging DEC Development Digital Research Documentation DOS DOS Extenders Dream E-mu Editors EISA Ensoniq ESDI Ethernet Fakes Fixes Floppies Floppy Images Graphics Hardware Hacks I18N IBM IDE Intel Internet Keyboard Kryoflux Kurzweil LAN Manager Legal Linux Marketing MCA Microsoft MIDI NetWare Networking NeXTSTEP NFS Novell NT OS X OS/2 PC architecture PC hardware PC history PC press PCI PCMCIA Pentium Pentium 4 Pentium II Pentium III Pentium Pro Plug and Play PowerPC Pre-release PS/2 QNX Quantum Random Thoughts RDRAM Roland Ryzen S3 SCO SCSI Seagate Security Site Management SMP Software Hacks Solaris Sound Sound Blaster Source code Standards Storage Supermicro TCP/IP ThinkPad Trident Typewriter UltraSound Uncategorized Undocumented UNIX UnixWare USB VGA VirtualBox Virtualization VLB Watcom Wave Blaster Western Digital Windows Windows 95 Windows XP Wireless WordStar X11 x86 x87 Xenix Xeon Yamaha OS/2 Museum Proudly powered by WordPress.