Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Пишем свою операционную систему Защита памяти и работа над ошибками


В этом выпуске мы проставим правильные атрибуты для памяти ядра, а также исправим некоторые ошибки в коде.

Открытие адресной линии A20

Работа с виртуальной машиной Bochs немного отличается от работы с реальным железом. Например, тем, что адресная линия A20 после загрузки в Bochs по умолчанию открыта. Это приводит к тому, что разработчику ОС не требуется открывать её вручную, потому что всё и так работает, однако ошибка проявится на реальном железе. Адресная линия A20 позволяет процессору обращаться к памяти выше первого мегабайта и появилась на процессорах, которые поддерживают защищённый режим, а для обратной совместимости изначально закрыта. Открыть A20 не сложно: достаточно вставить следующий код перед переходом в защищённый режим в нашем файле boot.asm:

	; Загрузим значение в CR3
	mov eax, 0x1000
	mov cr3, eax
	; Откроем адресную линию A20
	in al, 0x92
	or al, 2
	out 0x92, al
	; Загрузим значение в GDTR
	lgdt [gdtr32]
	; Запретим прерывания
	cli
	; Перейдём в защищённый режим
	mov eax, cr0
	or eax, 0x80000001
	mov cr0, eax 

Теперь наша ОС сможет корректно обращаться к памяти свыше первого мегабайта и на реальном железе, а не только в эмуляторе Bochs.

Защита от излишней оптимизации

Сейчас все оптимизации кода GCC отключены, поэтому мы используем некие допущения, из-за которых наш код выполняет то что нужно. Но для нормальной работы, а не разработки ядро стоит компилировать с различными оптимизациями, ведь это уменьшит его размер и увеличит скорость работы. Однако в случае низкоуровневого программирования компилятор часто не знает определённых тонкостей среды, где работает код. Например, мы можем записать значение на TEMP_PAGE, затем вызвать temp_map_page и переключить временную страницу на другую область физической памяти и прочитать значение. В случае разрешённых оптимизаций, компилятор просто сохранит записанное значение и сразу подставит его. Откуда ему знать, что переменные могут меняться без явного присваивания? Или компилятор может убрать инструкции, которые на его взгляд бессмысленны. Например, код, который присваивает переменной сначала одно значение, а потом сразу другое, может быть сокращён до последнего присваивания - результат тот же. Откуда компилятору знать, что мы работали с MMIO-регистром устройства? А ещё компилятор может переставлять команды местами, если они не влияют друг на друга (например, работа с независимыми переменными), но в случае работы с оборудованием совершенно обособленные переменные на самом деле могут оказаться тесно связанными.

Для того, чтобы избежать такого поведения оптимизатора, есть ключевое слово volatile. Его следует поставить перед объявлением переменной. Оно обозначает, что эта переменная может менять своё значение неявным образом и оптимизировать работу с ней нельзя. Злоупотреблять этим ключевым словом тоже не стоит, поэтому что оно сведёт на нет преимущества включения оптимизации.

В первую очередь добавим volatile в менеджер памяти - многие функции работают с TEMP_PAGE, отследить изменения которой компилятор не в силах:

#include
"stdlib.h"
#include "memory_manager.h"

typedef struct {
	uint64 base;
	uint64 length;
	uint32 type;
	uint32 acpi_ext_attrs;
} __attribute__((packed)) MemoryMapEntry;

typedef struct {
	phyaddr next;
	phyaddr prev;
	size_t size;
} PhysMemoryBlock;

size_t free_page_count = 0;
phyaddr free_phys_memory_pointer = -1;

void init_memory_manager(void *memory_map) {
	asm("movl %%cr3, %0":"=a"(kernel_page_dir));
	memory_size = 0x100000;
	MemoryMapEntry *entry;
	for (entry = memory_map; entry->type; entry++) {
		if ((entry->type == 1) && (entry->base >= 0x100000)) {
			free_phys_pages(entry->base, entry->length >> PAGE_OFFSET_BITS);
			memory_size += entry->length;
		}
	}
}

/* Physical memory manager */

size_t get_free_memory_size() {
	return free_page_count << PAGE_OFFSET_BITS;
}

