Компания
50,82
рейтинг
18 марта 2013 в 18:10

Разработка → Как запустить программу без операционной системы


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

В интернете существует огромное количество описаний и туториалов о для того как написать собственную мини-ОС, даже существуют сотни готовых маленьких хобби-ОС. Один из наиболее достойных ресурсов по этой тематике, который хотелось бы особо выделить, это портал osdev.org. Для дополнения предыдущей статьи про PCI (и возможности писать последующие статьи о различных функциях, которые присутствуют в любой современной ОС), мы опишем пошаговые инструкции по созданию загрузочного диска с привычной программой на языке С. Мы старались писать максимально подробно, чтобы во всем можно было разобраться самостоятельно.

Итак, цель: затратив как можно меньше усилий, создать собственную загрузочную флешку, которая всего-навсего печатает на экране компьютера классический “Hello World”.

Если быть более точным, то нам нужно “попасть” в защищенный режим с отключенной страничной адресацией и прерываниями – самый простой режим работы процессора с привычным поведением для простой консольной программы. Самый разумный способ достичь такой цели – собрать ядро поддерживающее формат multiboot и загрузить его с помощью популярного загрузчика Grub. Альтернативой такого решения является написание собственного volume boot record (VBR), который бы загружал написанный собственный загрузчик (loader). Приличный загрузчик, как минимум, должен уметь работать с диском, с файловой системой, и разбирать elf образы. Это означает необходимость написания множества ассемблерного кода, и немало кода на С. Одним словом, проще использовать Grub, который уже умеет делать все необходимое.

Начнем с того, что для дальнейших действий необходим определенный набор компиляторов и утилит. Проще всего воспользоваться каким-нибудь Linux (например, Ubuntu), поскольку он уже будет содержать все что нужно для создания загрузочной флэшки. Если вы привыкли работать в Windows, то можно настроить виртуальную машину с Linux (при помощи Virtual Box или VMware Workstation).

Если вы используете Linux Ubuntu, то прежде всего необходимо установить несколько утилит:
1. Grub. Для этого воспользуемся командой:
sudo apt-get install grub


2. Qemu. Он нужен, чтобы все быстро протестировать и отладить, для этого аналогично команда:
sudo apt-get install qemu


Теперь наш план выглядит так:
1. создать программу на C, печатающую строку на экране.
2. собрать из нее образ (kernel.bin) в формате miniboot, чтобы он был доступен для загрузки с помощью GRUB.
3. создать файл образа загрузочного диска и отформатировать его.
4. установить на этот образ Grub.
5. скопировать на диск созданную программу (kernel.bin).
6. записать образ на физический носитель или запустить его в qemu.

а процесс загрузки системы:


Чтобы все получилось, необходимо будет создать несколько файлов и каталогов:
kernel.c Код программы, написанный на языке С. Программа печатает на экран сообщение.
makefile Makefile, скрипт, выполняющий всю сборку программы и создание загрузочного образа.
linker.ld Скрипт компановщика для ядра.
loader.s Код на ассемблере, который вызывается Grub’ом и передает управление функции main из программы на С.
include/ Папка с заголовочными файлами.
grub/ Папка с файлами Grub’а.
common/ Папка с функциями общего назначения. В том числе реализация printf.


Шаг 1. Создание кода целевой программы (ядра):



Создаем файл kernel.c, который будет содержать следующий код, печатающий сообщение на экране:

#include "printf.h"
#include "screen.h"
#include "types.h"

void main(void)
{   
    clear_screen();
    printf("\n>>> Hello World!\n");
}



Тут все привычно и просто. Добавление функций printf и clear_screen будет рассмотрено дальше. А пока надо дополнить этот код всем необходимым, чтобы он мог загружаться Grub’ом.
Для того что бы ядро было в формате multiboot, нужно что бы в первых 8-ми килобайтах образа ядра находилась следующая структура:

0x1BADB002 = MAGIC Сигнатура формата Multiboot
0x0 = FLAGS Флаги, которые содержат дополнительные требования к загрузке ядра и параметрам, передаваемым загрузчиком ядру (нашей программе). В данном случае все флаги сброшены.
0xE4524FFE= -(MAGIC + FLAGS) Контрольная сумма.


Если все указанные условия выполнены, то Grub через регистры %eax и %ebx передает указатель на структуру multiboot Information и значение 0x1BADB002 соответственно. Структура multiboot Information содержит различную информацию, в том числе список загруженных модулей и их расположение, что может понадобиться для дальнейшей загрузки системы.
Для того, чтобы файл с программой содержал необходимые сигнатуры создадим файл loader.s, со следующим содержимым:


    .text
    .global loader                   # making entry point visible to linker

    # setting up the Multiboot header - see GRUB docs for details
    .set FLAGS,    0x0               # this is the Multiboot 'flag' field
    .set MAGIC,    0x1BADB002        # 'magic number' lets bootloader find the header
    .set CHECKSUM, -(MAGIC + FLAGS)  # checksum required

    .align 4
    .long MAGIC
    .long FLAGS
    .long CHECKSUM

