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

Пишем свою операционную систему. Улучшение стандартной библиотеки


Причина падения ОС при включении оптимизации была найдена - memset (как и memcpy) меняет значение регистра EDI, который считается неизменным по соглашению вызова C (вызываемая функция обязана сохранить его значение).

Я принял достаточно радикальное решение - часть функций стандартной библиотеки будет вынесена в ассемблерный файл для лучшей оптимизации (строковые операции компилятор делает неэффективно) и простоты написания. Теперь в нашем проекте появляется файл stdlib.i386.asm:

format ELF

public memset
public memsetw
public memcpy
public memcmp
public memchr

section ".text" executable

; void memset(void *mem, char value, size_t count);
memset:
	push edi
	mov edi, [esp + 8]
	mov edx, [esp + 12]
	mov al, dl
	shl eax, 8
	mov al, dl
	shl eax, 8
	mov al, dl
	shl eax, 8
	mov al, dl
	mov edx, [esp + 16]
	mov ecx, edx
	shr ecx, 2
	rep stosd
	mov ecx, edx
	and ecx, 3
	rep stosb
	pop edi
	ret

; void memsetw(void *mem, uint16 value, size_t count);
memsetw:
	push edi
	mov edi, [esp + 8]
	mov edx, [esp + 12]
	mov ax, dx
	shl eax, 16
	mov ax, dx
	mov edx, [esp + 16]
	mov ecx, edx
	shr ecx, 1
	rep stosd
	mov ecx, edx
	and ecx, 1
	rep stosw
	pop edi
	ret

; void memcpy(void *dest, void *src, size_t count);
memcpy:
	push esi edi
	mov edi, [esp + 12]
	mov esi, [esp + 16]
	mov edx, [esp + 20]
	cmp edi, esi
	jb .reversed
	mov ecx, edx
	shr ecx, 2
	rep movsd
	mov ecx, edx
	and ecx, 3
	rep movsb
 .exit:
	pop edi esi
	ret
 .reversed:
	add esi, edx
	add edi, edx
	dec esi
	dec edi
	std
	mov ecx, edx
	and ecx, 3
	rep movsb
	mov ecx, edx
	shr ecx, 2
	rep movsd
	cld
	jmp .exit

; int memcmp(void *mem1, void *mem2, size_t count);
memcmp:
	push esi edi
	mov edi, [esp + 12]
	mov esi, [esp + 16]
	mov ecx, [esp + 20]
	repe cmpsb
	seta al
	setb dl
	sub al, dl
	movzx eax, al
	pop edi esi
	ret

; void *memchr(void *mem, char value, size_t count);
memchr:
	push edi
	mov edi, [esp + 8]
	mov eax, [esp + 12]
	mov ecx, [esp + 16]
	repne scasb
	jne .not_found
	dec edi
	mov eax, edi
 .exit:
	pop edi
	ret
 .not_found:
	xor eax, eax
	jmp .exit 

Заголовочный файл stdlib.h остаётся неизменным, а вот stdlib.c укорачивается:

#include "stdlib.h"

size_t strlen(char *str) {
	return (char*)memchr(str, '\0', -1) - str;
}

void strcpy(char *dest, char *src) {
	memcpy(dest, src, strlen(src) + 1);
}

void strncpy(char *dest, char *src, size_t max_count) {
	size_t len = min(max_count - 1, strlen(src));
	memcpy(dest, src, len);
	dest[len] = '\0';
}

int strcmp(char *str1, char *str2) {
	return memcmp(str1, str2, strlen(str1) + 1);
}

char *strchr(char *str, char value) {
	return memchr(str, value, strlen(str));
}

bool mutex_get(Mutex *mutex, bool wait) {
	bool old_value = true;
	do {
		asm("xchg (,%1,), %0":"=a"(old_value):"b"(mutex),"a"(old_value));
	} while (old_value && wait);
	return !old_value;
}

void mutex_release(Mutex *mutex) {
	*mutex = false;
} 

К тому же я избавился от ряда не очень "красивых" ассемблерных вставок оставив лишь полностью корректные (в том числе и в других файлах - memory_manager.c, interrupts.c).

Теперь наша ОС работает полностью корректно и не падает при любом уровне оптимизации (O0, O1, O2, O3, Os).

Ещё немного о многозадачности

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

В обработчике прерывания от таймера я собираюсь сохранить в стек все регистры процессора, а также адрес temp_page, затем записать текущий указатель стека в структуру описателя нити. После этого производится поиск следующей нити для выполнения (в том числе это может оказаться и та же самая). Когда она найдена, из её описателя восстанавливается её указатель стека, а затем все регистры из стека. В итоге весь контекст задачи хранится в её стеке и не нужны дополнительные структуры данных. Для создания новой нити нужно заполнить её стек так, как будто только что случилось прерывание таймера (запихнуть туда адрес возврата, регистр флагов, регистры общего назначения и т. д.). Именно поэтому нам нужна полная предсказуемость содержимого стека. Возможно, обработчик прерывания таймера придётся писать на Assembler.

