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

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


Заголовочный файл стандартной библиотеки 

В прошлом выпуске мы наконец-то перешли с Ассемблера на язык высокого уровня Си и написали простейшее ядро. Поскольку мы пишем свою ОС, в нашем распоряжении лишь аппаратные возможности и конструкции языка Си, никакой стандартной библиотеки и функций - ведь они рассчитаны на работу под существующей системой (если вы компилируете программу в Linux, функции рассчитаны на работу с системными вызовами Linux, если под Windows, то функции уже другие и т. д.). Сегодня мы напишем базовые функции, являющиеся аналогами соответствующих функциям из libc, чтобы упростить разработку. Также наша стандартная библиотека будет содержать определения типов специфичные для низкоуровневого программирования. Для начала приведу заголовочный файл stdlib.h:

#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 long uint32;
typedef signed long int32;

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

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

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

void memset(void *mem, char value, size_t count);
void memset_word(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);

#endif 

В этом заголовочном файле описан булевный тип, типы заданной разрядности (uint8 - целое 8 бит без знака, int32 - целое 32 бита со знаком и т. д.), которых так не хватает в обычном Си, полезные макросы min и max (возвращают минимальный или максимальный свой аргумент), прототипы функций работы с памятью, прототипы строковых функций. Описанные функции теперь надо ещё реализовать в файле stdlib.c. Начнём с подключения заголовочного файла:

#include "stdlib.h" 

Важно указать имя файла в кавычках, а не знаках больше-меньше, как мы привыкли (#include <stdlib.h>), чтобы файл в первую очередь искался к текущем каталоге, а не в каталоге с библиотечными include'ами.

Ассемблерные вставки

GCC как и многие другие компиляторы позволяет включать в программу фрагменты ассемблерных инструкций. Это просто необходимая возможность для разработчиков ОС. Синтаксис у ассемблерных  вставок таков:

asm("код на языке Assembler":выходные параметры:входные
параметры); 

 Входные и выходные параметры - список переменных, значение которых следует поместить в регистры перед исполнением кода, а потом переместить обратно в переменные. Важно также отметить, что используется не Intel, а AT&T синтаксис:

 

  • Параметры большинства инструкций указываются в обратном порядке: вместо "mov приёмник, источник" надо писать "mov источник, приёмник".
  • В конце имени команды добавляется суффикс "b" (байт), "w" (слово), "l" (двойное слово), "q" (четверное слово, только для 64-битной системы) в зависимости от размера операндов.
  • Все константы имеют префикс "$", все регистры "%". Если были указаны входные-выходные параметры, то параметры выглядят как "%0", "%1" в зависимости от порядка их следования, а настоящие регистры претворяются двумя символами процента.

 

 Пример ассемблерной вставки в GCC:

asm("movl
%1, %%edx \n add $10, %%edx \n movl %%edx, %0":"=a"(dest):"b"(src));
asm("lgdt (,%0,)"::"a"(&gdtr));
asm("cli");

 Как можно заметить, для того чтобы указать несколько команд в одной конструкции asm используется вставка символа переноса строки "\n".

Не трудно догадаться, что имя аргумента "a" обозначает EAX, "b" - EBX, "c" - ECX, "d" - EDX, но лучше на это не надеяться, потому что всё зависит от особенностей компилятора и целевой архитектуры.

Функции работы с памятью

Теперь мы можем реализовать функции работы с памятью:

void memset(void *mem, char value, size_t count) {
asm("movl %0, %%eax \n movl %1, %%edi \n movl %2, %%ecx \n rep stosl"
::"a"((uint32)value | ((uint32)value << 8) | ((uint32)value << 16) | ((uint32)value << 24)),"b"(mem),"c"(count >> 2));
asm("movb %b0, %%al \n movl %1, %%ecx \n rep stosb"::"a"(value),"b"(count & 3));
} void memset_word(void *mem, uint16 value, size_t count) { asm("movl %0, %%eax \n movl %1, %%edi \n movl %2, %%ecx \n rep stosl" ::"a"((uint32)value | ((uint32)value << 16)),"b"(mem),"c"(count >> 1)); }

void memcpy(void *dest, void *src, size_t count) {
asm("movl %0, %%edi \n movl %1, %%esi \n movl %2, %%ecx \n rep movsl"::"a"(dest),"b"(src),"c"(count >> 2));
asm("movl %0, %%ecx \n rep movsb"::"a"(count & 3));
}

int memcmp(void *mem1, void *mem2, size_t count) {
char above, below;
asm("movl %0, %%esi \n movl %1, %%edi \n movl %2, %%ecx \n repe cmpsb"::"a"(mem1),"b"(mem2),"c"(count));
asm("seta %0 \n setb %1":"=a"(above),"=b"(below));
return above - below;
}

void *memchr(void *mem, char value, size_t count) {
void *result;
asm("movb %b0, %%al \n movl %1, %%edi \n movl %2, %%ecx \n repe cmpsb"::"a"(value),"b"(mem),"c"(count));
asm("movl %%edi, %0":"=a"(result));
if (result < mem + count) {
return result;
} else {
return NULL;
}
}

Эти функции, написанные с использованием Assembler, послужат основой для строковых функций:

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));
}