# reserve initial kernel stack space
    .set STACKSIZE, 0x4000           # that is, 16k.
    .lcomm stack, STACKSIZE          # reserve 16k stack
    .comm  mbd, 4                    # we will use this in kmain
    .comm  magic, 4                  # we will use this in kmain

loader:
    movl  $(stack + STACKSIZE), %esp # set up the stack
    movl  %eax, magic                # Multiboot magic number
    movl  %ebx, mbd                  # Multiboot data structure
    call  main                      # call C code
    cli
hang:
    hlt                              # halt machine should kernel return
    jmp   hang



Рассмотрим код подробнее. Этот код в почти не измененном виде взят с wiki.osdev.org/Bare_Bones. Так как для компиляции используется gcc, то используется синтаксис GAS. Рассмотрим подробнее, что делает этот код.
.text

Весь последующий код попадет в исполняемую секцию .text.
.global loader

Объявляем символ loader видимым для линковщика. Это требуется, так как линковщик будет использовать loader как точку входа.
    .set FLAGS,    0x0               # присвоить FLAGS = 0x0
    .set MAGIC,    0x1BADB002       	# присвоить MAGIC = 0x1BADB002       	
    .set CHECKSUM, -(MAGIC + FLAGS) 	# присвоить CHECKSUM = -(MAGIC + FLAGS)

    .align 4			# выровнять последующие данные по 4 байта
    .long MAGIC			# разместить по текущему адресу значение MAGIC
    .long FLAGS			# разместить по текущему адресу значение FLAGS
    .long CHECKSUM			# разместить по текущему адресу значение CHECKSUM

Этот код формирует сигнатуру формата Multiboot. Директива .set устанавливает значение символа в выражение справа от запятой. Директива .align 4 выравнивает последующее содержимое по 4 байта. Директива .long сохраняет значение в четырех последующих байтах.
 .set STACKSIZE, 0x4000   # присвоить STACKSIZE = 0x4000
    .lcomm stack, STACKSIZE  # зарезервировать STACKSIZE байт. stack ссылается на диапазон
    .comm  mbd, 4            # зарезервировать 4 байта под переменную mdb в области COMMON
    .comm  magic, 4          # зарезервировать 4 байта под переменную magic в области COMMON

В процессе загрузки grub не настраивает стек, и первое что должно сделать ядро это настроить стек, для этого мы резервируем 0x4000(16Кб) байт. Директива .lcomm резервирует в секции .bss количество байт, указанное после запятой. Имя stack будет видимо только в компилируемом файле. Директива .comm делает то же что и .lcomm, но имя символа будет объявлено глобально. Это значит что, написав в коде на Си следующую строчку, мы сможем его использовать.
extern int magic

И теперь последняя часть:
loader:
    movl  $(stack + STACKSIZE), %esp 	# инициализировать стек
    movl  %eax, magic                # записать %eax по адресу magic
    movl  %ebx, mbd                  # записать %ebx по адресу mbd
    call  main                       # вызвать функцию main
    cli				# отключить прерывания от оборудования
hang:
    hlt                     # остановить процессор пока не возникнет прерывание
    jmp   hang		# прыгнуть на метку hang


Первой инструкцией происходит сохранение значения верхушки стека в регистре %esp. Так как стек растет вниз, то в %esp записывается адрес конца диапазона отведенного под стек. Две последующие инструкции сохраняют в ранее зарезервированных диапазонах по 4 байта значения, которые Grub передает в регистрах %eax, %ebx. Затем происходит вызов функции main, которая уже написана на Си. В случае возврата из этой процедуры процессор зациклится.

Шаг 2. Подготовка дополнительного кода для программы (системная библиотека):



Поскольку вся программа пишется с нуля, то функцию printf нужно написать с нуля. Для этого нужно подготовить несколько файлов.
Создадим папку common и include:

mkdir common
mkdir include



Создадим файл common\printf.c, который будет содержать реализацию привычной функции printf. Этот файл целиком можно взять из проекта www.bitvisor.org. Путь до файла в исходниках bitvisor: core/printf.c. В скопированном из bitvisor файле printf.c, для использования в целевой программе нужно заменить строки:

#include "initfunc.h"
#include "printf.h"
#include "putchar.h"
#include "spinlock.h"

на строки:
#include "types.h"
#include "stdarg.h"
#include "screen.h"


Потом, удалить функцию printf_init_global и все ее упоминания в этом файле:

static void
printf_init_global (void)
{
    spinlock_init (&printf_lock);
}
INITFUNC ("global0", printf_init_global);


Затем удалить переменную printf_lock и все ее упоминания в этом файле:

static spinlock_t printf_lock;
…
spinlock_lock (&printf_lock);
…
spinlock_unlock (&printf_lock);