phyaddr alloc_phys_pages(size_t count) {
	if (free_page_count < count) return -1;
	phyaddr result = -1;
	if (free_phys_memory_pointer != -1) {
		phyaddr cur_block = free_phys_memory_pointer;
		do {
			temp_map_page(cur_block);
			if (((volatile PhysMemoryBlock*)TEMP_PAGE)->size == count) {
				phyaddr next = ((volatile PhysMemoryBlock*)TEMP_PAGE)->next;
				phyaddr prev = ((volatile PhysMemoryBlock*)TEMP_PAGE)->prev;
				temp_map_page(next);
				((volatile PhysMemoryBlock*)TEMP_PAGE)->prev = prev;
				temp_map_page(prev);
				((volatile PhysMemoryBlock*)TEMP_PAGE)->next = next;
				if (cur_block == free_phys_memory_pointer) {
					free_phys_memory_pointer = next;
					if (cur_block == free_phys_memory_pointer) {
						free_phys_memory_pointer = -1;
					}
				}
				result = cur_block;
				break;
			} else if (((volatile PhysMemoryBlock*)TEMP_PAGE)->size > count) {
				((volatile PhysMemoryBlock*)TEMP_PAGE)->size -= count;
				result = cur_block + (((volatile PhysMemoryBlock*)TEMP_PAGE)->size << PAGE_OFFSET_BITS);
				break;
			}
			cur_block = ((volatile PhysMemoryBlock*)TEMP_PAGE)->next;
		} while (cur_block != free_phys_memory_pointer);
		if (result != -1) {
			free_page_count -= count;
		} 
	}
	return result;
}

void free_phys_pages(phyaddr base, size_t count) {
	if (free_phys_memory_pointer == -1) {
		temp_map_page(base);
		((volatile PhysMemoryBlock*)TEMP_PAGE)->next = base;
		((volatile PhysMemoryBlock*)TEMP_PAGE)->prev = base;
		((volatile PhysMemoryBlock*)TEMP_PAGE)->size = count;
		free_phys_memory_pointer = base;
	} else {
		phyaddr cur_block = free_phys_memory_pointer;
		do {
			temp_map_page(cur_block);
			if (cur_block + (((volatile PhysMemoryBlock*)TEMP_PAGE)->size << PAGE_OFFSET_BITS) == base) {
				((volatile PhysMemoryBlock*)TEMP_PAGE)->size += count;
				if (((volatile PhysMemoryBlock*)TEMP_PAGE)->next == base + (count << PAGE_OFFSET_BITS)) {
					phyaddr next1 = ((volatile PhysMemoryBlock*)TEMP_PAGE)->next;
					temp_map_page(next1);
					phyaddr next2 = ((volatile PhysMemoryBlock*)TEMP_PAGE)->next;
					size_t new_count = ((volatile PhysMemoryBlock*)TEMP_PAGE)->size;
					temp_map_page(next2);
					((volatile PhysMemoryBlock*)TEMP_PAGE)->prev = cur_block;
					temp_map_page(cur_block);
					((volatile PhysMemoryBlock*)TEMP_PAGE)->next = next2;
					((volatile PhysMemoryBlock*)TEMP_PAGE)->size += new_count;
				}
				break;
			} else if (base + (count << PAGE_OFFSET_BITS) == cur_block) {
				size_t old_count = ((volatile PhysMemoryBlock*)TEMP_PAGE)->size;
				phyaddr next = ((volatile PhysMemoryBlock*)TEMP_PAGE)->next;
				phyaddr prev = ((volatile PhysMemoryBlock*)TEMP_PAGE)->prev;
				temp_map_page(next);
				((volatile PhysMemoryBlock*)TEMP_PAGE)->prev = base;
				temp_map_page(prev);
				((volatile PhysMemoryBlock*)TEMP_PAGE)->next = base;
				temp_map_page(base);
				((volatile PhysMemoryBlock*)TEMP_PAGE)->next = next;
				((volatile PhysMemoryBlock*)TEMP_PAGE)->prev = prev;
				((volatile PhysMemoryBlock*)TEMP_PAGE)->size = count + old_count;
				break;
			} else if ((cur_block > base) || (((volatile PhysMemoryBlock*)TEMP_PAGE)->next == free_phys_memory_pointer)) {
				phyaddr prev = ((volatile PhysMemoryBlock*)TEMP_PAGE)->next;
				((volatile PhysMemoryBlock*)TEMP_PAGE)->prev = base;
				temp_map_page(prev);
				((volatile PhysMemoryBlock*)TEMP_PAGE)->next = base;
				temp_map_page(base);
				((volatile PhysMemoryBlock*)TEMP_PAGE)->next = cur_block;
				((volatile PhysMemoryBlock*)TEMP_PAGE)->prev = prev;
				((volatile PhysMemoryBlock*)TEMP_PAGE)->size = count;
				break;
			}
			cur_block = ((volatile PhysMemoryBlock*)TEMP_PAGE)->next;
		} while (cur_block != free_phys_memory_pointer);
		if (base < free_phys_memory_pointer) {
			free_phys_memory_pointer = base;
		}
		
	}
	free_page_count += count;
}

