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
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
# 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?
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
- What the -m16 flag actually does
- Proper use of assembly directives
- Alternative solutions for pure 16-bit code
- Limitations and processor requirements
- Practical recommendations
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
.code16
This directive switches the assembler to pure 16-bit code generation mode. All instructions will use 16-bit operands and registers.
.code16gcc directive
.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:
__asm__(".code16gcc\n");
Or create a header file code16gcc.h:
// code16gcc.h
#ifndef _CODE16GCC_H_
#define _CODE16GCC_H_
__asm__(".code16gcc\n");
#endif
And then include it in your C files:
#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:
# pure16.asm
.code16
pure_16bit_function:
; pure 16-bit code
movw %ax, %bx
ret
// 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
// Won't work with pure 16-bit code
long large_value = 0xFFFFFFFF; // 32-bit value
int* pointer = &some_variable; // 32-bit pointers
// WILL work with limited 16-bit code
unsigned short value = 0xFFFF; // 16-bit value
Practical recommendations
1. Update your 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
// code16gcc.h
#ifndef _CODE16GCC_H_
#define _CODE16GCC_H_
__asm__(".code16gcc\n");
#endif
3. Modify your C code
// 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
# 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:
# pure16.asm
.code16
pure_16bit_routine:
movw %ax, %bx
addw $1, %bx
retw
And call it from C code.
Conclusion
-
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.
-
The -m16 flag creates pseudo-16-bit code - it uses 32-bit instructions with prefixes and requires a 386+ processor.
-
For better compatibility, use .code16gcc - this directive is specifically designed for GCC-generated code.
-
Consider alternative compilers - for real pure 16-bit code, use Open Watcom, Turbo C, or Borland C++.
-
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.