Функция printf использует функцию putchar, которую так же нужно написать. Для этого создадим файл common\screen.с, со следующим содержимым:

#include "types.h"

#define GREEN    0x2
#define MAX_COL  80		// Maximum number of columns 
#define MAX_ROW  25		// Maximum number of rows 
#define VRAM_SIZE (MAX_COL*MAX_ROW)	// Size of screen, in short's 
#define DEF_VRAM_BASE 0xb8000	// Default base for video memory

static unsigned char curr_col = 0;
static unsigned char curr_row = 0;

// Write character at current screen location
#define PUT(c) ( ((unsigned short *) (DEF_VRAM_BASE)) \
	[(curr_row * MAX_COL) + curr_col] = (GREEN << 8) | (c))

// Place a character on next screen position
static void cons_putc(int c)
{
    switch (c) 
    {
    case '\t':
        do 
        {
            cons_putc(' ');
        } while ((curr_col % 8) != 0);
        break;
    case '\r':
        curr_col = 0;
        break;
    case '\n':
        curr_row += 1;
        if (curr_row >= MAX_ROW) 
        {
            curr_row = 0;
        }
        break;
    case '\b':
        if (curr_col > 0) 
        {
            curr_col -= 1;
            PUT(' ');
        }
        break;
    default:
        PUT(c);
        curr_col += 1;
        if (curr_col >= MAX_COL) 
        {
            curr_col = 0;
            curr_row += 1;
            if (curr_row >= MAX_ROW) 
            {
                curr_row = 0;
            }
        }
    };
}

void putchar( int c )
{
    if (c == '\n') 
        cons_putc('\r');
    cons_putc(c);
}

void clear_screen( void )
{
    curr_col = 0;
    curr_row = 0;
    
    int i;
    for (i = 0; i < VRAM_SIZE; i++)
        cons_putc(' ');
    
    curr_col = 0;
    curr_row = 0;
}


Указанный код, содержит простую логику печати символов на экран в текстовом режиме. В этом режиме для записи символа используется два байта (один с кодом символа, другой с его атрибутами), записываемые прямо в видео память отображаемую сразу на экране и начинающуюся с адреса 0xB8000. Разрешение экрана при этом 80x25 символов. Непосредственно печать символа осуществляется при помощи макроса PUT.
Теперь не хватает всего несколько заголовочных файлов:
1. Файл include\screen.h. Объявляет функцию putchar, которая используется в функции printf. Содержимое файла:

#ifndef _SCREEN_H
#define _SCREEN_H

void clear_screen( void );
void putchar( int c );

#endif


2. Файл include\printf.h. Объявляет функцию printf, которая используется в main. Содержимое файла:
#ifndef _PRINTF_H
#define _PRINTF_H

int printf (const char *format, ...); 

#endif


3. Файл include\stdarg.h. Объявляет функции для перебора аргументов, количество которых заранее не известно. Файл целиком берется из проекта www.bitvisor.org. Путь до файла в коде проекта bitvisor: include\core\stdarg.h.
4. Файл include\types.h. Объявляет NULL и size_t. Содержимое файла:
#ifndef _TYPES_H
#define _TYPES_H

#define NULL 0
typedef unsigned int size_t;

#endif

Таким образом папки include и common содержат минимальный код системной библиотеки, которая необходима любой программе.

Шаг 3. Создание скрипта для компоновщика:



Создаем файл linker.ld, который будет использоваться компоновщиком для формирования файла целевой программы (kernel.bin). Файл должен содержать следующее:

ENTRY (loader)
LMA = 0x00100000;
SECTIONS
{
    . = LMA;
    .multiboot ALIGN (0x1000) :   {  loader.o( .text ) }
    .text      ALIGN (0x1000) :   {  *(.text)          }
    .rodata    ALIGN (0x1000) :   {  *(.rodata*)       }
    .data      ALIGN (0x1000) :   {  *(.data)          }
    .bss :                        {  *(COMMON) *(.bss) }
    /DISCARD/ :                   {  *(.comment)       }
}


Встроенная функция ENTRY() позволяет задать входную точку для нашего ядра. Именно по этому адресу передаст управление grub после загрузки ядра. Компоновщик при помощи этого скрипта создаст бинарный файл в формате ELF. ELF-файл состоит из набора сегментов и секций. Список сегментов содержится в Program header table, список секций в Section header table. Линковщик оперирует с секциями, загрузчик образа (в нашем случае это GRUB) с сегментами.


Как видно на рисунке, сегменты состоят из секций. Одним из полей, описывающих секцию, является виртуальный адрес, по которому секция должна находиться на момент выполнения. На самом деле, у сегмента есть 2 поля, описывающих его расположение: виртуальный адрес сегмента и физический адрес сегмента. Виртуальный адрес сегмента это виртуальный адрес первого байта сегмента в момент выполнения кода, физический адрес сегмента это физический адрес по которому должен быть загружен сегмент. Для прикладных программ эти адреса всегда совпадают. Grub загружает сегменты образа, по их физическому адресу. Так как Grub не настраивает страничную адресацию, то виртуальный адрес сегмента должен совпадать с его физическим адресом, поскольку в нашей программе виртуальная память так же не настраивается.