/* Lowlevel virtual memory manager */

static inline void flush_page_cache(void *addr) {
	asm("invlpg (,%0,)"::"a"(addr));
}

void temp_map_page(phyaddr addr) {
	*((phyaddr*)TEMP_PAGE_INFO) = (addr & ~PAGE_OFFSET_MASK) | PAGE_PRESENT | PAGE_WRITABLE;
	flush_page_cache((void*)TEMP_PAGE);
}

bool map_pages(phyaddr page_dir, void *vaddr, phyaddr paddr, size_t count, unsigned int flags) {
	for (; count; count--) {
		phyaddr page_table = page_dir;
		char shift;
		for (shift = PHYADDR_BITS - PAGE_TABLE_INDEX_BITS; shift >= PAGE_OFFSET_BITS; shift -= PAGE_TABLE_INDEX_BITS) {
			unsigned int index = ((size_t)vaddr >> shift) & PAGE_TABLE_INDEX_MASK;
			temp_map_page(page_table);
			if (shift > PAGE_OFFSET_BITS) {
				page_table = ((volatile phyaddr*)TEMP_PAGE)[index];
				if (!(page_table & PAGE_PRESENT)) {
					phyaddr addr = alloc_phys_pages(1);
					if (addr != -1) {
						temp_map_page(paddr);
						memset((void*)TEMP_PAGE, 0, PAGE_SIZE);
						temp_map_page(page_table);
						((volatile phyaddr*)TEMP_PAGE)[index] = addr | PAGE_PRESENT | PAGE_WRITABLE | PAGE_USER;
						page_table = addr;
					} else {
						return false;
					}
				}
			} else {
				((volatile phyaddr*)TEMP_PAGE)[index] = (paddr & ~PAGE_OFFSET_BITS) | flags;
				flush_page_cache(vaddr);
			}
		}
		vaddr += PAGE_SIZE;
		paddr += PAGE_SIZE;
	}
	return true;
}

phyaddr get_page_info(phyaddr page_dir, void *vaddr) {
	phyaddr page_table = page_dir;
	char shift;
	for (shift = PHYADDR_BITS - PAGE_TABLE_INDEX_BITS; shift >= PAGE_OFFSET_BITS; shift -= PAGE_TABLE_INDEX_BITS) {
		unsigned int index = ((size_t)vaddr >> shift) & PAGE_TABLE_INDEX_MASK;
		temp_map_page(page_table);
		if (shift > PAGE_OFFSET_BITS) {
			page_table = ((volatile phyaddr*)TEMP_PAGE)[index];
			if (!(page_table & PAGE_PRESENT)) {
				return 0;
			}
		} else {
			return ((volatile phyaddr*)TEMP_PAGE)[index];
		}
	}
}

/* Highlevel virtual memory manager */

void *alloc_virt_pages(AddressSpace *address_space, void *vaddr, phyaddr paddr, size_t count, unsigned int flags) {
	
}

void free_virt_pages(AddressSpace *address_space, void *vaddr, size_t count, unsigned int flags) {
	
} 

Теперь необходимо внести ещё одно незначительное исправление в функцию init_interrupts:

        map_pages(kernel_page_dir, idt, alloc_phys_pages(1), 1, PAGE_PRESENT | PAGE_WRITABLE | PAGE_GLOBAL);
	memset(idt, 0, 256 * sizeof(IntDesc));
	volatile IDTR idtr = {256 * sizeof(IntDesc), idt};
	asm("lidt (,%0,)"::"a"(&idtr));
	irq_base = 0x20;
	irq_count = 16;

Без этого значение в IDTR будет загружаться некорректно.
Ну и наконец осталось добавить несколько volatile в tty.c:

const char digits[] = "0123456789ABCDEF";
char num_buffer[65];

#define KEY_BUFFER_SIZE 16
volatile char key_buffer[KEY_BUFFER_SIZE];
volatile unsigned int key_buffer_head = 0;
volatile unsigned int key_buffer_tail = 0;

void keyboard_int_handler();

