NeuroAgent

How to Make GCC Generate Pure 16-bit Code

Learn why GCC can't generate true 16-bit code and how to properly configure it for real mode processor operation using .code16gcc directives.

Question

How can I force GCC (i686) to generate true 16-bit code without using 32-bit registers like EAX, ECX, etc.?

I’m using the -m16 flag in GCC, but the generated code still uses 32-bit registers. How do I properly configure GCC to generate pure 16-bit code that’s compatible with real mode processor?

Here’s my Makefile and example code:

makefile
# Makefile

METAL=-m16 -nostdlib -ffreestanding

all: prepare
	i686-elf-as Bootloader.asm -o Build/Objects/Bootloader.o
	i686-elf-ld Build/Objects/Bootloader.o -o Build/Binaries/Bootloader \
		-Ttext 0x7C00 --oformat binary

	i686-elf-gcc -c Main.c -o Build/Objects/Main.o $(METAL)
	i686-elf-gcc -c Modules/*.c $(METAL) \
	&& mv *.o Build/Objects/Modules
	i686-elf-ld Build/Objects/Main.o Build/Objects/Modules/* -o Build/Binaries/Main \
		-Ttext 0x7E00 --oformat binary

	dd if=/dev/zero of=Build/Floppy.img \
		bs=1K count=360
	dd if=Build/Binaries/Bootloader of=Build/Floppy.img conv=notrunc
	dd if=Build/Binaries/Main of=Build/Floppy.img \
		seek=1 conv=notrunc
clean:
	-rm -r Build/{Objects,Binaries}
scrub:
	-rm -r Build

run:
	qemu-system-x86_64 -fda Build/Floppy.img

prepare:
	mkdir -p Build
	mkdir -p Build/{Objects,Binaries}
	mkdir -p Build/Objects/Modules
assembly
# Bootloader.asm

.code16
.globl boot

boot:
	movb $0, %ah
	movb $0x13, %al
	int $0x10

	movb $0x2, %ah
	movb $0x2, %al

	movw $0x7E00, %bx
	movb $0, %ch
	movb $2, %cl
	int $0x13

	jc error

	ljmp $0, $0x7E00

error:
	movb $0, %ah
	movb $3, %al
	int $0x10

	movb $0xE, %ah
	movb $'E', %al
	int $0x10

	cli
	hlt

.org 510
.word 0xAA55

When compiling with the -m16 flag, GCC still generates code using 32-bit registers, which isn’t suitable for real mode processor. How do I properly configure GCC to generate pure 16-bit code?

NeuroAgent

GCC cannot generate real pure 16-bit code without using 32-bit registers, even with the -m16 flag. GCC is fundamentally a 32-bit compiler and does not support generating real 16-bit code, although it can produce code that runs in 16-bit mode with some limitations.

Table of Contents

Why GCC cannot generate pure 16-bit code

GCC is a 32-bit compiler by its architecture and does not support generating real 16-bit code. As noted in sources, “gcc cannot actually generate real 16 bit code” and “GCC is 32 bit compiler, it can’t generate 16 bit assembly”. Even when using the -m16 flag, the compiler still produces 32-bit instructions that can run in 16-bit mode but are not purely 16-bit.

The main problem is that GCC internally uses 32-bit architecture and generates code that requires a 386-level or higher processor to execute in real mode.

What the -m16 flag actually does

With the -m16 flag, GCC generates code that:

  • Modifies instructions to run in 16-bit mode
  • Adds prefixes (66h and 67h) to switch between 16-bit and 32-bit modes
  • Uses 32-bit registers (EAX, ECX, etc.) with prefixes
  • Requires a 386 or higher processor, as it cannot work on purely 16-bit processors (8086/8088/80286)

As noted in sources, “it generates the same code as in 32 bit mode but with some adjustments to run in 16 bit mode” and “it still insists on using the 32-bit registers, even for 2 byte wide variables”.

Proper use of assembly directives

For proper 16-bit code generation, you need to use GNU AS assembly directives:

.code16 directive

assembly
.code16

This directive switches the assembler to pure 16-bit code generation mode. All instructions will use 16-bit operands and registers.

.code16gcc directive

assembly
.code16gcc

This directive is specifically designed for GCC-generated code. It differs from .code16 in that the instructions call, ret, enter, leave, push, pop, pusha, popa, pushf, popf are 32-bit by default.

For your C code case, you need to add at the beginning of each C file:

c
__asm__(".code16gcc\n");

Or create a header file code16gcc.h:

c
// code16gcc.h
#ifndef _CODE16GCC_H_
#define _CODE16GCC_H_
__asm__(".code16gcc\n");
#endif

And then include it in your C files:

c
#include "code16gcc.h"

Alternative solutions for pure 16-bit code

1. Using Open Watcom

Open Watcom natively supports 16-bit compilation and can generate real 16-bit code without using 32-bit registers.

2. Using Turbo C or Borland C++

These older compilers were specifically designed for 16-bit code and can generate pure 16-bit code.

3. Combined approach

Mix C code (with limitations) and pure assembly:

assembly
# pure16.asm
.code16
pure_16bit_function:
    ; pure 16-bit code
    movw %ax, %bx
    ret
c
// main.c
__asm__(".code16gcc\n");

extern void pure_16bit_function(void);

void c_function(void) {
    // code with limitations
    pure_16bit_function();
}

Limitations and processor requirements

GCC limitations

  • Requires a 386 or higher processor
  • Doesn’t work on 8086/8088/80286
  • Still uses 32-bit registers with prefixes
  • Limited support for data types (long ints and others)

Code requirements

c
// Won't work with pure 16-bit code
long large_value = 0xFFFFFFFF;  // 32-bit value
int* pointer = &some_variable;  // 32-bit pointers
c
// WILL work with limited 16-bit code
unsigned short value = 0xFFFF;  // 16-bit value

Practical recommendations

1. Update your Makefile

makefile
# Makefile

METAL=-m16 -ffreestanding -fno-stack-protector -fno-pic
ASFLAGS=-m16

# Add .code16gcc directive for C files
%.o: %.c
	i686-elf-gcc -c $< -o $@ $(METAL) -include code16gcc.h

%.o: %.asm
	i686-elf-as $< -o $@ $(ASFLAGS)

2. Create the proper header file

c
// code16gcc.h
#ifndef _CODE16GCC_H_
#define _CODE16GCC_H_
__asm__(".code16gcc\n");
#endif

3. Modify your C code

c
// Main.c
#include "code16gcc.h"

// Use only 16-bit types
unsigned short x = 0x1234;
unsigned char y = 0xAB;

// Avoid 32-bit operations
void my_function(void) {
    __asm__("pushw %ax");     // instead of push %eax
    __asm__("popw %ax");      // instead of pop %eax
    // etc.
}

4. Use proper directives in assembler

assembly
# Bootloader.asm
.code16gcc  # instead of .code16 for GCC compatibility

5. Alternative option - use assembler for critical code

If you really need pure 16-bit code, write critical parts in pure assembly:

assembly
# pure16.asm
.code16
pure_16bit_routine:
    movw %ax, %bx
    addw $1, %bx
    retw

And call it from C code.

Conclusion

  1. GCC cannot generate real pure 16-bit code - the compiler is fundamentally 32-bit by architecture and doesn’t support generating purely 16-bit code.

  2. The -m16 flag creates pseudo-16-bit code - it uses 32-bit instructions with prefixes and requires a 386+ processor.

  3. For better compatibility, use .code16gcc - this directive is specifically designed for GCC-generated code.

  4. Consider alternative compilers - for real pure 16-bit code, use Open Watcom, Turbo C, or Borland C++.

  5. Mix C and assembly - for performance-critical or compatibility-critical code sections, use pure assembly.

Real 16-bit compatibility with 8086/8088/80286 processors requires either using specialized compilers or writing the code completely in assembly.

Sources

  1. Stack Overflow - How to tell GCC to generate 16-bit code for real mode
  2. GNU Assembler Documentation - i386-16bit
  3. OSDev Forum - gcc 16bit
  4. Stack Overflow - gcc compiles 16 bit real time code with -m16 option
  5. dc0d32 Blog - Real mode in C with gcc