SECTIONS

Говорит о том, что далее описываются секции.
. = LMA;

Это выражение указывает линковщику, что все последующие секции находятся после адреса LMA.
 ALIGN (0x1000)

Директива выше, означает, что секция выровнена по 0x1000 байт.
.multiboot ALIGN (0x1000) : {  loader.o( .text )  }

Отдельная секция multiboot, которая включает в себя секцию .text из файла loader.o, сделана для того, что бы гарантировать попадание сигнатуры формата multiboot в первые 8кб образа ядра.
.bss :  { *(COMMON) *(.bss) }

*(COMMON) это область, в которой резервируется память инструкциями .comm и .lcomm. Мы располагаем ее в секции .bss.
/DISCARD/ : { *(.comment) }

Все секции, помеченные как DISCARD, удаляются из образа. В данном случае мы удаляем секцию .comment, которая содержит информацию о версии линковщика.

Теперь скомпилируем код в бинарный файл следующими командами:
as -o loader.o loader.s
gcc -Iinclude -Wall -fno-builtin -nostdinc -nostdlib -o kernel.o -c kernel.c
gcc -Iinclude -Wall -fno-builtin -nostdinc -nostdlib -o printf.o -c common/printf.c
gcc -Iinclude -Wall -fno-builtin -nostdinc -nostdlib -o screen.o -c common/screen.c
ld -T linker.ld -o kernel.bin kernel.o screen.o printf.o loader.o

С помощью objdump’а рассмотрим, как выглядит образ ядра после линковки:

objdump -ph ./kernel.bin




Как можно видеть, секции в образе совпадают с теми, что мы описали в скрипте линковщика. Линковщик сформировал 3 сегмента из описанных секций. Первый сегмент включает в себя секции .multiboot, .text, .rodata и имеет виртуальный и физический адрес 0x00100000. Второй сегмент содержит секции .data и .bss и располагается по адресу 0x00104000. Значит все готово для загрузки этого файла при помощи Grub.

Шаг 4. Подготовка загрузчика Grub:
Создать папку grub:
mkdir grub


Скопировать в эту папку несколько файлов Grub, которые необходимы для его установки на образ (указанные далее файлы существуют, если в системе установлен Grub). Для этого нужно выполнить следующие команды:

cp /usr/lib/grub/i386-pc/stage1 ./grub/
cp /usr/lib/grub/i386-pc/stage2 ./grub/
cp /usr/lib/grub/i386-pc/fat_stage1_5 ./grub/


Создать файл grub/menu.lst, со следующим содержимым:
timeout   3
default   0

title  mini_os
root   (hd0,0)
kernel /kernel.bin



Шаг 5. Автоматизация и создание загрузочного образа:


Для автоматизации процесса сборки будем использовать утилиту make. Для этого создадим файл makefile, который будет собирать компилировать исходный код, собирать ядро и создавать загрузочный образ. Makefile должен иметь следующее содержимое:

CC      = gcc
CFLAGS  = -Wall -fno-builtin -nostdinc -nostdlib
LD      = ld

OBJFILES = \
	loader.o  \
	common/printf.o  \
	common/screen.o  \
	kernel.o

image:
	@echo "Creating hdd.img..."
	@dd if=/dev/zero of=./hdd.img bs=512 count=16065 1>/dev/null 2>&1

	@echo "Creating bootable first FAT32 partition..."
	@losetup /dev/loop1 ./hdd.img
	@(echo c; echo u; echo n; echo p; echo 1; echo ;  echo ; echo a; echo 1; echo t; echo c; echo w;) | fdisk /dev/loop1 1>/dev/null 2>&1 || true

	@echo "Mounting partition to /dev/loop2..."
	@losetup /dev/loop2 ./hdd.img \
    --offset    `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$3}'\`*512 | bc` \
    --sizelimit `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$4}'\`*512 | bc`
	@losetup -d /dev/loop1

	@echo "Format partition..."
	@mkdosfs /dev/loop2

	@echo "Copy kernel and grub files on partition..."
	@mkdir -p tempdir
	@mount /dev/loop2 tempdir
	@mkdir tempdir/boot
	@cp -r grub tempdir/boot/
	@cp kernel.bin tempdir/
	@sleep 1
	@umount /dev/loop2
	@rm -r tempdir
	@losetup -d /dev/loop2

	@echo "Installing GRUB..."
	@echo "device (hd0) hdd.img \n \
	       root (hd0,0)         \n \
	       setup (hd0)          \n \
	       quit\n" | grub --batch 1>/dev/null
	@echo "Done!"

all: kernel.bin
rebuild: clean all
.s.o:
	as -o $@ $<
.c.o:
	$(CC) -Iinclude $(CFLAGS) -o $@ -c $<
