Як відкрити програму на комп'ютер. Як запустити програму без операційної системи

Не було достатньо докладно описано найголовніше: як же запустити цей код на реальному залізі? Як створити власний завантажувальний диск? У цій статті ми докладно відповімо на всі ці питання (частково ці питання розбиралися в попередній статті, але для зручності читання дозволимо собі невелике дублювання матеріалу).

В інтернеті існує безліч описів і туторіалів про для того як написати свою міні-ОС, навіть існують сотні готових невеликих хобі-ОС. Один з найбільш гідних ресурсів на цю тематику, який хотілося б особливо виділити, це портал 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.

а процес завантаження системи:

Щоб усе вийшло, необхідно буде створити кілька файлів та каталогів:

Крок 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-ми кілобайтах образу ядра знаходилася наступна структура:

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

Text .global loader # making entry point visible to linker # setting up Multiboot header - pozri GRUB docs for details .set FLAGS, 0x0 # this is the Multiboot "flag" field .set MAGIC, 0x1BADB002 # "magic number" lets boot 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










































Розглянемо код докладніше. Цей код майже не зміненому вигляді взятий з wiki.osdev.org/Bare_Bones . Оскільки для компіляції використовується gcc, використовується синтаксис GAS. Розглянемо докладніше, що робить цей код.

Весь наступний код потрапить до виконуваної секції.text.

Global loader

Оголошуємо символ loader видимим для лінковника. Це потрібно, так як лінковщик буде використовувати loader як точку входу.

Set FLAGS, 0x0 # привласнити FLAGS = 0x0. 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 посилається на діапазон.com mbd, 4 # зарезервувати 4 байти під змінну mdb в області COMMON .com 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

Створимо файл commonprintf.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, яку потрібно написати. Для цього створимо файл commonscreen.с, з таким вмістом:

#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 JP define DEF_VRAM_BASE 0xb8000 // Попередня основа для відеопам'яті static unsigned char curr_col = 0; static unsigned char curr_row = 0; (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;< VRAM_SIZE; i++) cons_putc(" "); curr_col = 0; curr_row = 0; }

Вказаний код містить просту логіку друку символів на екран у текстовому режимі. У цьому режимі для запису символу використовується два байти (один з кодом символу, інший з його атрибутами), пам'ять, що записуються прямо у відео, що відображається відразу на екрані і починається з адреси 0xB8000. Роздільна здатність екрана при цьому 80x25 символів. Безпосередньо друк символу здійснюється з допомогою макросу PUT.
Тепер не вистачає лише кілька заголовних файлів:
1. Файл includescreen.h. Оголошує функцію putchar, яка використовується у функції printf. Вміст файлу:

#ifndef _SCREEN_H #define _SCREEN_H void clear_screen(void); void putchar(int c); #endif

2. Файл includeprintf.h. Оголошує функцію printf, яка використовується у main. Вміст файлу:

#ifndef _PRINTF_H #define _PRINTF_H int printf (const char *format, ...); #endif

3. Файл includestdarg.h. Оголошує функції для перебору аргументів, кількість яких наперед не відома. Файл повністю береться з проекту www.bitvisor.org/. Шлях до файлу в коді проекту bitvisor: includecorestdarg.h.
4. Файл includetypes.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 не налаштовує сторінкову адресацію, то віртуальна адреса сегмента має збігатися з її фізичною адресою, оскільки в нашій програмі віртуальна пам'ятьтак само не налаштовується.

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 quitn" | 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 (мета зображення). Розглянемо поетапно, як і відбувається.

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; 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 quitn" | 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

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

Що робити, якщо потрібно запустити програму, але ви не бажаєте встановлювати її? Zero Install на допомогу!

Мільярди людей по всьому світу використовують комп'ютери або ноутбуки на операційній системі Windows. Зазвичай ми встановлюємо програми, після чого вони займають вільне місцена диску та використовують оперативну пам'ять. Це знижує швидкість роботи комп'ютера.

Сьогодні ми розповімо, як ви можете збільшити продуктивність комп'ютера та знизити навантаження на згадку. Для цього вам потрібно запускати необхідні програмибез їх встановлення.

Як запустити програму без встановлення?

1. Завантажте Zero Install.

По-перше, завантажте програмне забезпечення Zero Install, яке дозволить вам запускати програми без необхідності встановлювати їх на комп'ютер.

2. Встановіть Zero Install.

Після того, як ви завантажили Zero Install, двічі натисніть на файл, щоб встановити його. Потім запустіть нову програмуна комп'ютері.

3. Натисніть накладку "Каталог".

Як тільки Zero Install буде запущена на комп'ютері або ноутбук Windows, ви повинні перейти до вкладки «Catalog». Тут натисніть кнопку «Refresh list», щоб оновити список доступних програм.

4. Виберіть програму, щоб запустити.

Уважно подивіться повний списокдоступних програм. Якщо ви знайшли потрібну вам програму, виділіть її та натисніть Run. Деякі програми можуть вибрати браузер Firefoxабо Mozilla для запуску. Просто зачекайте до повного завантаження програмного забезпечення, після чого ви зможете запустити її на своєму комп'ютері, не встановлюючи.


Підбиття підсумків

Якщо на вашому комп'ютері недостатньо вільної пам'ятіабо потужності, щоб запустити програму, можна використовувати Zero Install. Я думаю, що це чудовий спосіб не засмічувати свій комп'ютер програмами, які вам потрібні на один раз.

