Пишем операционную систему. Часть 1. Загрузчик

Всем привет! Сегодня мы напишем загрузчик, который будет выводить «Hello World» и запустим его на VirtualBox. Писать будем на ассемблере FASM. Скачать его можно отсюда. Также нам понадобится собственно VirtualBox и UltraISO. Перед тем как писать код, разберемся как загружаются операционные системы.

Итак, когда мы нажимаем большую кнопку включения на нашем компьютере запускается система, которая есть на любом компьютере — BIOS (Basic Input/Output System или базовая система ввода/вывода). Задача BIOS это:


  1. Обнаружить все подключенные устройства и проверить их на работоспособность. За это отвечает программа POST (Power On Self Test, самотестирование при включении). Если жизненно необходимое железо не обнаружено, то системный динамик (если таковой имеется) пропищит что-то непонятное и дальше загрузка не пойдет.
  2. Предоставить операционной системе функции для работы с железом.
  3. Считать самый первый сектор загрузочного устройства в нулевой сегмент оперативной памяти по смещению 0x7C00h и передать туда управление. 1 сектор на диске равен 512 байтам. Поэтому, наш загрузчик не должен превышать 512 байт. BIOS определяет, что сектор загрузочный по наличию в последних двух его байтах значений 0x55 и 0xAA.

Теперь можно приступать к написанию кода. Запускаем файл FASMW.EXE, который находится в архиве с FASM-мом и вставляем туда следующий код:

org 7C00h

 start:
    cli              ;Запрещаем прерывания (чтобы ничего не отвлекало)
    xor ax, ax       ;Обнуляем регистр ax
    mov ds, ax       ;Настраиваем dataSegment на нулевой адрес
    mov es, ax       ;Настраиваем сегмент es на нулевой адрес
    mov ss, ax       ;Настраиваем StackSegment на нулевой адрес
    mov sp, 07C00h   ;Указываем на текущую вершину стека
    sti              ;Запрещаем прерывания

  ;Очищаем экран
  mov ax, 3
  int 10h

  mov ah, 2h
  mov dh, 0
  mov dl, 0
  xor bh, bh
  int 10h

  ;Печатаем строку
  mov ax, 1301h
  mov bp, message
  mov cx, 12
  mov bl, 02h
  int 10h

  jmp $

message db 'Hello World!',0

times 510 - ($ - $$) db 0 ;Заполнение оставшихся байт нулями до 510-го байта
db 0x55, 0xAA ;Загрузочная сигнатура  

Этот код требует немного пояснений. Командой

org 7C00h

мы говорим, что код нужно загружать в ОЗУ по адресу 0x7C00. В строках

  mov ax, 3
  int 10h

мы устанавливаем видео режим 80х25 (80 символов в строке и 25 строк) и тем самым очищаем экран.

  mov ah, 2h
  mov dh, 0
  mov dl, 0
  xor bh, bh
  int 10h

Здесь мы устанавливаем курсор. За это отвечает функция 2h прерывания 10h. В регистр dh мы помещаем координату курсора по Y, а в регистр dl — по X.

  mov ax, 1301h
  mov bp, message
  mov cx, 12
  mov bl, 02h
  int 10h

Печатаем строку. За это отвечает функция 13h прерывания 10h. В регистр bp мы помещаем саму строку, в регистр cx — число символов в строке, в регистр bl — атрибут, в нашем случае цвет, он будет зеленым. На цвет фона влияют первые 4 бита, на цвет текста — вторые 4 бита. Ниже представлена таблица цветов

0 - черный, 1 - синий, 2 - зеленый, 3 - желтый, 4 - красный, 5 - фиолетовый, 6 - коричневый, 7 - светло-серый, 8 - темно-серый, 9 - светло-синий, A - светло-зеленый, B - светло-желтый, C - светло-красный, D- светло-фиолетовый, E - светло-коричневый, F – Белый.

В строке

jmp $

Программа зависает.

Откомпилируем код нажатием клавиш Ctrl + F9 и сохраним полученный файл как boot.bin.

Запуск


Запускаем UltraISO и перетаскиваем наш бинарник в специальную область (отмечено красной стрелкой).