kernel.bin: $(OBJFILES)
	$(LD) -T linker.ld -o $@ $^
clean:
	rm -f $(OBJFILES) hdd.img kernel.bin


В файле объявлены две основные цели: all – компилирует ядро, и image – которая создает загрузочный диск. Цель all подобно привычным makefile содержит подцели .s.o и .c.o, которые компилируют *.s и *.c файлы в объектные файлы (*.o), а так же цель для формирования kernel.bin, которая вызывает компоновщик с созданным ранее скриптом. Эти цели выполняют ровно те же команды, которые указаны в шаге 3.
Наибольший интерес здесь представляет создание загрузочного образа hdd.img (цель image). Рассмотрим поэтапно, как это происходит.
dd if=/dev/zero of=./hdd.img bs=512 count=16065 1>/dev/null 2>&1

Эта команда создает образ, с которым будет происходить дальнейшая работа. Количество секторов выбрано не случайно: 16065 = 255 * 63. По умолчанию fdsik работает с диском так, как будто он имеет CHS геометрию, в которой Headers (H) = 255, Sectors (S) = 63, а Cylinders( С ) зависит от размера диска. Таким образом, минимальный размер диска, с которым может работать утилита fdsik, без изменения геометрии по умолчанию, равен 512 * 255 * 63 * 1 = 8225280 байт, где 512 – размер сектора, а 1 – количество цилиндров.
Далее создается таблица разделов:
losetup /dev/loop1 ./hdd.img
(echo c; echo u; echo n; echo p; echo 1; echo ;  echo ; echo a; echo 1; echo t; echo c; echo w;) | fdisk /dev/loop1 1>/dev/null 2>&1 || true

Первая команда монтирует файл hdd.img к блочному устройству /dev/loop1, позволяя работать с файлом как с устройством. Вторая команда создает на устройстве /dev/loop1 таблицу разделов, в которой находится 1 первичный загрузочный раздел диска, занимающий весь диск, с меткой файловой системы FAT32.
Затем форматируем созданный раздел. Для этого нужно примонтировать его как блочное устройство и выполнить форматирование.
losetup /dev/loop2 ./hdd.img \
    --offset    `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$3}'\`*512 | bc` \
    --sizelimit `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$4}'\`*512 | bc`
losetup -d /dev/loop1

Первая команда монтирует ранее созданный раздел к устройству /dev/loop2. Опция –offset указывает адрес начала раздела, а –sizelimit адрес конца раздела. Оба параметра получаются с помощью команды fdisk.
mkdosfs /dev/loop2

Утилита mkdosfs форматирует раздел в файловую систему FAT32.
Для непосредственной сборки ядра используются рассмотренные ранее команды в классическом синтаксисе makefile.
Теперь рассмотрим как установить GRUB на раздел:
mkdir -p tempdir		# создает временную директорию
mount /dev/loop2 tempdir	# монтирует раздел в директорию
mkdir tempdir/boot		# создает директорию /boot на разделе
cp -r grub tempdir/boot/	# копируем папку grub в /boot
cp kernel.bin tempdir/	# копирует ядро в корень раздела
sleep 1   		# ждем Ubuntu
umount /dev/loop2		# отмонтируем временную папку
rm -r tempdir		# удаляем временную папку
losetup -d /dev/loop2	# отмонтируем раздел

После выполнения вышеприведенных команд, образ будет готов к установке GRUB’а. Следующая команда устанавливает GRUB в MBR образа диска hdd.img.
echo "device (hd0) hdd.img \n \
      root (hd0,0)         \n \
      setup (hd0)          \n \
      quit\n" | grub --batch 1>/dev/null



Все готово к тестированию!

Шаг 6. Запуск:



Для компиляции, воспользуемся командой:
make all

После которой должен появиться файл kernel.bin.
Для создания загрузочного образа диска, воспользуемся командой:
sudo make image

В результате чего должен появиться файл hdd.img.
Теперь с образа диска hdd.img можно загрузиться. Проверить это можно с помощью следующей команды:
qemu -hda hdd.img -m 32

или:
qemu-system-i386 -hda hdd.img




Для проверки на реальной машине нужно сделать dd этого образа на флэшку и загрузиться с нее. Например такой командой:
sudo dd if=./hdd.img of=/dev/sdb


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

Ссылки на следующие статьи цикла:
"Как запустить программу без операционной системы: часть 2"
"Как запустить программу без операционной системы: часть 3: Графика"
"Как запустить программу без операционной системы: часть 4. Параллельные вычисления"
"Как запустить программу без операционной системы: часть 5. Обращение к BIOS из ОС"
"Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT"
Автор: @NWOcs
НеоБИТ
рейтинг 50,82

