Добрый день, уважаемые читатели. Я думаю настал тот момент, когда пора бы уже и перейти на уровень разработки повыше. Лучший помощник в этом язык программирования Си. Этот язык появился в 1972 году.

Кстати, есть одна несправедливость, автор языка Си, Деннис Ритчи, умер на третий день, после смерти Стива Джобса. О смерти основателя Apple не написал только ленивый. Легендарный разработчик же ушел тихо, будто никому не нужный. Так вот, это не так. Мы помним о тебе, Деннис! Надеюсь читатели поймут меня и не осудят за этот эмоциональный абзац.

Деннис Ритчи — создатель языка программирования Си

Язык программирования Си - Деннис Ритчи

Так вот, Деннис Ритчи, работая в Bell Labs, разработал язык Си. Дизайн языка предполагал, что использоваться он будет в тех местах, где раньше использовался ассемблер. Более того, данный язык программирования разрабатывался специально для создания операционной системы Unix.

Unix - язык программирования Си

Исходник на Си

Что-же, начнем. Нужно создать новый файл с наименованием — kernel.с. Исходники на языке Си используют расширение файла — .c. Реализуем функцию вывода наименования и версии операционной системы. Язык программирования Си очень похож на ассемблер, поэтому мы, фактически, просто перепишем наш код, который я написал в статье: Ядро операционной системы — первые шаги, используя синтаксис Си.

/*
*  kernel.c
*/
void kmain(void)
{
	const char *str = "Simple OS, version 0.0.1";
        /* Помещаем в указатель vidptr адрес первого байта видеопамяти */
	char *vidptr = (char*)0xb8000; 	
	unsigned int i = 0;
	unsigned int j = 0;

	/* Очистка экрана */
	while(j < 80 * 25 * 2) {
		vidptr[j] = ' ';
		vidptr[j+1] = 0x07; 		
		j = j + 2;
	}

        /* Не забываем обнулять переменные, мы в ядре */
	j = 0;

	/* Вывод наименования ОС и версии ядра */
	while(str[j] != '\0') {
		vidptr[i] = str[j];
		vidptr[i+1] = 0x07;
        /* Указатель на строку увеличиваем на 1, а указатель видеопамяти
         на 2 байта (символ + атрибут цвета) */
		++j;
		i = i + 2;
	}
	return;
}

Очень просто, не так ли. Комментарии в коде ясно показывают где и что происходит.

Доработка загрузчика

Для внедрения данного кода в загрузчик ядра Simple OS, нужно немного поправить ассемблерный исходник. Как Вы помните там есть список инструкций для вывода текста. Удалим их! Должен остаться такой код:

section .text
bits 32	
global start

start:
    cli 			
    mov esp, stack_space
    ; Удаляем весь код, связанный с выводом строки и очисткой экрана
    ; Удаляем метку done
    hlt
    
; Удаляем строку OS_DETAILS	 	
    
section .bss

Настала пора подняться на уровень выше! (барабанная дробь) Добавляем в код вызов процедуры из исходника Си. Функция называется kmain. Покажем загрузчику кто есть кто, перед меткой start, что она находится в другом файле:

extern kmain

А теперь вызов после установки адреса стека:

call kmain

В целом код получается такой:

section .multiboot_header
header_start:
    dd 0xE85250D6                
    dd 0                         
    dd header_end - header_start 

    dd 0x100000000 - (0xE85250D6 + 0 + (header_end - header_start))
    
    dw 0    
    dw 0   
    dd 8   
header_end:
section .text
bits 32	
global start
extern kmain ;kmain определена в файле Си

start:
  cli 			
  mov esp, stack_space	
  call kmain ;Вызов функции
  hlt	

section .bss
resb 8192
stack_space:

Сборка с помощью GNU LD

Теперь у нас есть два файла исходников: boot.asm и kernel.c. Чтобы собрать единое ядро используем компоновщик, или как его правильно называют: редактор связей, из пакета GNU LD. По ссылке можно посмотреть документацию на русском языке. Мой файл — link.ld — это файл с конфигурацией для компоновщика на языке управления линкером,очень простой:

OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
 {
   . = 0x100000;
   .boot : { *(.multiboot_header) }
   .text : { *(.text) }
   .data : { *(.data) }
   .bss  : { *(.bss)  }
 }

В нем указан формат выходного файла, точка входа и секции бинарника.

В терминале нужно будет написать следующую команду:

ld -m elf_i386 -T link.ld -o kernel.bin boot.o kernel.o

kernel.o и boot.o — это файлы объектного кода, компилированного из исходников: kernel.c и boot.asm. На выходе получается собранное ядро — kernel.bin.

После этого стандартно собираем iso-образ диска, запускаем в эмуляторе. И, вуаля:

вывод на экран - язык программирования Си

Автоматизация сборки

Добавлю знания, почерпнутые в теоретической обзорной статье: Автоматизация — используем make.

BUILD_DIR=./build
CC=/usr/bin/gcc
CC_FLAGS=-m32 -c
ASM=/usr/bin/nasm
ASM_FLAGS=-f elf32
LD=/usr/bin/ld
MKDIR=mkdir -p
RM=rm -f -r

project_structure:
	${MKDIR} ${BUILD_DIR}
boot: project_structure boot.asm
	${ASM} ${ASM_FLAGS} boot.asm -o ${BUILD_DIR}/boot.o
kernel: project_structure kernel.c
	${CC} ${CC_FLAGS} kernel.c -o ${BUILD_DIR}/kernel.o
link: boot kernel link.ld
	${LD} -m elf_i386 -T link.ld -o ${BUILD_DIR}/kernel.bin ${BUILD_DIR}/boot.o ${BUILD_DIR}/kernel.o
iso: link
	${MKDIR} ${BUILD_DIR}/isofiles/boot/grub
	cp grub.cfg ${BUILD_DIR}/isofiles/boot/grub
	cp ${BUILD_DIR}/kernel.bin ${BUILD_DIR}/isofiles/boot
	grub-mkrescue -o ${BUILD_DIR}/simpleos.iso ${BUILD_DIR}/isofiles
qemu: iso
	qemu-system-i386 build/simpleos.iso
qemu_cd: iso
	qemu-system-i386 -cdrom build/simpleos.iso
clean:
	${RM} ${BUILD_DIR}/*

Пробегусь быстренько по целям:

  • project_structure — подготовка структуры проекта
  • boot — компиляция загрузчика ядра ОС
  • kernel — компиляция ядра операционной системы, написанного на языке программирования Си
  • link — компоновка ядра
  • iso — сборка образа диска
  • qemu — запуск подготовленного образа в эмуляторе в качестве жесткого диска
  • qemu_cd — запуск образа в качестве cd-диска
  • clean — очистка директории сборки

Ну, вот. Немного облегчил себе жизнь. Теперь для того, чтобы пройти весь процесс нужно, всего лишь, в терминале написать команду:

make qemu_cd

Надеюсь Вам понравилась статья. А, пока, до скорых встреч!

Канал на Яндекс.Дзен

Картинки найдены на просторах интернета.