Как заставить GCC (i686) генерировать настоящий 16-битный код без использования 32-битных регистров вроде EAX, ECX и т. д.?
Я использую флаг -m16 в GCC, но сгенерированный код все равно использует 32-битные регистры. Как правильно настроить GCC для генерации чистого 16-битного кода, совместимого с реальным режимом процессора?
Вот мой 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
# 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
При компиляции с флагом -m16 GCC все равно генерирует код с использованием 32-битных регистров, что не подходит для реального режима процессора. Как правильно настроить GCC для генерации чистого 16-битного кода?
GCC не может генерировать настоящий чистый 16-битный код без использования 32-битных регистров, даже с флагом -m16. GCC является fundamentally 32-битным компилятором и не поддерживает генерацию настоящего 16-битного кода, хотя может производить код, который выполняется в 16-битном режиме с некоторыми ограничениями.
Содержание
- Почему GCC не может генерировать чистый 16-битный код
- Что на самом деле делает флаг -m16
- Правильное использование директив сборки
- Альтернативные решения для чистого 16-битного кода
- Ограничения и требования к процессору
- Практические рекомендации
Почему GCC не может генерировать чистый 16-битный код
GCC является 32-битным компилятором по своей архитектуре и не поддерживает генерацию настоящего 16-битного кода. Как указывается в источниках, “gcc cannot actually generate real 16 bit code” и “GCC is 32 bit compiler, it can’t generate 16 bit assembly”. Даже при использовании флага -m16 компилятор все равно производит 32-битные инструкции, которые могут выполняться в 16-битном режиме, но не являются чисто 16-битными.
Основная проблема заключается в том, что GCC внутренне использует 32-битную архитектуру и генерирует код, который требует процессора уровня 386 или выше для выполнения в реальном режиме.
Что на самом деле делает флаг -m16
С флагом -m16 GCC генерирует код, который:
- Модифицирует инструкции для выполнения в 16-битном режиме
- Добавляет префиксы (66h и 67h) для переключения между 16-битным и 32-битным режимами
- Использует 32-битные регистры (EAX, ECX и т.д.) с префиксами
- Требует процессора 386 или выше, так как не может работать на чисто 16-битных процессорах (8086/8088/80286)
Как отмечается в источниках, “it generates the same code as in 32 bit mode but with some adjustments to run in 16 bit mode” и “it still insists on using the 32-bit registers, even for 2 byte wide variables”.
Правильное использование директив сборки
Для правильной генерации 16-битного кода вам нужно использовать директивы сборки GNU AS:
Директива .code16
.code16
Эта директива переключает ассемблер в режим генерации чисто 16-битного кода. Все инструкции будут использовать 16-битные операнды и регистры.
Директива .code16gcc
.code16gcc
Эта директива специально разработана для кода, сгенерированного GCC. Она отличается от .code16 тем, что инструкции call, ret, enter, leave, push, pop, pusha, popa, pushf, popf по умолчанию имеют 32-битный размер.
Для вашего случая с C-кодом нужно добавить в начале каждого C-файла:
__asm__(".code16gcc\n");
Или создать заголовочный файл code16gcc.h:
// code16gcc.h
#ifndef _CODE16GCC_H_
#define _CODE16GCC_H_
__asm__(".code16gcc\n");
#endif
И затем включать его в ваши C-файлы:
#include "code16gcc.h"
Альтернативные решения для чистого 16-битного кода
1. Использование Open Watcom
Open Watcom нативно поддерживает 16-битную компиляцию и может генерировать настоящий 16-битный код без использования 32-битных регистров.
2. Использование Turbo C или Borland C++
Эти старые компиляторы были специально разработаны для 16-битного кода и могут генерировать чистый 16-битный код.
3. Комбинированный подход
Смешивать C-код (с ограничениями) и чистый ассемблер:
# pure16.asm
.code16
pure_16bit_function:
; чисто 16-битный код
movw %ax, %bx
ret
// main.c
__asm__(".code16gcc\n");
extern void pure_16bit_function(void);
void c_function(void) {
// код с ограничениями
pure_16bit_function();
}
Ограничения и требования к процессору
Ограничения GCC
- Требуется процессор 386 или выше
- Не работает на 8086/8088/80286
- 32-битные регистры все равно используются с префиксами
- Ограниченная поддержка типов данных (long ints и другие)
Требования к коду
// НЕ будет работать с чистым 16-битным кодом
long large_value = 0xFFFFFFFF; // 32-битное значение
int* pointer = &some_variable; // 32-битные указатели
// БУДЕТ работать с ограниченным 16-битным кодом
unsigned short value = 0xFFFF; // 16-битное значение
Практические рекомендации
1. Обновите ваш Makefile
# Makefile
METAL=-m16 -ffreestanding -fno-stack-protector -fno-pic
ASFLAGS=-m16
# Добавьте директиву .code16gcc для C-файлов
%.o: %.c
i686-elf-gcc -c $< -o $@ $(METAL) -include code16gcc.h
%.o: %.asm
i686-elf-as $< -o $@ $(ASFLAGS)
2. Создайте правильный заголовочный файл
// code16gcc.h
#ifndef _CODE16GCC_H_
#define _CODE16GCC_H_
__asm__(".code16gcc\n");
#endif
3. Измените ваш C-код
// Main.c
#include "code16gcc.h"
// Используйте только 16-битные типы
unsigned short x = 0x1234;
unsigned char y = 0xAB;
// Избегайте 32-битных операций
void my_function(void) {
__asm__("pushw %ax"); // вместо push %eax
__asm__("popw %ax"); // вместо pop %eax
// и т.д.
}
4. Используйте правильные директивы в ассемблере
# Bootloader.asm
.code16gcc # вместо .code16 для совместимости с GCC
5. Альтернативный вариант - используйте ассемблер для критичного кода
Если вам нужен действительно чистый 16-битный код, напишите критичные части на чистом ассемблере:
# pure16.asm
.code16
pure_16bit_routine:
movw %ax, %bx
addw $1, %bx
retw
И вызывайте его из C-кода.
Заключение
-
GCC не может генерировать настоящий чистый 16-битный код - компилятор по своей архитектуре является 32-битным и не поддерживает генерацию чисто 16-битного кода.
-
Флаг -m16 создает псевдо-16-битный код - он использует 32-битные инструкции с префиксами и требует процессора 386+.
-
Для лучшей совместимости используйте .code16gcc - эта директива специально разработана для кода, сгенерированного GCC.
-
Рассмотрите альтернативные компиляторы - для настоящего чистого 16-битного кода используйте Open Watcom, Turbo C или Borland C++.
-
Смешивайте C и ассемблер - для критичных к производительности или совместимости участков кода используйте чистый ассемблер.
Реальная 16-битная совместимость с процессорами 8086/8088/80286 требует либо использования специализированных компиляторов, либо написания кода полностью на ассемблере.