Доступ к этим переменным осуществляется как в обычном коде, так и в обработчике прерывания, поэтому компилятор не может отследить изменение переменных правильно. Простейший цикл while (key_buffer_head == key_buffer_tail) превратиться в бесконечный, если в самом начале эти переменные были равны, потому что значения будут прочитаны лишь один раз.

Ещё одна ошибка проявившаяся при включении оптимизации - работа со стеком в ассемблерной вставке в функции set_int_handler. Мы запихивали в стек регистр флагов, а потом его восстанавливали, но теперь для извлечения параметров из стека используется сам ESP вместо его копии в EBP, поэтому временная дестабилизация стека приводила к неправильному доступу к аргументу функции, хотя возврат из неё был корректен (до инструкции RET стек был приведён к правильному виду). Пока единственное что приходит на ум - сохранять регистр файлов не в стеке, а в локальной переменной (на самом деле она тоже оказывается в стеке, но об этом заботится уже компилятор). Так и сделаем:

void set_int_handler(uint8 index, void *handler, uint8 type) {
	size_t saved_flags;
	asm("pushf \n popl %0 \n cli":"=a"(saved_flags));
	idt[index].selector = 8;
	idt[index].address_0_15 = (size_t)handler & 0xFFFF;
	idt[index].address_16_31 = (size_t)handler >> 16;
	idt[index].type = type;
	idt[index].reserved = 0;
	asm("pushl %0 \n popf"::"a"(saved_flags));
}

Ну вот и всё. Теперь наше ядро правильно работает с любым уровнем оптимизации. Чтобы включить оптимизацию следует в Makefile добавить к CFLAGS опцию -On, где n число от 0 до 3 (0 - отсутствие оптимизации, 3 - максимальная оптимизация). Обычно рекомендуется использовать 0-ой уровень для отладки и 2-ой для обычной работы. 3-ий уровень является режимом агрессивной оптимизации скорости, который может приводить к нестабильной работе приложения и не рекомендуется к применению без необходимости. Пример нового Makefile:

ifdef OS
	LDFLAGS = -mi386pe
else
	LDFLAGS = -melf_i386
endif

CFLAGS = -m32 -ffreestanding -O2

all: script.ld startup.o stdlib.o main.o memory_manager.o interrupts.o tty.o
	ld $(LDFLAGS) -T script.ld -o kernel.bin startup.o stdlib.o main.o memory_manager.o interrupts.o tty.o
	objcopy kernel.bin -O binary
startup.o: startup.i386.asm
	fasm startup.i386.asm startup.o
stdlib.o: stdlib.c stdlib.h
	gcc -c $(CFLAGS) -o stdlib.o stdlib.c
main.o: main.c stdlib.h interrupts.h tty.h
	gcc -c $(CFLAGS) -o main.o main.c
memory_manager.o: memory_manager.c memory_manager.h stdlib.h
	gcc -c $(CFLAGS) -o memory_manager.o memory_manager.c
interrupts.o: interrupts.c interrupts.h stdlib.h
	gcc -c $(CFLAGS) -o interrupts.o interrupts.c
tty.o: tty.c tty.h stdlib.h
	gcc -c $(CFLAGS) -o tty.o tty.c
clean:
	rm -v *.o kernel.bin

Защита памяти ядра

Теперь можно и правильно настроить атрибуты памяти ядра - запретить запись в область кода, поставить атрибут глобальности для всех страниц.

Для начала нужно как-то позволить реализовать определение базовых адресов секций кода и данных и их размеров. Всё это сделаем с помощью файла script.ld:

ENTRY(_start)

KERNEL_BASE = 0xFFC00000;

SECTIONS {
 	.text KERNEL_BASE : {
 		KERNEL_CODE_BASE = .;
		*(.text)
		*(.code)
		*(.rodata*)
	}
	.data ALIGN(0x1000) : {
		KERNEL_DATA_BASE = .;
		*(.data)
  	}
	.bss ALIGN(0x1000) : {
		KERNEL_BSS_BASE = .;
		*(.bss)
	}
	.empty ALIGN(0x1000) - 1 : {
		BYTE(0)
		KERNEL_END = .;
	}
}

Теперь из любого модуля будет доступен ряд глобальных символов. Для удобства доступа опишем их в memory_manager.h:

#ifndef MEMORY_MANAGER_H
#define MEMORY_MANAGER_H

#include "stdlib.h"

#define PAGE_SIZE 0x1000
#define PAGE_OFFSET_BITS 12
#define PAGE_OFFSET_MASK 0xFFF
#define PAGE_TABLE_INDEX_BITS 10
#define PAGE_TABLE_INDEX_MASK 0x3FF

