Back When Unions Were Useful

Though it’s shunned today, using a union in your C code was common in the 1980s. Back then, compilers and IDEs such as Borland’s TurboC were customized for the MS-DOS operating system. Daring C programmers used the C language’s union keyword to access the system’s hardware directly. Scary.

Hardware meddling, specifically accessing the CPU’s registers directly from within your C code, was possible because your program was the only thing running on the computer at the time; calling or responding to an 8086 interrupt was okee-doke. So loading up a few registers via a union wasn’t something that made other programs go nuts.

To make the magic happen, the C source code included the dos.h header file. This header file prototyped a few handy but DOS-specific functions as well as various defined constants and other stuff useful to that operating system. Remember that this era was when the K&R version of C was the only standard. It was years before ANSI C came about. In fact, the chaos of C versions and varieties is what prompted the development of the POSIX standard in 1988.

In the dos.h header file was the definition for a union you could load up with values for low-level system access:

union REGS {
    struct  WORDREGS x;
    struct  BYTEREGS h;
};

The union contains two structures, WORDREGS and BYTEREGS. Yes, unions can contain structures as they are a valid C language data type.

Each structure in the REGS union represents a different sized CPU register, 16-bit and 8-bit respectively. This layout reflects the architecture of the 8086 CPU found in the original IBM PC and its various copies or “clones.” The 8086 CPU registers are illustrated in Figure 1.

8086 CPU Architecture (limited)

Figure 1. Basic 8086 CPU architecture. The data registers are shown, along with SI and DI. What’s not shown are the IP (instruction pointer), SP (stack pointer), flags registers, and others.

For example, the AX register holds 16 bits. The lower part, only 8-bits, is register AL. The upper part is register AH. Together, they make register AX. From the REGS union, AX is the “word” register, WORDREGS; AL or AH is a “byte” register, BYTEREGS. The goal of the union is to represent both of these items, AL and AH, which fit into register AX that holds the same data. Hence, the union is an ideal structure to reference data stored in whichever register type is required.

Here are the structures associated with the REGS union:

struct WORDREGS {
    unsigned int ax,bx,cx,dx,si,di,cflag,flags;
};

struct BYTEREGS {
    unsigned char al,ah,bl,bh,cl,ch,dl,dh;
};

The REGS union was used with the int86() function, which initiated an 8086 interrupt, or a system call directly to the CPU. Here’s sample code:

2026_09_26-Lesson.c

/* Clear the screen via the BIOS */
#include <dos.h>

int main()
{
    union REGS in,out;

/* Read the current video mode,
   BIOS function 0x0f, set in register ah */
    in.h.ah = 0x0f;
    int86(0x10,&in,&out);    /* call the BIOS */

/* the CPU register values are now saved in the
   'out' union */

/* Call BIOS function 0x00 to clear the screen */
    /* BIOS function 0x00, set in register ah */
    in.h.ah = 0x00;

    /* copy the video mode number currently in
       the al register */
    in.h.al = out.h.al;

    /* make the call */
    int86(0x10,&in,&out);

/* the screen is cleared */

    return 0;
}

This code calls the system BIOS via 8086 interrupt, 0x10. The BIOS function is loaded into the ah register, which is represented in the C code via REGS union member in.h.al. The int86() function — unique to MS-DOS and prototyped in the dos.h header file — requires three arguments: the interrupt number, the address of the CPU registers to set before the call, and the address for CPU registers set after the call.

The first call to int86() obtains the video mode, which is saved in the al register after the call returns.

The second call to int86() clears the screen for the mode obtained from the first call.

The program has no output and it will not build on a modern computer. My point is to reminisce (of course), but also to show a practical example of how a union can work to represent multiple values referenced in different ways. But unions can also be abused, which I cover in next week’s Lesson.

5 thoughts on “Back When Unions Were Useful”

  1. I too remember using software interrupts under MS-DOS for all sorts of things that would otherwise have required assembly code.

    However, I think the word ‘shunned’ is a bit of an exaggeration here. While structures are much more common than unions, there still are use cases for them.

    Specifically, in order to enable code optimizations based on Type-Based Alias Analysis (TBAA), classic type punning,

    float fp_val = M_PI;
    uint32_t int_repr = *(uint32_t *)&fp_val;

    has been considered undefined behavior⁽¹⁾ since C90 (as per the rules in §6.3 Expressions of the ISO/IEC 9899:1990 standard).

    Now, personally, I donʼt like this rule change (never have), so I disable this behavior in GCC and Clang for all my programs by supplying -fno-strict-aliasing on the command line.

    That said, there are essentially only two “official” ways to perform such a reinterpretation of data in a standards-compliant manner:

    • Copy the bits via memcpy():

    memcpy (&int_repr, &fp_val, sizeof(int_repr));

    • Access the same byte values via different union members:

    union {
    float as_float;
    uint32_t as_uint;
    } variant;

    variant.as_float = M_PI;
    uint32_t int_repr = variant.as_uint;

    Used as such, unions are used quite heavily in some codebases ☺ (Especially in math related ones like libcoremath.)

    ⁽¹⁾ Modern compilers like GCC and Clang actually treat this as undefined behavior only when at least -std=c99 is specified, since that standard introduced the concept of an effective type to clarify the intended meaning of §6.5 Expressions, paragraph 7, in the ISO/IEC 9899:1999 standard.

  2. uint32_t int_repr = *(uint32_t *)&fp_val;

    Golly. It would take me some time to figure that out. At first glance, it looks like a noob’s attempt to unravel a pointer problem. Even so, clever stuff. I can appreciate a union from that perspective, but this storage container still appears on the warnings lists when you look at secure coding in C.

  3. I guess youʼre referring to MISRA guidelines with the above remark. (Even there unions are considered problematic primarily when they are untagged though.)

    I would argue that the problem such rules aim to address is mostly that reinterpreting bits via unions could lead to accessing values that are “trap representations”.

    If one knows the target platform, is certain that one is dealing with twoʼs compliment and IEEE-754 floating point values these rules loose much of their bite.

    Had to research that a bit myself, but one has to look at older machines to find examples where type punning via unions actually can be problematic. For the VAX architecture, the MOVF instruction appears to have been an example of this. There, even just accessing a floating-point value with sign-bit=1, biased-exponent=0, fraction=* (i.e. simply reading such a value) seems to have caused a trap⁽¹⁾, a “reserved operand exception”.

    On modern architectures, be it x86, ARM or RISC-V, even sNaNs (signaling NaNs) arenʼt truly “trap representations”, as merely reading such values is unproblematic. Even with sNaN values a SIGFPE will only result when one tries to use them as operands in floating-point operations.

    ⁽¹⁾ As documented, for example, on page 6-18 of this VAX-11 reference manual.

  4. I’m getting spanked on LinkedIn where I shared this post. There is definitely an issue between tagged and untagged unions. I don’t make this distinction here.

  5. I am sorry you have to take the heat for my (apparently) controversial assertions!

    I guess we C programmers are quite an opiniated bunch ☺ (Besides the point, but I wonder what the median age for C programmers currently is. Of course, itʼs nigh impossible to get hard data on this… but Iʼd guess itʼs already over forty.)

    Anyway, it surely is the case that with tagged unions itʼs simply less likely that a previously written value will be read back using the wrong union member than with untagged unions. That’s about it, I’d say.

Leave a Reply