Далее кликаем правой кнопкой мыши по бинаринку и нажимаем кнопку примерно с такой надписью: «Установить загрузочным файлом». Далее сохраняем наш файл в формате ISO.
Открываем VIrtualBox и создаем новую виртуальную машину (если вы не знаете, как это делается, кликайте сюда). Итак, после того, как вы создали виртуальную машину, нажимаем «Настроить, выбираем пункт „Носители“, нажимаем на „Пусто“, там где „Привод“ есть значок оптического диска. Нажимаем на него и выбираем „Выбрать образ оптического диска“, ищем наш ISO файл, нажимаем „Открыть“. Сохраняем все настройки и запускаем виртуальную машину. На экране появляется наш „Hello World!“.



На этом первый выпуск подходит к концу. В следующей части мы научим наш загрузчик читать сектора диска и загрузим свое первое ядро!

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 37

  • НЛО прилетело и опубликовало эту надпись здесь
      0
      Хм, а что не так? Все понятно ведь вроде
      +10
      система, которая есть на любом компьютере — BIOS

      Вынужден огорчить, но BIOS встречается уже далеко не везде: например, отсутствует на компьютерах Apple Mac.
      Сейчас операционные системы переходят на UEFI.
        +9

        Во-во. Про Биос и 512 байт уже все кто лень писали. Я было понадеялся, что ну может наконец кто-нибудь через UEFI будет грузиться. Ан нет. Все как в статьях 20 летней (а может и больше) давности

          +4
          Поддержу ветку. Таких статей ещё до Хабра было полно, правда загрузчик записывали на дискетку, и на реальном железе тестили. Даешь UEFI и операционку на Rust или JavaScript! :)
            +3

            Цикл статей про OS на Rust под Raspbery Pi. Это одна статья, но я помню, что было еще. И еще статья на хабре.


            UEFI (я могу ошибаться, потому что только hello world писал) в общих чертах не сложно. Вам потребуется EDK2 (девелопмент тулкит для TianoCore — Open Source имплементации интерфейса UEFI) и документация на API (возможно, я ошибся и не ту ссылку кинул). EFI-приложение с точки зрения кода — обычная программа на C, только API свое. Потом все собираете согласно манам, полученный .efi-файл кладете куда-нибудь в /EFI/BOOT на диске, загружаетесь в EFI Shell и запускаете. Для x86-64, если я не ошибаюсь, если он называется bootx64.efi, то будет запущен автоматом, если загружаться с того диска, где он лежит.


            Никакой магии, ассемблера, 512 байт и int 21. А про сам UEFI интерфейс было и на хабре.

              +1
              Да, я видел эти статьи поэтому и упомянул тот же Rust. Посыл моего комментария скорее был таким, что писать про «свою ОС» уже следует с оглядкой на современные средства и технологии, а не пересказывать вдоль и поперек изъезженные темы.
                0

                Здесь я с Вами соглашусь. Туториалов "пишем свою ОС", начинающихся и заканчивающихся на BIOS-загрузчике полно.

          +5
          > Командой org 7C00h мы говорим, что код нужно загружать в ОЗУ по адресу 0x7C00.

          Я правильно понимаю, что, следовательно, если написать org 666h, то код загрузится в ОЗУ по адресу 0x666? (Спойлер: нет).
            +5

            Опять??!
            Зачем??

              +3

              Может, автору было просто лень поискать подобные статьи на Хабре?

                0
                Учитывая время года — вероятно, публикация на Хабре была условием зачёта.
                +4
                cli ;Запрещаем прерывания (чтобы ничего не отвлекало)
                sti ;Запрещаем прерывания
                Не ошиблись в комментарии?
                  +7
                  Попробую угадать. Следующая статья будет про загрузку с образа флоппи и работу с fat12. Скажите, какой смысл в этой статье на хабре в 2020 году? Чем вас не устраивали прошлые?
                    0
                    А прошлые были лучше? Я без сарказма или иронии, просто не встречал до этого ни одной похожей, если у Вас есть возможность, то не могли бы вы поделиться ссылкой, пожалуйста?
                      +1
                        +2

                        Их было много… Большинство начинает за здравие, кончает за упокой… Но это не значит, что не надо продолжать работу в этом направлении. Нормальной, а главное, законченной серии статей про написание ОС на хабре лично я не видел.

                          +1
                          Так же это и не значит, что надо начинать ту же самую работу, с того же самого начала, с которого её начинали уже много раз до.
                      +1
                      В качестве загрузчика лучше уж тот же grub использовать. Зачем велосипедить? Впрочем я недавно сам всё это писал, чисто вспомнить детство, дошел до загрузки ядра и остановился на переходе в защищенный режим, именно потому что подумал, что нет смысла это всё писать, если можно использовать уже готовое решение, к тому же более универсальное.
                        +2
                        Лучший способ понять и запомнить, как оно внутри работает, это написать такой хелловорд. «Испачкать руки». Кроме обучающего, другого смысла я не вижу.
                          0
                          Ну это да, я вот тоже не так давно игрался, вспоминая былые времена :) Просто решил что нет смысла дальше возиться с защищенным режимом (который я кое-как помню еще) если сейчас используется long mode.

                          Заголовок спойлера
                          use16
                          org 0h
                          
                          ; ---------------------------------------------------------------------------------------
                          ; Boot
                          ;
                          ; DL - Boot drive number
                          ; ---------------------------------------------------------------------------------------
                          
                          ; Start
                              mov     ax, 07C0h
                              mov     ds, ax
                          
                          ; Initialize stack
                              mov     ax, 9000h
                              cli
                              mov     ss, ax
                              mov     sp, 0FFF0h
                              sti
                          
                          ; Set destination segment to load kernel to
                              mov     ax, 50h
                              mov     es, ax
                          
                              mov     si, msg_boot_loading
                              call    print_string
                          
                          ; Reset floppy drive
                              mov     ah, 0
                              int     13h
                              jc      .boot_error
                          
                          ; Load kernel into memory
                              mov     ax, 0
                              mov     bx, sectors_per_track
                              mov     cx, heads_per_cylinder
                              call    calc_chs
                          
                              xor     bx, bx ; es:bx -> buffer
                              mov     al, 18 ; sectors count; read 18 sectors of loader
                              mov     ah, 2
                              int     13h
                              jc      .boot_error
                          
                          ; Set data segment
                              push    es
                              pop     ds
                          
                          ; Pass control to kernel
                              push    ds
                              push    word kernel_entry
                              retf
                          
                          ; Show error
                          .boot_error:
                              mov     si, msg_fail
                              call    print_string
                          
                          .boot_finish:
                              hlt
                              jmp     .boot_finish
                          
                          ;
                          ; Print String
                          ;
                          ; DS:SI must point to zero-terminated string
                          ;------------------
                          print_string:
                              pushf
                              push    ax
                              push    bx
                              push    si
                              cld
                              mov     ah, 0Eh
                              mov     bh, 0
                          print_string.repeat:
                              lodsb
                              cmp     al, 0
                              je      print_string.finish
                              int     10h
                              jmp     print_string.repeat
                          print_string.finish:
                              pop     si
                              pop     bx
                              pop     ax
                              popf
                              ret
                          
                          ;
                          ; Calculate CHS address by logical sector address
                          ;
                          ; AX - Logical sector number
                          ; BX - Sectors per track
                          ; CX - Heads per cylinder
                          ; Returns (ready for Int13h:02h call):
                          ; CL - Sector number
                          ; CH - Track number
                          ; DH - Head number
                          ; -----------------
                          calc_chs:
                              pushf
                              push    ax
                              push    bx
                              xor     dx, dx ; prepare dx:ax for operation
                              div     bx ; divide by sectors per track
                              inc     dl ; add 1 (obsolute sector formula)
                              mov     bl, dl
                              xor     dx, dx ; prepare dx:ax for operation
                              div     cx ; mod by number of heads (Absolue head formula)
                              mov     dh, dl ; everything else was already done from the first formula
                              mov     ch, al ; not much else to
                              mov     cl, bl
                              pop     bx
                              pop     ax
                              popf
                              ret
                          
                          ; Data
                          
                          msg_boot_loading        db 'Loading kernel... ', 0
                          msg_ok                  db 'Ok.', 0Dh, 0Ah, 0
                          msg_fail                db 'Failed!', 0Dh, 0Ah, 0
                          
                          sectors_per_track       dw 18
                          heads_per_cylinder      dw 2
                          
                          times 510 - ($ - $$)    db 0
                                                  dw 0xAA55
                          
                          ; ---------------------------------------------------------------------------------------
                          ; Kernel (real mode)
                          ; ---------------------------------------------------------------------------------------
                          
                          ; CS, DS, and ES now set to 0050h, SS to 9000h
                          
                          kernel_entry:
                              mov     si, msg_ok
                              call    print_string
                          
                              mov     eax, 0FAFAFDFDh
                              shr     eax, 16
                              cmp     ax, 0FAFAh
                              jne     .fail
                          
                              mov     si, msg_ok
                              call    print_string
                          
                          ; push 0B800h
                          ; pop es
                          ; xor di, di
                          ; mov word [es:di], 0730h
                          
                              nop
                              nop
                              nop
                              nop
                              mov     ax, an_address
                          
                          .finish:
                              hlt
                              jmp     .finish
                          
                          .fail:
                              mov     si, msg_fail
                              call    print_string
                              jmp     .finish
                          
                          ; ---------------------------------------------------------------------------------------
                          ; Kernel (protected mode)
                          ; ---------------------------------------------------------------------------------------
                          
                          ; Data
                          
                          gdt:
                          
                          ; null descriptor
                                                  dd 0
                                                  dd 0
                          
                          ; code descriptor
                          gdt_code_limit_low      dw 0FFFFh
                          gdt_code_base_low       dw 0
                          gdt_code_base_middle    db 0
                          gdt_code_access         db 10011010b
                          gdt_code_granularity    db 11001111b
                          gdt_code_base_high      db 0
                          
                          ; data descriptor
                          gdt_data_limit_low      dw 0FFFFh
                          gdt_data_base_low       dw 0
                          gdt_data_base_middle    db 0
                          gdt_data_access         db 10010010b
                          gdt_data_granularity    db 11001111b
                          gdt_data_base_high      db 0
                          
                          ;
                          ; Print Byte
                          ;
                          ; AL - byte to print
                          ;------------------
                          print_byte:
                              pushf
                              push    ax
                              push    dx
                              mov     dl, al
                              shr     al, 4
                              add     al, 48
                              cmp     al, 58
                              jl      print_byte.print
                              add     al, 7
                          print_byte.print:
                              call    print_char
                              mov     al, dl
                              and     al, 0Fh
                              add     al, 48
                              cmp     al, 58
                              jl      print_byte.print2
                              add     al, 7
                          print_byte.print2:
                              call    print_char
                              pop     dx
                              pop     ax
                              popf
                              ret
                          
                          ;
                          ; Print Char
                          ;
                          ; AL - char to print
                          ;------------------
                          print_char:
                              pushf
                              push    ax
                              push    bx
                              xor     bx, bx
                              mov     ah, 0Eh
                              int     10h
                              pop     bx
                              pop     ax
                              popf
                              ret
                          
                          ;
                          ; Print New Line Chars
                          ;
                          ;------------------
                          print_nl:
                              pushf
                              push    ax
                              mov     al, 0Dh
                              call    print_char
                              mov     al, 0Ah
                              call    print_char
                              pop     ax
                              popf
                              ret
                          
                          ;
                          ; Print Word
                          ;
                          ; AX - word to print
                          ;------------------
                          print_word:
                              push    ax
                              mov     al, ah
                              call    print_byte
                              pop     ax
                              call    print_byte
                              ret
                          
                          ;
                          ; Print Bytes
                          ;
                          ; DS:SI - pointer to data to print
                          ; CX - number of bytes to print
                          ;------------------
                          print_bytes:
                              pushf
                              push    ax
                              push    cx
                              push    si
                          print_bytes.repeat:
                              mov     al, [si]
                              call    print_byte
                              dec     cx ; !!
                              inc     si
                              jcxz    print_bytes.finish ; replace to loop???
                              mov     al, 20h
                              call    print_char
                              jmp     print_bytes.repeat
                          print_bytes.finish:
                              pop     si
                              pop     cx
                              pop     ax
                              popf
                              ret
                          
                          ;
                          ; Print Number
                          ;
                          ; AX - number to print
                          ; BL - number base (2, 8 or 10)
                          ;------------------
                          print_num:
                              pushf
                              push    ax
                              push    bx
                              push    cx
                              push    dx
                              xor     cx, cx
                              mov     bh, 0
                          print_num.repeat:
                              xor     dx, dx
                              div     bx
                              push    dx
                              inc     cx
                              cmp     ax, 0
                              jne     print_num.repeat
                          print_num.output:
                              pop     ax
                              add     al, 48
                              call    print_char
                              loop    print_num.output
                              pop     dx
                              pop     cx
                              pop     bx
                              pop     ax
                              popf
                              ret
                          
                          ;
                          ; Print Decimal Number
                          ;
                          ; AX - number to print
                          ; -----------------
                          print_decimal:
                              push    bx
                              mov     bl, 10
                              call    print_num
                              pop     bx
                              ret
                          
                          an_address:
                          
                          ;
                          ;
                          ;
                          ;
                          ; -----------------
                          



                          Вообще скорее просто ассемблер хотел вспомнить от нечего делать.
                            0
                            Помню как то раз прыгал из real mode сразу в long mode… Если мне не изменяет память, то это возможно (главное таблицы страниц настроить так, чтобы физические адреса совпадали с виртуальными для кода).
                              0
                                0
                                Я что-то такое читал на osdev. Просто с long mode еще не разбирался, а с protected mode игрался еще в старших классах. Ну и я не супероперационку делаю, просто балуюсь :) Хотел вспомнить ассемблер — написал загрузик, вспомнил, ну и забил пока. Будет совсем нечего делать — попробую уже с grub поиграть.

                                P.S. Ну вот выше как раз ссылку на ту статью дали.
                                0
                                Хм, помню, я много лет назад на заре становления себя писал загрузчик из бут-сектора в лонг-мод, но я вроде бы делал это через защищённый… И думал, что это обязательно или по крайней мере предпочтительно. А оно на самом деле нет?
                                  0
                                  Там выше ссылка на osdev, где показано как это делается.
                            +3

                            Эхх сколько таких циклов было лет 15 назад
                            90%стопорилось на загрузчике 9% доходили до интерпретатора командной строки и поддержки fat, 0,(9)%доходили до vesa графики
                            Дошедших до прикладных программ единицы

                              0
                              зануда_mode=true
                              println('0.(9) == 1 по определению');
                                –1
                                зануда_mode=true
                                0.(9)%*число статей
                                зависит от числа статей и не всегда равен 1
                              0
                              При всем уважении к автору — писать первичный загрузчик самому — контр-продуктивно. Во-первых, как верно сказали выше, UEFI шагает по планете. Во-вторых, и это самое главное в моем тезисе — лучше изучить спецификацию multiboot и использовать тот же GRUB, сосредоточившись на управлении памятью в вашем ядре. Вот о чем мало полезных статей.

                              Если поможет — вот мои потуги 2013 года в этом направлении.
                                0
                                Кстати, а разве mov не «тяжелее» xor'а? Зачем ax обнулять xor'ом, а остальные регистры mov'ом?
                                  +2

                                  Потому что xor-ить сегментные регистры нельзя — таких инструкций попросту нет.

                                    0
                                    Уже не тяжелее, в процессоре они преобразуются в одинаковую внутреннюю инструкцию.
                                    0

                                    Ностальгия. Вспомнил, как 15 лет назад на WASM писал загрузчик, ядро с переходом в защищённый режим, простенький менеджер процессов. Забросил на этапе реализации виртуальной памяти.

                                      0

                                      спасибо! жду продолжения! здорово что всё с пояснениями и без сторонних библиотек, хочется понять как всё работает с самого начала

                                        0
                                        Рекомендую всем, кто хочет пописать OS пройти курс MIT на основе операционной системы JOS. У нас в универе был отличный набор лабораторных работ на основе этого курса.
                                          +2
                                          Лучше бы написали под ARM.

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

                                          Самое читаемое