#define PHYADDR_BITS 32

#define PAGE_PRESENT		(1 << 0)
#define PAGE_WRITABLE		(1 << 1)
#define PAGE_USER		(1 << 2)
#define PAGE_WRITE_THROUGH	(1 << 3)
#define PAGE_CACHE_DISABLED	(1 << 4)
#define PAGE_ACCESSED		(1 << 5)

#define PAGE_MODIFIED		(1 << 6)
#define PAGE_GLOBAL		(1 << 8)

void KERNEL_BASE();
void KERNEL_CODE_BASE();
void KERNEL_DATA_BASE();
void KERNEL_BSS_BASE();
void KERNEL_END();

#define KERNEL_PAGE_TABLE ((void*)0xFFFFE000)
#define TEMP_PAGE ((void*)0xFFFFF000)
#define TEMP_PAGE_INFO ((size_t)KERNEL_PAGE_TABLE + (((size_t)TEMP_PAGE >> PAGE_OFFSET_BITS) & PAGE_TABLE_INDEX_MASK) * sizeof(phyaddr))

#define USER_MEMORY_START ((void*)0)
#define USER_MEMORY_END ((void*)0x7FFFFFFF)
#define KERNEL_MEMORY_START ((void*)0x80000000)
#define KERNEL_MEMORY_END ((void*)(KERNEL_BASE - 1))

typedef size_t phyaddr;

typedef enum {
	VMB_RESERVED,
	VMB_MEMORY,
	VMB_IO_MEMORY
} VirtMemoryBlockType;

typedef struct {
	VirtMemoryBlockType type;
	void *base;
	size_t length;
} VirtMemoryBlock;

typedef struct {
	phyaddr page_dir;
	void *start;
	void *end;
	size_t block_count;
	VirtMemoryBlock *blocks;
} AddressSpace;

phyaddr kernel_page_dir;
size_t memory_size;
AddressSpace kernel_address_space;

void init_memory_manager(void *memory_map);

size_t get_free_memory_size();
phyaddr alloc_phys_pages(size_t count);
void free_phys_pages(phyaddr base, size_t count);

void temp_map_page(phyaddr addr);
bool map_pages(phyaddr page_dir, void *vaddr, phyaddr paddr, size_t count, unsigned int flags);
phyaddr get_page_info(phyaddr page_dir, void *vaddr);

void *alloc_virt_pages(AddressSpace *address_space, void *vaddr, phyaddr paddr, size_t count, unsigned int flags);
void free_virt_pages(AddressSpace *address_space, void *vaddr, size_t count, unsigned int flags);

#endif

Осталось только изменить атрибуты страниц с помощью map_pages:

void init_memory_manager(void *memory_map) {
	asm("movl %%cr3, %0":"=a"(kernel_page_dir));
	memory_size = 0x100000;
	MemoryMapEntry *entry;
	for (entry = memory_map; entry->type; entry++) {
		if ((entry->type == 1) && (entry->base >= 0x100000)) {
			free_phys_pages(entry->base, entry->length >> PAGE_OFFSET_BITS);
			memory_size += entry->length;
		}
	}
	map_pages(kernel_page_dir, KERNEL_CODE_BASE, get_page_info(kernel_page_dir, KERNEL_CODE_BASE),
		((size_t)KERNEL_DATA_BASE - (size_t)KERNEL_CODE_BASE) >> PAGE_OFFSET_BITS, PAGE_PRESENT | PAGE_GLOBAL);
	map_pages(kernel_page_dir, KERNEL_DATA_BASE, get_page_info(kernel_page_dir, KERNEL_DATA_BASE),
		((size_t)KERNEL_END - (size_t)KERNEL_DATA_BASE) >> PAGE_OFFSET_BITS, PAGE_PRESENT | PAGE_WRITABLE | PAGE_GLOBAL);
	map_pages(kernel_page_dir, KERNEL_PAGE_TABLE, get_page_info(kernel_page_dir, KERNEL_PAGE_TABLE), 1, PAGE_PRESENT | PAGE_WRITABLE | PAGE_GLOBAL);
}

Теперь страниц настроены более привычно, если бы мы писали обычное приложение - доступ к страницам с кодом на запись запрещён. Также страницы ядра теперь имеют флаг PAGE_GLOBAL, который предотвращает выгрузку этих элементов из кеша при переключении каталога страниц поскольку ядро общее для всех процессов.

Заключение

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


В избранное