How can I force GCC (i686) to generate true 16-bit code without using 32-bit registers (EAX, ECX, EDX, etc.)? Despite using the -m16 flag in my Makefile and the -ffreestanding option, my assembly code still contains 32-bit instructions, as visible in the objdump output. What additional compiler options or techniques can be used to ensure pure 16-bit code generation for a bootloader? How can I avoid using extended registers in 16-bit mode, given that -march only supports 386 and above?
GCC cannot generate true pure 16-bit code without 32-bit instructions since it’s a 32-bit compiler. Even with the -m16 flag, it creates code that requires a 386+ processor and uses 32-bit operand-size/address-size prefixes. To obtain truly pure 16-bit code compatible with 8086/8088/80286, alternative compilers or a combination of GCC with GNU Assembler and special directives are necessary.
Table of Contents
- Why GCC doesn’t generate pure 16-bit code
- Problem with the -m16 flag
- Solutions to minimize 32-bit instructions
- Alternative compilers for pure 16-bit code
- Practical examples for bootloader
- Build configuration recommendations
Why GCC doesn’t generate pure 16-bit code
GCC was originally designed as a 32-bit compiler and cannot generate “true” pure 16-bit code that would work on 8086, 8088, or 80286 processors without modifications. As noted in research, “GCC is 32 bit compiler, it can’t generate 16 bit assembly”.
Key GCC limitations:
- Generates code requiring a 386 or higher processor
- Uses 32-bit instructions even in 16-bit mode
- Applies operand-size and address-size prefixes for compatibility
- Limited to 64K memory segment in 16-bit mode
Important: Code generated by GCC with -m16 will not work on real hardware before a 286 processor, as it contains instructions like
pushl,calll,retl,leavel,jmplwhich are not present on earlier processors source.
Problem with the -m16 flag
The -m16 flag in GCC doesn’t create pure 16-bit code, but only adds the .code16gcc directive at the beginning of the assembly output. According to GNU documentation:
The -m16 option is the same as -m32, except for that it outputs the .code16gcc assembly directive at the beginning of the assembly output so that the binary can run in 16-bit mode
Characteristics of code with -m16:
- Unavoidable 32-bit instructions: Even with -m16, GCC generates 32-bit versions of instructions with prefixes
- Processor requirement: Code requires a 386+ processor due to the use of extended instructions
- Code size: Code becomes larger due to the need for prefixes in 16-bit mode
- Limited compatibility: Doesn’t work on 8086/8088/80286 without emulation
As noted on Reddit, “The catch is that with GCC you still need a 386 or better processor to run such code.”
Solutions to minimize 32-bit instructions
Despite GCC’s limitations, there are techniques to minimize the use of 32-bit registers and instructions:
1. Using the .code16gcc directive
Add at the beginning of your C file:
asm(".code16gcc");
This directive tells the GNU Assembler that the code is intended for 16-bit mode and will be compiled by GCC. As explained in Stack Overflow:
put asm(“.code16gcc”) at the start of your C source, your program will be limited to 64Kibytes
2. Compilation with -ffreestanding and additional options
CFLAGS = -m16 -ffreestanding -fno-pic -fno-pie -fno-stack-protector \
-fno-unit-at-a-time -fno-toplevel-reorder
The -fno-unit-at-a-time and -fno-toplevel-reorder options are important so that the asm() directive is processed before the rest of the code is compiled source.
3. Using inline assembly for register control
For critical code sections, use inline assembly with explicit 16-bit registers:
asm("mov %ax, %bx"); // instead of mov %eax, %ebx
asm("push %ax"); // instead of push %eax
4. Restricting instruction set with -march
Although -march only supports 386+, you can try:
CFLAGS += -march=i386 -mtune=i386
However, this won’t eliminate the need for 32-bit prefixes.
Alternative compilers for pure 16-bit code
To get truly pure 16-bit code compatible with early processors, consider these alternatives:
1. OpenWatcom C/C++
As noted in research, “The only way to generate 16-but executables is with OpenWatcom”. OpenWatcom can generate true 16-bit code without 32-bit prefixes.
2. IA- GCC Port
There’s a specialized GCC port for 16-bit code:
apt-get install gcc-ia16 # for Debian/Ubuntu
Usage:
CC = ia16-elf-gcc CFLAGS = -march=i8086 -msmall-code
3. Turbo C/C++ or Borland C++
Classic compilers that were originally created for 16-bit environments.
4. Hybrid approach: C for logic, Assembly for low-level code
// boot.c
asm(".code16gcc");
void main() {
// C code for logic
asm("mov $0x0E, %ah"); // BIOS teletype output
asm("mov %al, %bh"); // Using 16-bit registers
}
Practical examples for bootloader
Example Makefile for GCC -m16
CC = gcc
CFLAGS = -m16 -ffreestanding -fno-pic -fno-pie -fno-stack-protector \
-fno-unit-at-a-time -fno-toplevel-reorder -O0 -nostdlib
LDFLAGS = -Ttext 0x7C00 -e main -o boot.bin
bootloader: bootloader.c
$(CC) $(CFLAGS) -c bootloader.c -o bootloader.o
ld $(LDFLAGS) bootloader.o
clean:
rm -f *.o *.bin
Example of minimizing 32-bit code
asm(".code16gcc");
void print_char(char c) {
// Instead of:
// asm("mov $0x0E, %ah");
// asm("mov %al, %bh");
// We use explicit 16-bit operations
asm volatile("mov $0x0E, %%ah" ::);
asm volatile("mov %%al, %%bh" : : "a"(c));
}
void main() {
print_char('A');
// Infinite loop
for(;;);
}
Checking code with objdump
objdump -d boot.bin | grep -E "(eax|ebx|ecx|edx|esp|ebp|esi|edi)"
This command will show any remaining 32-bit registers in the code.
Build configuration recommendations
Configuring GCC for minimal 32-bit code
# Maximum restrictions for minimal 32-bit code
CFLAGS = -m16 -ffreestanding -fno-pic -fno-pie \
-fno-stack-protector -fno-unit-at-a-time \
-fno-toplevel-reorder -O0 -nostdlib \
-march=i386 -mtune=i386 \
-masm=intel
Building directly with GNU Assembler
If you want more control, you can use GNU Assembler directly:
# Compile C code
$(CC) $(CFLAGS) -S bootloader.c -o bootloader.s
# Manual assembly modification (optional)
sed -i 's/eax/ax/g' bootloader.s
sed -i 's/ebx/bx/g' bootloader.s
# ... and so on for all 32-bit registers
# Compile to binary
as --16 bootloader.s -o bootloader.o
ld -Ttext 0x7C00 -e main -o boot.bin bootloader.o
Alternative toolchain
For pure 16-bit code, use specialized tools:
# Install 16-bit code toolchain
sudo apt install gcc-ia16 binutils-ia16
# Usage
ia16-elf-gcc -march=i8086 -msmall-code bootloader.c -o bootloader.bin
Sources
- How to tell GCC to generate 16-bit code for real mode - Stack Overflow
- r/osdev on Reddit: C compiler for 16-bit x86 bootsector
- 16BIT C compiler (Maybe GCC!) - OSDev.org
- Using as - i386-16bit - GNU Manual
- GCC x86 Options Documentation
- Linux Jianghu: Use GCC and GNU Binutils to write 16-bit code
- Writing 16-bit Code - Stack Overflow
- Help in building an 16 bit os - Stack Overflow
Conclusion
- GCC doesn’t generate pure 16-bit code: Even with -m16, it creates code requiring a 386+ processor and using 32-bit prefixes
- Minimizing 32-bit instructions: Use .code16gcc, -ffreestanding, and limit optimizations to reduce 32-bit code
- Alternative compilers: For true pure 16-bit code, use OpenWatcom, IA- GCC port, or a hybrid C/Assembly approach
- Practical recommendations: For a bootloader, it’s better to use specialized toolchains or accept GCC limitations and test on an emulator
- Testing: Always check the generated code with objdump to ensure there are no unwanted 32-bit instructions
For creating a bootloader compatible with early x86 processors, it’s recommended to use specialized compilers or a hybrid approach with minimal GCC usage and manual control of assembly output.