Також вашому комп'ютеру може не вистачати потужності для запуску певних програм, наприклад Eclipse IDE, JetBrains, NetBeans і т.д. Це справді важкі програми для розробників, які споживають велику кількість оперативної пам'яті.

Zero Install допоможе запустити ці, та багато інших програм, без встановлення на комп'ютер.

Не було достатньо докладно описано найголовніше: як же запустити цей код на реальному залізі? Як створити власний завантажувальний диск? У цій статті ми докладно відповімо на всі ці питання (частково ці питання розбиралися в попередній статті, але для зручності читання дозволимо собі невелике дублювання матеріалу).

В інтернеті існує безліч описів і туторіалів про для того як написати свою міні-ОС, навіть існують сотні готових невеликих хобі-ОС. Один з найбільш гідних ресурсів на цю тематику, який хотілося б особливо виділити, це портал 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.

А процес завантаження системи:

Щоб усе вийшло, необхідно буде створити кілька файлів та каталогів:

Крок 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-ми кілобайтах образу ядра знаходилася наступна структура:

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

Text .global loader # making entry point visible to linker # setting up Multiboot header - pozri GRUB docs for details .set FLAGS, 0x0 # this is the Multiboot "flag" field .set MAGIC, 0x1BADB002 # "magic number" lets boot 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










































Розглянемо код докладніше. Цей код майже не зміненому вигляді взятий з wiki.osdev.org/Bare_Bones . Оскільки для компіляції використовується gcc, використовується синтаксис GAS. Розглянемо докладніше, що робить цей код.
.text
Весь наступний код потрапить до виконуваної секції.text.
.global loader
Оголошуємо символ loader видимим для лінковника. Це потрібно, так як лінковщик буде використовувати loader як точку входу.
.set FLAGS, 0x0 # привласнити FLAGS = 0x0. .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 посилається на діапазон.com mbd, 4 # зарезервувати 4 байти під змінну mdb в області COMMON .com 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

Створимо файл commonprintf.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 JP define DEF_VRAM_BASE 0xb8000 // Попередня основа для відеопам'яті static unsigned char curr_col = 0; static unsigned char curr_row = 0; [(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. Файл includeprintf.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; 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

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

Бажаєте дозволити користувачам зі стандартним обліковим записом запускати програми з правами адміністратора без UAC або запиту на введення пароля? Тоді я розповім, як це зробити. Ми створимо ярлик, який використовує команду runas/savecredяка зберігає пароль. Зауважу, що це можна вважати діркою у безпеці – звичайний користувачзможе використовувати runas /savecred для виконання будь-якої команди від імені адміністратора без введення пароля. Тим не менш, в деяких ситуаціях це може бути корисно – наприклад, якщо ви хочете, щоб ваша дитина з-під стандартної облікового записуміг запускати програми від імені адміністратора, не питаючи вас.

Включаємо обліковий запис адміністратора

У першу чергу необхідно включити вбудований обліковий запис адміністратора, який за замовчуванням вимкнено. Отже, клацніть правою кнопкою миші по ярлику командного рядкаі виберіть команду «Запустити від імені адміністратора».

У вікні командного рядка виконайте наступну команду:

net user administrator /active:yes


Тепер обліковий запис увімкнено, хоча і без пароля. Щоб встановити пароль, відкрийте «Панель керування, виберіть категорію «Облікові записи користувачів та сімейна безпека», а потім відкрийте меню «Облікові записи користувачів». Далі клацніть на посилання «Управління іншим обліковим записом».

Виберіть обліковий запис адміністратора, натисніть кнопку «Створити пароль» і створіть пароль для облікового запису адміністратора.

Створюємо ярлик

Тепер ми створимо ярлик, який запускатиме додаток з адміністраторськими привілеями. Клацніть правою кнопкою миші на робочому столі, виберіть пункт "Створити", а потім натисніть "Ярлик".

У вікні потрібно ввести команду наступного типу:

runas /user: ComputerName\Administrator /savecred “ C:\Path\To\Program.exe

Зверніть увагу, що вам необхідно замінити ComputerNameна ім'я вашого комп'ютера, а C:\Path\To\Program.exeна повний шлях до програми ви хочете запустити. Наприклад, якщо ім'я комп'ютера Laptop, а програмою, яку ви хочете запустити, є Auslogics BoostSpeed, вам необхідно ввести наступний шлях:

runas /user:Laptop\Administrator /savecred “C:\Program Files\Auslogics\Auslogics BoostSpeed\BoostSpeed.exe"


У наступному вікні введіть ім'я ярлика. За бажанням можна вибрати іконку для нового ярлика – клацніть правою кнопкою миші по ньому і виберіть пункт «Властивості».

У діалоговому вікні «Властивості» натисніть кнопку «Змінити значок» та виберіть відповідний.

Коли ви вперше двічі клацніть по ярлику, вам буде запропоновано ввести пароль від облікового запису адміністратора, який ви створили раніше.


Цей пароль буде збережено – наступного разу, коли ви запускатимете програму, вам уже не доведеться вводити його знову.

Як вже згадувалося вище, з-під стандартних облікових записів користувачі можуть запускати будь-які програми з правами адміністратора без введення пароля (за допомогою команди runas /savecred), так що майте це на увазі.

Пароль адміністратора зберігається в диспетчері облікових даних – якщо ви хочете видалити збережений пароль, ви можете зробити це звідти.

Чудового Вам дня!