Двунаправленные связанные списки

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

Чтобы работать со списками было удобно, реализую три функции стандартной библиотеки специально для них:

#ifndef STDLIB_H
#define STDLIB_H

typedef enum {
	false = 0,
	true = 1
} bool;

#define NULL ((void*)0)

typedef unsigned char uint8;
typedef signed char int8;

typedef unsigned short uint16;
typedef signed short int16;

typedef unsigned int uint32;
typedef signed int int32;

typedef unsigned long long uint64;
typedef signed long long int64;

#ifdef __x86_64__
	typedef uint64 size_t;
#else
	typedef uint32 size_t;
#endif

typedef bool Mutex;

typedef struct _ListItem ListItem;

typedef struct {
	ListItem *first;
	size_t count;
	Mutex mutex;
} List;

struct _ListItem {
	ListItem *next;
	ListItem *prev;
	List *list;
};

#define min(a, b) (((a) > (b)) ? (b) : (a))
#define max(a, b) (((a) > (b)) ? (a) : (b))

#define outportb(port, value) asm("outb %b0, %w1"::"a"(value),"d"(port));
#define outportw(port, value) asm("outw %w0, %w1"::"a"(value),"d"(port));
#define outportl(port, value) asm("outl %0, %w1"::"a"(value),"d"(port));

#define inportb(port, out_value) asm("inb %w1, %b0":"=a"(out_value):"d"(port));
#define inportw(port, out_value) asm("inw %w1, %w0":"=a"(out_value):"d"(port));
#define inportl(port, out_value) asm("inl %w1, %0":"=a"(out_value):"d"(port));

void memset(void *mem, char value, size_t count);
void memsetw(void *mem, uint16 value, size_t count);
void memcpy(void *dest, void *src, size_t count);
int memcmp(void *mem1, void *mem2, size_t count);
void *memchr(void *mem, char value, size_t count);

size_t strlen(char *str);
void strcpy(char *dest, char *src);
void strncpy(char *dest, char*src, size_t max_count);
int strcmp(char *str1, char *str2);
char *strchr(char *str, char value);

bool mutex_get(Mutex *mutex, bool wait);
void mutex_release(Mutex *mutex);

void list_init(List *list);
void list_append(List *list, ListItem *item);
void list_remove(ListItem *item);

#endif 

А вот и сам код функций. Он не использует какие-либо особенности процессора и может быть реализован даже в обычных пользовательских приложениях.

void list_init(List *list) {
	list->first = NULL;
	list->count = 0;
	list->mutex = false;
}

void list_append(List *list, ListItem *item) {
	if (item->list == NULL) {
		mutex_get(&(list->mutex), true);
		if (list->first) {
			item->list = list;
			item->next = list->first;
			item->prev = list->first->prev;
			item->prev->next = item;
			item->next->prev = item;
		} else {
			item->next = item;
			item->prev = item;
			list->first = item;
		}
		list->count++;
		mutex_release(&(list->mutex));
	}
}

void list_remove(ListItem *item) {
	mutex_get(&(list->mutex), true);
	if (item->list->first == item) {
		item->list->first = item->next;
		if (item->list->first == item) {
			item->list->first = NULL;
		}
	}
	item->next->prev = item->prev;
	item->prev->next = item->next;
	list->count--;
	mutex_release(&(list->mutex));
} 

Внимание! Для перебора списка захват семафора не требуется!

Как это работает? Мы создаём новую структуру данных (например, описатель процесса или нити), самым первым полем структуры должно являться поле с любым именем и типом ListItem. После этого мы можем передавать указатель на новую структуру функциям list_add и list_remove в качестве параметра item, совершая явное приведение типов.

Новый Makefile

Мы добавили новый файл - stdlib.i386.asm. Следует описать правило для его сборки:

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

CFLAGS = -m32 -ffreestanding -O3

all: script.ld startup.o stdlib_asm.o stdlib.o main.o memory_manager.o interrupts.o tty.o
	ld $(LDFLAGS) -T script.ld -o kernel.bin startup.o stdlib_asm.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
stdlib_asm.o: stdlib.i386.asm
	fasm stdlib.i386.asm stdlib_asm.o
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

Ну вот и всё. У нас есть практически полноценный менеджер памяти и стандартная библиотека с поддержкой многопоточности, то есть всё, чтобы приступить к реализации многозадачности. Кстати, в качестве "домашнего задания" можете добавить потокобезопасность в драйвер экрана (tty.c).

Мой e-mail для всех вопросов: kiv.apple@gmail.com.

До встречи! 


В избранное