Комментарии (68)

  • +9
    Вот если бы вы еще объяснили, как запустить программу без Grub :)
    Ведь grub, фактически, является пускалкой нашего хелло-ворлда.
    • +17
      Без Груб запустить нетрудно, но первое, что надо уяснить — так делать не нужно. Не нужно бутлодеры переписывать, иначе так на них и остановитесь.
      • +4
        То, что свой бутлоадер писать смысла нет — и так понятно. Но, в общем случае, интересно, как пускается сам бутлоадер.

        Сейчас ковыряю свою lpcxpresso по мотивам этих статей. Жуть, как интересно!
        • +1
          Это на кортексе М3 что-ли? Ну там попроще, мягко говоря) Без MMU-то. Кстати, туда тоже часто стоит подумать, а нужен ли свой костыль, или лучше FreeRTOS поставить — все-таки она протестирована многими тысячами, если не сотнями тысяч разработчиков. А дополнительных затрат почти не вносит, очень легкий код, без навязывания HALa.

          А в общем случае это все рассматривается в подавляющем большинстве статей про программирование под x86, погуглите «hello world защищенный режим» — там рассматривается достаточно информации, чтобы понять как это вообще работает (по сути, теории-то там мало, вам БИОС отдает управление и крутитесь как хотите, настраивайте ваш протектед режим и радуйтесь), но в то же время от хеллоуворлда до нормального бутлодера очень далеко — не потому что там какие-то тайные знания, а потому что надо реализовать очень много рутинного кода по работе с ФС и периферией, чтобы лодер смог реально грузить ваше ядро на уровне современных.
          • +1
            Я потому и выбрал эту железку в силу своей простоты. Что касается FreeRTOS, задача-то в том и состоит, чтобы понять как оно сделано, а не «поставил-работает».
        • +1
          Бутлоадер пускает BIOS или EFI. А как они это делают, это уже отдельная история. Сложность в том, что пока контролёр памяти не инициализирован, использовать память нельзя Взято отсюда. Я не представляю, откуда в этот момент исполняется код. Буду благодарен, если кто-нибудь объяснит это.

          З.Ы. Сам раньше игрался с бутлоадерами, даже мини-отладчик писал, который выводил содержимое регистров и немного данных из стека. Выводить умел в любой позиционной системе счисления (ну пока алфавит не кончится).
          • +2
            Из микросхемы биоса и исполняет. Она мапирована на адресное пространство.
            • 0
              Это то я знаю. Т.е. саму микросхему никак инициировать не нужно? Процессор получает из неё данные, минуя контроллер памяти?
              • +2
                Да. Самая первая инструкция, которую исполняет ваш процессор — это инструкция из ПЗУ БИОСа. Джамп, обычно.
          • +2
            Вы про x86? Там контроллер памяти — это первое, что инициализируется БИОСом (ну или UEFI, не суть важно). Процессор же при старте сразу начинает исполнять код, который в БИОСе хранится. Я ковырял старый биос от phoenix времён второго пентиума, там интересная техника использовалась для вызова функций. Памяти то нету, стека, следовательно, тоже. В листинге по этой ссылке можете посмотреть: sites.google.com/site/pinczakko/pinczakko-s-guide-to-award-bios-reverse-engineering#Relocatable_Hardware_Port. Вообще, почитайте эту статью, крайне интересно.

            Вкратце, исполняется «пролог», который инициализирует память, распаковывает в память образ биоса, вычленяет из него модули и начинает их по-очереди запускать, а каждый модуль, в свою очередь, инициализирует свою часть железа. Потом уже подгружаются биосы pci карточек, а потом запускается загрузчик с диска или pxe сетевухи.
            • 0
              Мне интересно, как запускается BIOS на многоядерном ЦП?
              Процессор же при старте сразу начинает исполнять код
              а в многоядерном другие ядра выключены в момент старта или что они исполняют?
              • 0
                Выключены, они запускаются позже операционной системой.
              • 0

                На многоядерных процах читается бит из MSR регистров, и то ядро, которое является загрузочным, получит бит BSP установленным в 1, остальные ядра в ноль.

            • 0
              В этой книге есть описание 2-х способов вызовов функций без стека. Насколько я помню, первый способ это использование в коде макросов, которые сохраняют адреса возврата, вместо call и ret. Второй предполагает использование кэша процессора в качестве стека. Кстати книжка весьма интересная, всем кто интересуется работой биоса должна понравиться. Даже есть перевод на русский.
          • 0

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


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


            Ну а потом проключается RAM, делается её POST и дальше все из RAM.
            Это очень кратко, на самом деле все куда сложнее)

            • 0

              Ну 64к для низкоуровневого кода это очень много. Можно всю инициализацию туда и уместить.

              • 0

                у winbond на чипсет AMD760g боис 2 мегабайта идет.

                • 0

                  В 64К только ранняя. Самое сложное из RAM stage проворачивается, инициализация, нумерация, назначение ресурсов всех деавайсов, + ROM всякие запускаются, например для видюхи и драйвера для Ethernet, чтобы WAN обеспечивать, т.е. кода там очень даже прилично)

    • 0
      На ЦИТ форуме лет 10 назад валялась статья «The real hello world» где это делали. Если займетесь некромантией, может найдете. Там делали загрузочкую дискету в которой инициализировали А20 и переходили в защищенный режим. А потом из него уже писали строку hello world. Без каких либо костылей. Было довольно интересно написано, рекомендую.
      • 0
        Не такая уж и некромантия. Всего один запрос в гугл и вуаля.
        • 0
          Угу, она самая
  • +2
    Великолепная статья, спасибо.
  • +16
    Хорошая статья. Главное донести мысль до начинающих, что те, кто начинает ОС писать с бутлодера, скорее всего, на этом бутлодере и закончат.
    В вашей статье отлично смещен фокус на разработку именно полезного кода, это радует.
  • +1
    Зачитался просто, огромное спасибо. Расширил свой кругозор и уменьшил в размерах своё черное пятно в знаниях (хоть оно так и осталось безграничным :-) ).
    • 0
      скорее увеличили свое белое пятно в черной вселенной знаний ))
      А вообще статья интересная и увлекателная, но непонятная )) Никогда не задумывался, создавая очередную загрузочную флешку, как оно все устроено… А оно вот оно как…
    • 0
      «Если представить наши знания в виде расширяющегося круга, то чем больше мы знаем, тем больше граница с непознанным» :)
      • 0
        В этой аналогии лишь одно хорошо — площадь (объем знаний) увеличивается квадратично, а длина окружности (граница с непознанным) — линейно. :)
  • 0
    Очень познавательно, спасибо.

  • 0
    Спасибо, было интересно.
    В общем-то, в 8-разрядных микроконтроллерах это обычное дело, запускать код без ОС. И дисплея там зачастую нет, вывод только на какой-нибудь UART.
    Настоящие процессоры это, в каком-то смысле, «микроконтроллеры на стероидах». Поэтому меня всегда интересовало, а как там обстоит дело с запуском кода без ОС. Из статьи я понял, что весьма и весьма сложно.
    • +1
      На самом деле ничего сложного. В реальном режиме (сразу после передачи управления бутлоадеру) вы можете делать что хотите: выодить текст на экран, писать\читать из портов, общаться с жёстким диском. Вот только не будет у вас страничной адресации, которая доступна в защищенном режиме, и память доступна будет не вся.
    • 0
      В традиционном x86 PC это как раз весьма несложно.

      При старте BIOS просто-напросто загружает в память по известному адресу содержимое первого сектора диска и передает ему управление. Код дальше может вывести что-либо на экран, либо просто записав что-либо в видеопамять (адреса известны), либо вызвав функции BIOS. Можно какой-нибудь тетрис написать, это несложно. Все оборудование под рукой.
  • 0
    Классно. Спаисбо. Если хватит желания, попобую реализовать. Еще интересно было бы почитать как сделать очень простой дистрибутив линукс. Без всяких дополнений.
  • +21
    0xB8000

    этот адрес будит воспоминания…
    • +2
      У меня плохое детство было, вспоминается больше 0x7C00.
  • 0
    все ушли писать собственные ОС ;-) Если серьезно, то мне раньше это казалось одной из высших ступеней знания компьютера и программирования. Хотя оно так и осталось, но для сознания стало более осязаемо) Вряд ли применю как то на практике, но почитать было интересно.
  • 0
    Класс, кто-то писал про Python OS, вот он шанс поднять онное :) главное перелинковать все правильно.
    • 0
      Угу. Только сначала написать менеджер памяти как минимум. А еще неплохо бы управление процессами/потоками и драйвер ФС.
      • 0
        Ага, и ещё один мааааленький кусочек — TCP/IP стек :)
        • 0
          Вот именно его отсутствие интерпретатор вполне переживет. =)
          Да и планировщик, наверное, можно отрезать.
          • 0
            Нуу, тогда сразу возникает вопрос — а как код попадает на целевую машину. Набирать с клавиатуры вряд ли захотите, писать каждый раз на cd — запаришься, а дисководы нынче не найти. Остаются сеть и флешки, но tcp/ip в базовом варианте всяко проще, чем usb стек.
            • 0
              Grub и его директива module(http://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-modules и forum.osdev.org/viewtopic.php?t=11601) решают. В ядре интерпретатор, рядом в архиве, прописанном как модуль, код.
              • 0
                Так не, как вы будете доставлять код на машину извне?
                • 0
                  Что мешает запихать grub, ядро и модуль на флешку? Grub с них вполне успешно грузится.
                  • 0
                    Если научиться на ту же флешку писать через BIOS, как это делает grub — то вполне рабочий прототип получится, да.
                    • 0
                      Ну, это уже выходит за «минимальную работоспособность». И VM86, по моим воспоминаниям, тот еще гемор.
                      • +1
                        Не обязательно настраивать VM86. Можно прыгнуть в RM, выполнить код, и вернуться в PM.
                        • 0
                          Я, честно говоря, уже многое забыл, но у меня есть подозрение, что переход в RM создаст кучу интересных проблем в плане адресации — нужно будет перенастраивать сегменты так, чтобы не потерять данные, которые планируется записать.
  • +1
    Не следует брать откуда-то файлы stdarg.h и types.h (который на самом деле stddef.h). Эти файлы — это не часть libc, это builtin заголовки вашего компилятора, они всегда поставляются с компилятором Си, они могут отличаться между версиями компилятора, и конечно же, реализация для, например, gcc и clang, различается.
    • 0
      И что это означает на практике?
      • +2
        На практике это означает что:

        > Не следует брать откуда-то файлы stdarg.h и types.h (который на самом деле stddef.h).

        Используйте эти заголовки только такими, какие они идут с вашим компилятором.
  • 0
    Интересно, а как MemTest86 загружается (можно с дискетки без всяких Грубов)?

    А ещё, вроде (точно не знаю, не судите строго), досовские com-файлы голый машинный код содержали. Нельзя, скажем, взять какую-нибудь первобытную com-игруху и сразу ей управление передать, не загружая ОС?
    • +1
      У memtest'а собственный загрузчик.

      .com файл — это обычный выполняемый файл который просто не содержит заголовка для ОС (ms-dos в вашем случае). А по факту его выполнение требовало подготовки, а также практически любая более-менее приличная программа как и игры зачастую полагаются на функции предоставляемые ms-dos и без оной конечно работать не будут.
    • +2
      Можно. Только загвоздка в самих .COM файлах. Их писали, полагая, что вы всё же загрузили ОС. А потому их писали, используя Int 21h направо и налево. И мало кто этим не пользовался.
    • 0
      Если Вы скачаете исходники MemTest86, там есть файл README.background, озаглавленный «The Anatomy & Physiology of Memtest86-SMP», где детально разобран процесс загрузки. Сначала BIOS считывает boot-сектор, который загружает из последующих секторов загрузчик, потом переход в защищенный режим, считывание основного кода, передача ему управления и так далее.

      Ведь что такое GRUB? В GRUBом приближении, это штука, которая при старте умеет найти на диске разные разделы, умеет читать разные файловые системы, умеет загружать в память различные форматы бинарных файлов (т.е. загрузчики этих операционок), умеет передавать им разные параметры, спросив что-то у пользователя. Т.е. берет на себя основную рутину. Никакой магии.
  • 0
    losetup /dev/loop2 ./hdd.img \
    --offset `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$3}'\`*512 | bc` \
    --sizelimit `echo \`fdisk -lu /dev/loop1 | sed -n 10p | awk '{print $$4}'\`*512 | bc`
    losetup -d /dev/loop1

    Первая команда монтирует ранее созданный раздел к устройству /dev/loop2

    А вторая-то зачем?
    А ещё, специально для ленивых, есть kpartx. Попробуйте kpartx -a -v hdd.img
  • 0
    Спасибо за статью, очень познавательно, есть теперь чем заняться на досуге)
  • +1
    Большое спасибо за статью, потратил часа два (большая часть времени ушла на устранение последствий собственной невнимательности), но все-таки удалось собрать все как следует и запустить. :)
  • 0
    Ммм, захотелось наконец заняться LFS.

    — Хм, а нет ли на Курсере чего-нибудь такого…

    ;)
  • 0
    А не проще ли что-нибудь вроде www.denx.de/wiki/DULG/UBootStandalone
  • 0
    Если хочется без OS, и чего-то практичного — build.erlangonxen.org/ Правда придется писать на Erlang'e. Кстати, сам erlangonxen.org уже давно самохостящийся и запущен поверх голого гипервизора. Нажав на странице Esc или кликнув в верхнем правом углу можно даже посмотреть как оно там внутри тикает.
  • 0
    Описанный способ старта, понятное дело, не работает для GRUB2.
    Достаточно в файл /etc/grub.d/40_custom (на ArchLinux, в других аналогично должно быть) прописать:
    menuentry "mini_os" {
      set root='hd0,msdos1'
      multiboot /kernel.bin
      boot
    }
    

    Естественно, что 'hd0,msdos1' и путь к вашему kernel.bin могут отличаться.
  • 0
    Если не трудно, пожалуйста, добавьте в статьи ссылку на следующую, чтобы было удобно читать цикл последователььно. Спасибо!
    • +2
      Добавили в каждую статью ссылки на следующие. Приятного прочтения!
  • 0
    А как тут реализовать sleep? Вызывать asm(«NOP») в цикле?
    • 0
      Можно попробовать воспользоваться системным таймером, но я не настоящий сварщик, потому не расскажу, как.
    • 0
      Об этом мы подробно расскажем в следующей статье цикла, реализация sleep достаточно нетривиальна.
      • 0
        Я уже нагуглил про int 15 (+ ваша статья № 5) — проблема только в том, что я понимаю C но не понимаю ассемблер — не могу переправить int в регистры

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

Самое читаемое Разработка