Теперь можно переписать main.c с учётом появления новых функций:

#include "stdlib.h"

typedef struct {
	uint64 base;
	uint64 size;
} BootModuleInfo;

void kernel_main(uint8 boot_disk_id, void *memory_map, BootModuleInfo *boot_module_list) {
	char *screen_buffer = (void*)0xB8000;
	memset_word(screen_buffer, 0x0E00, 2000);
	char msg[] = {'H',0x0E,'e',0x0E,'l',0x0E,'l',0x0E,'o',0x0E,' ',0x0E,'w',0x0E,'o',0x0E,'r',0x0E,'l',0x0E,'d',0x0E,'!',0x0E};
	memcpy(screen_buffer, msg, sizeof(msg));
} 

Данный код сначала очищает экран, а потом выводит надпись "Hello world!" жёлтым цветом на чёрном фоне в левом верхнем углу.

Заключение

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

stdlib.c:

#include "stdlib.h"

void memset(void *mem, char value, size_t count) {
	asm("movl %0, %%eax \n movl %1, %%edi \n movl %2, %%ecx \n rep stosl"
		::"a"((uint32)value | ((uint32)value << 8) | ((uint32)value << 16) | ((uint32)value << 24)),"b"(mem),"c"(count >> 2));
	asm("movb %b0, %%al \n movl %1, %%ecx \n rep stosb"::"a"(value),"b"(count & 3));
}

void memset_word(void *mem, uint16 value, size_t count) {
	asm("movl %0, %%eax \n movl %1, %%edi \n movl %2, %%ecx \n rep stosl"
		::"a"((uint32)value | ((uint32)value << 16)),"b"(mem),"c"(count >> 1));
}

void memcpy(void *dest, void *src, size_t count) {
	asm("movl %0, %%edi \n movl %1, %%esi \n movl %2, %%ecx \n rep movsl"::"a"(dest),"b"(src),"c"(count >> 2));
	asm("movl %0, %%ecx \n rep movsb"::"a"(count & 3));
}

int memcmp(void *mem1, void *mem2, size_t count) {
	char above, below;
	asm("movl %0, %%esi \n movl %1, %%edi \n movl %2, %%ecx \n repe cmpsb"::"a"(mem1),"b"(mem2),"c"(count));
	asm("seta %0 \n setb %1":"=a"(above),"=b"(below));
	return above - below;
}

void *memchr(void *mem, char value, size_t count) {
	void *result;
	asm("movb %b0, %%al \n movl %1, %%edi \n movl %2, %%ecx \n repe cmpsb"::"a"(value),"b"(mem),"c"(count));
	asm("movl %%edi, %0":"=a"(result));
	if (result < mem + count) {
		return result;
	} else {
		return NULL;
	}
}

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));
} 

Makefile: 

all: startup.o stdlib.o main.o script.ld
	ld --oformat=binary -melf_i386 -T script.ld -o kernel.bin startup.o stdlib.o main.o
startup.o: startup.i386.asm
	fasm startup.i386.asm startup.o
stdlib.o: stdlib.c stdlib.h
	gcc -c -m32 -ffreestanding -o stdlib.o stdlib.c
main.o: main.c stdlib.h
	gcc -c -m32 -ffreestanding -o main.o main.c
clean:
	rm -v *.o kernel.bin 

До встречи!


В избранное