Как составить программу для процессора

Написание простого процессора и окружения для него

Время на прочтение
4 мин

Количество просмотров 21K

Здравствуйте! В этой статье я расскажу какие шаги нужно пройти для создания простого процессора и окружения для него.

Архитектура набора команд (ISA)

Для начала нужно определиться с тем, каким будет процессор. Важны такие параметры как:

  • Размер машинного слова и регистров(разрядность/»битность» процессора)
  • Машинные команды (инструкции) и их размер

Архитектуры процессоров можно разделить по размеру инструкций на 2 вида (на самом деле их больше, но другие варианты менее популярны):

  • RISC
  • CISC

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

Я решил сделать RISC процессор во многом похожий на MIPS.

Я это сделал по целому ряду причин:

  • Довольно просто создать прототип такого процессора.
  • Вся сложность такого вида процессоров перекладывается на такие программы как ассемблер и/или компилятор.

Вот основные характеристики моего процессора:

  • Машинное слово и размер регистров — 32 бита
  • 64 регистра (включая счетчик команд)
  • 2 типа инструкций

Register type(досл. Регистровый тип) выглядит вот так:

rtype

Особенность таких инструкций заключается в том, что они оперируют с тремя регистрами.

Immediate type(досл. Немедленный тип):

itype

Инструкции этого типа оперируют с двумя регистрами и числом.

OP — это номер инструкции, которую нужно выполнить (или же для указания, что эта инструкция Register type).

R0, R1, R2 — это номера регистров, которые служат операндами для инструкции.

Func — это дополнительное поле, которое служит для указания вида Register type инструкций.

Imm — это поле куда записывается то значение, которое мы хотим явно предоставить инструкции в качестве операнда.

  • Всего 28 инструкций

Полный список инструкций можно посмотреть в github репозитории.

Вот лишь пару из них:

nor r0, r1, r2

NOR это Register type инструкция, которая делает логическое ИЛИ НЕ на регистрах r1 и r2, после записывает результат в регистр r0.

Для того, чтобы использовать эту инструкцию нужно изменить поле OP на 0000 и поле Func на 0000000111 в двоичной системе счисления.

lw r0, n(r1)

LW это Immediate type инструкция, которая загружает значение памяти по адресу r1 + n в регистр r0.

Для того, чтобы использовать эту инструкцию в свою очередь нужно изменить поле OP на 0111, а в поле IMM записать число n.

Написание кода процессора

После создания ISA можно приступить к написанию процессора.

Для этого нам нужно знание какого нибудь языка описания оборудования. Вот некоторые из них:

  • Verilog
  • VHDL (не путать с предыдущим!)

Я выбрал Verilog, т.к. программирование на нем было частью моего учебного курса в университете.

Для написания процессора нужно понимать логику его работы:

  1. Получение инструкции по адресу Счетчика команд (PC)
  2. Декодирование инструкции
  3. Выполнение инструкции
  4. Прибавление к Cчетчику команды размера выполненной инструкции

И так до бесконечности.

Получается нужно создать несколько модулей:

  • Регистровый файл
  • Декодер
  • АЛУ

Разберем по отдельности каждый модуль.

Регистровый файл

Регистровый файл предоставляет доступ к регистрам. С его помощью нужно получать значения каких то регистров, или изменять их.

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

Декодер

Декодер это тот блок, который отвечает за декодирование инструкций. Он указывает какие операции нужно выполнить АЛУ и другим блокам.

Например, инструкция addi должна сложить значение регистра $zero(Он всегда хранит 0) и 20 и положить результат в регистр $t0.

addi $t0, $zero, 20

На этом этапе декодер определяет, что эта инструкция:

  • Immediate type
  • Должна записать результат в регистр

И передает эти сведения следующим блокам.

АЛУ

После управление переходит в АЛУ. В нем обычно выполняются все математические, логические операции, а также операции сравнения чисел.

То есть, если рассмотреть ту же инструкцию addi, то на этом этапе происходит сложение 0 и 20.

Другие

По мимо вышеперечисленных блоков, процессор должен уметь:

  • Получать и изменять значения в памяти
  • Выполнять условные переходы

Тут и там можно увидеть как это выглядит в коде.

Ассемблер

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

Я решил реализовать его на языке программирования Си.

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

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

Обычная программа начинается с объявления сегмента.

Для нас достаточно двух сегментов .text — в котором будет храниться исходный код наших программ — и .data — в котором будет хранится наши данные и константы.

Инструкция может выглядеть вот так:

.text
    jie $zero, $zero, $zero # Ветвление
    addi $t1, $zero, 2 # $t1 = $zero + 2
    lw $t1, 5($t2) # $t1 = *($t2 + 5)
    syscall 0, $zero, $zero # syscall(0, 0, 0)
    la $t1, label# $t1 = label

Сначала указывается название инструкции, потом операнды.

В .data же указываются объявления данных.

.data
    .byte 23        # Константа размером 1 байт
    .half 1337      # Константа размером 2 байта
    .word 69000, 25000  # Константы размером 4 байта
    .asciiz "Hello World!"  # Константная нуль терминируемая строка (Си строка)
    .ascii  "12312009"  # Константная строка (без терминатора)
    .space 45       # Пропуск 45 байтов

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

Удобно парсить (сканировать) ассемблер файл в таком виде:

  1. Сначала сканируем сегмент
  2. Если это .data сегмент, то мы парсим разные типы данных или .text сегмент
  3. Если это .text сегмент, то мы парсим команды или .data сегмент

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

    la  $s4,    loop          # Загружаем адрес loop в s4

loop:   # Ссылка!

    mul $s2, $s2, $s1   # s2 = s2 * s1
    addi $s1, $s1, -1    # s1 = s1 - 1
    jil $s3, $s1, $s4   # если s3 < s1 то перейди на метку 

А во второй проход можно уже и генерировать файл.

Итог

В дальнейшем, можно запускать выходной файл из ассемблера на нашем процессоре и оценивать результат.

Также готовый ассемблер можно использовать в Си компиляторе. Но это уже позже.

Ссылки:

  • Designing Digital Computer Systems with Verilog. David J. Lilja and Sachin S. Sapatnekar
  • Исходный код
  • Исходный код другого процессора

Ты решил освоить ассемблер, но не знаешь, с чего начать и какие инструменты для этого нужны? Сейчас расскажу и покажу — на примере программы «Hello, world!». А попутно объясню, что процессор твоего компьютера делает после того, как ты запускаешь программу.

Содержание

  1. Основы ассемблера
  2. Если наборы инструкций у процессоров разные, то на каком учить ассемблер лучше всего?
  3. Что и как процессор делает после запуска программы
  4. Регистры процессора: зачем они нужны, как ими пользоваться
  5. Подготовка рабочего места
  6. Написание, компиляция и запуск программы «Hello, world!»
  7. Инструкции, директивы
  8. Метки, условные и безусловные переходы
  9. Комментарии, алгоритм, выбор регистров
  10. Взаимодействие с пользователем: получение данных с клавиатуры
  11. Полезные мелочи: просмотр машинного кода, автоматизация компиляции
  12. Выводы

Основы ассемблера

Я буду исходить из того, что ты уже знаком с программированием — знаешь какой-нибудь из языков высокого уровня (С, PHP, Java, JavaScript и тому подобные), тебе доводилось в них работать с шестнадцатеричными числами, плюс ты умеешь пользоваться командной строкой под Windows, Linux или macOS.

Если наборы инструкций у процессоров разные, то на каком учить ассемблер лучше всего?

Знаешь, что такое 8088? Это дедушка всех компьютерных процессоров! Причем живой дедушка. Я бы даже сказал — бессмертный и бессменный. Если с твоего процессора, будь то Ryzen, Core i9 или еще какой-то, отколупать все примочки, налепленные туда под влиянием технологического прогресса, то останется старый добрый 8088.

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

РЕКОМЕНДУЕМ:
Лучшие игры для программистов и технарей

Больше того, когда ты начинаешь с начала — то есть сперва выучиваешь классический набор инструкций 8088 и только потом постепенно знакомишься с современными фичами, — ты в какой-то миг начинаешь видеть нестандартные способы применения этих самых фич.

Что и как процессор делает после запуска программы

После того как ты запустил софтину и ОС загрузила ее в оперативную память, процессор нацеливается на первый байт твоей программы. Вычленяет оттуда инструкцию и выполняет ее, а выполнив, переходит к следующей. И так до конца программы.

Некоторые инструкции занимают один байт памяти, другие два, три или больше. Они выглядят как-то так:

90

B0 77

B8 AA 77

C7 06 66 55 AA 77

Вернее, даже так:

90 B0 77 B8 AA 77 C7 06 66 55 AA 77

Хотя погоди! Только машина может понять такое. Поэтому много лет назад программисты придумали более гуманный способ общения с компьютером: создали ассемблер.

Благодаря ассемблеру ты теперь вместо того, чтобы танцевать с бубном вокруг шестнадцатеричных чисел, можешь те же самые инструкции писать в мнемонике:

nop

mov al, 0x77

mov ax, 0x77AA

mov word [0x5566], 0x77AA

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

Регистры процессора: зачем они нужны, как ими пользоваться

Что делает инструкция
mov? Присваивает число, которое указано справа, переменной, которая указана слева.

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

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

У процессора 8088 регистры 16-битные, их восемь штук (в скобках указаны типичные способы применения регистра):

  • AX — общего назначения (аккумулятор);
  • BX — общего назначения (адрес);
  • CX — общего назначения (счетчик);
  • DX — общего назначения (расширяет
    AX до 32 бит);
  • SI — общего назначения (адрес источника);
  • DI — общего назначения (адрес приемника);
  • BP — указатель базы (обычно адресует переменные, хранимые на стеке);
  • SP — указатель стека.

Несмотря на то что у каждого регистра есть типичный способ применения, ты можешь использовать их как заблагорассудится. Четыре первых регистра —
AX,
BX,
CX и
DX — при желании можно использовать не полностью, а половинками по 8 бит (старшая
H и младшая
L):
AH,
BH,
CH,
DH и
AL,
BL,
CL,
DL. Например, если запишешь в
AX число
0x77AA (
mov ax, 0x77AA), то в
AH попадет
0x77, в
AL
0xAA.

С теорией пока закончили. Давай теперь подготовим рабочее место и напишем программу «Hello, world!», чтобы понять, как эта теория работает вживую.

Подготовка рабочего места

  1. Скачай компилятор NASM с www.nasm.us. Обрати внимание, он работает на всех современных ОС: Windows 10, Linux, macOS. Распакуй NASM в какую-нибудь папку. Чем ближе папка к корню, тем удобней. У меня это
    c:nasm (я работаю в Windows). Если у тебя Linux или macOS, можешь создать папку
    nasm в своей домашней директории.
  2. Тебе надо как-то редактировать исходный код. Ты можешь пользоваться любым текстовым редактором, который тебе по душе: Emacs, Vim, Notepad, Notepad++ — сойдет любой. Лично мне нравится редактор, встроенный в Far Manager, с плагином Colorer.
  3. Чтобы в современных ОС запускать программы, написанные для 8088, и проверять, как они работают, тебе понадобится DOSBox или VirtualBox.

Написание, компиляция и запуск программы «Hello, world!»

Сейчас ты напишешь свою первую программу на ассемблере. Назови ее как хочешь (например,
first.asm) и скопируй в папку, где установлен
nasm.

Основы ассемблера. Hello World

Если тебе непонятно, что тут написано, — не переживай. Пока просто постарайся привыкнуть к ассемблерному коду, пощупать его пальцами. Чуть ниже я все объясню. Плюс студенческая мудрость гласит: «Тебе что-то непонятно? Перечитай и перепиши несколько раз. Сначала непонятное станет привычным, а затем привычное — понятным».

Теперь запусти командную строку, в Windows это cmd.exe. Потом зайди в папку
nasm и скомпилируй программу, используя вот такую команду:

nasm f bin first.asm o first.com

Если ты все сделал правильно, программа должна скомпилироваться без ошибок и в командной строке не появится никаких сообщений.
NASM просто создаст файл
first.com и завершится.

Чтобы запустить этот файл в современной ОС, открой DOSBox и введи туда вот такие три команды:

Само собой, вместо
c:nasm тебе надо написать ту папку, куда ты скопировал компилятор. Если ты все сделал правильно, в консоли появится сообщение «Hello, world!».

Основы ассемблера. Hello World

Инструкции, директивы

В нашей с тобой программе есть только три вещи: инструкции, директивы и метки.

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

Директивы (в нашей программе их две:
org и
db) — это распоряжения, которые ты даешь компилятору. Каждая отдельно взятая директива говорит компилятору, что на этапе ассемблирования нужно сделать такое-то действие. В машинный код директива не переводится, но она влияет на то, каким образом будет сгенерирован машинный код.

Директива
org говорит компилятору, что все инструкции, которые последуют дальше, надо размещать не в начале сегмента кода, а отступить от начала столько-то байтов (в нашем случае 0x0100).

Директива
db сообщает компилятору, что в коде нужно разместить цепочку байтов. Здесь мы перечисляем через запятую, что туда вставить. Это может быть либо строка (в кавычках), либо символ (в апострофах), либо просто число.

В нашем случае:
db «Hello, world», ‘!’, 0.

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

Метки, условные и безусловные переходы

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

Что значит «прыгать из других мест программы»? В норме процессор выполняет инструкции последовательно, одну за другой. Но если тебе надо организовать ветвление (условие или цикл), ты можешь задействовать инструкцию перехода. Прыгать можно как вперед от текущей инструкции, так и назад.

РЕКОМЕНДУЕМ:
Язык программирования Ада

У тебя в распоряжении есть одна инструкция безусловного перехода (
jmp) и штук двадцать инструкций условного перехода.

В нашей программе задействованы две инструкции перехода:
je и
jmp. Первая выполняет условный переход (Jump if Equal — прыгнуть, если равно), вторая (Jump) — безусловный. С их помощью мы организовали цикл.

Обрати внимание: метки начинаются либо с буквы, либо со знака подчеркивания, либо со знака собаки. Цифры вставлять тоже можно, но только не в начало. В конце метки обязательно ставится двоеточие.

Комментарии, алгоритм, выбор регистров

Итак, в нашей программе есть только три вещи: инструкции, директивы и метки. Но там могла бы быть и еще одна важная вещь: комментарии. С ними читать исходный код намного проще.

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

Основы ассемблера. Комментарии, алгоритм, выбор регистров

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

  1. Поместить в
    BX адрес строки.
  2. Поместить в
    AL очередную букву из строки.
  3. Если вместо буквы там 0, выходим из программы — переходим на 6-й шаг.
  4. Выводим букву на экран.
  5. Повторяем со второго шага.
  6. Конец.

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

Взаимодействие с пользователем: получение данных с клавиатуры

От программ, которые не могут взаимодействовать с пользователем, толку мало. Так что смотри, как можно считывать данные с клавиатуры. Сохрани вот этот код как
second.asm.

Основы ассемблера. Получение данных с клавиатуры

Потом иди в командную строку и скомпилируй его в NASM:

nasm f bin second.asm o second.com

Затем запусти скомпилированную программу в DOSBox:

Как работает программа? Две строки после метки
@@start вызывают функцию BIOS, которая считывает символы с клавиатуры. Она ждет, когда пользователь нажмет какую-нибудь клавишу, и затем кладет ASCII-код полученного значения в регистр
AL. Например, если нажмешь заглавную
A, в
AL попадет
0x41, а если строчную
A
0x61.

Дальше смотрим: если нажата клавиша с кодом 0x1B (клавиша ESC), то выходим из программы. Если же нажата не ESC, вызываем ту же функцию, что и в предыдущей программе, чтобы показать символ на экране. После того как покажем — прыгаем в начало (
jmp):
start.

Обрати внимание, инструкция
cmp (от слова compare — сравнить) выполняет сравнение, инструкция
je (Jump if Equal) — прыжок в конец программы.

Полезные мелочи: просмотр машинного кода, автоматизация компиляции

Если тебе интересно, в какой машинный код преобразуются инструкции программы, скомпилируй исходник вот таким вот образом (добавь опцию
l):

nasm f bin second.asm l second.lst o second.com

Тогда NASM создаст не только исполняемый файл, но еще и листинг:
second.lst. Листинг будет выглядеть как-то так.

Основы ассемблера. Просмотр машинного кода, автоматизация компиляции

Еще тебе наверняка уже надоело при каждом компилировании вколачивать в командную строку длинную последовательность одних и тех же букв. Если ты используешь Windows, можешь создать батник (например,
m.bat) и вставить в него вот такой текст.

Основы ассемблера. Автоматизация компиляции

Теперь ты можешь компилировать свою программу вот так:

Само собой, вместо
first ты можешь подставить любое имя файла.

Выводы

Итак, ты теперь знаешь, как написать простейшую программу на ассемблере, как ее скомпилировать, какие инструменты для этого нужны. Конечно, прочитав одну статью, ты не станешь опытным программистом на ассемблере. Чтобы придумать и написать на нем что-то стоящее — вроде Floppy Bird и «МикроБ», которые написал я, — тебе предстоит еще много пройти. Но первый шаг в эту сторону ты уже сделал.

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (2 оценок, среднее: 5,00 из 5)

Загрузка…

Я давно хотел научиться писать операционную систему или хотя бы попробовать эту задачу на зуб. Почему ОС начинают писать на языке ассемблера? Дело в том, что работа ОС обеспечивается определенными особенностями архитектуры процессора. Это, например, наличие нескольких уровней привилегий (режим ядра, режим пользователя), поддержка виртуальной памяти, поддержка многозадачности. Работа с этими особенностями архитектуры процессора подразумевает использование таких машинных команд и регистров процессора, о которых компиляторы высокоуровневых языков программирования (например C/C++) ничего не знают. Поэтому — ассемблер. В следующей серии заметок я буду писать о своих опытах исследования архитектуры процессоров Intel как то: работа в реальном режиме, переход в защищенный режим, написание загрузчика (bootloader), обработка прерываний, включение механизма виртуальной памяти и пр.

В Интернете я нашел несколько хороших ресурсов по теме:

  • Руслан Аблязов — Программирование на ассемблере на платформе х86-64 — 2011
  • Intel 64 and IA-32 Architectures Software Developer’s Manual
  • BrokenThorn OS Development Series
  • OSDev.org
  • OSDever.net

Кроме того, если вы не знакомы с языком ассемблера для процессоров семейства x86, рекомендую вам книгу Kip Irvine — Assembly Language for x86 Processors, 7th edition — 2014.

Начнем с того, что напишем программу HelloWorld, запускающуюся на голом железе (без операционной системы) и запустим ее на виртуальной машине.

Программа HelloWorld

Ниже показан исходный код программы HelloWorld на языке ассемблера Flat Assembler (FASM), которая будет стартовать на голом железе в реальном режиме работы x86-совместимого процессора (комментарии пишу на английском — привычка, которая возникла у меня в связи с проблемами с кодировками кириллицы).

; HelloWorld real mode
use16               ; generate 16-bit code
org 7C00h           ; the code starts at 0x7C00 memory address

start:
    jmp far dword 0x0000:entr ; makes CS=0, IP=0x7c05
entr:
    xor  ax, ax     ; ax = 0
    mov  ds, ax     ; setup data segment ds=ax=0
    cli             ; when we set up stack we need disable interrupts because stack is involved in interrupts handling
    mov  ss, ax     ; setup stack segment ss=ax=0
    mov  sp, 0x7C00 ; stack will grow starting from 0x7C00 memory address
    sti             ; enable interrupts

 
    mov  si, message
    cld             ; clear direction flag (DF is a flag used for string operations)
    mov  ah, 0Eh    ; BIOS function index (write a charachter to the active video page)
puts_loop:
    lodsb           ; load to al the next charachter located at [si] address in memory (si is incremented automatically because the direction flag DF = 0)
    test al, al     ; zero in al denotes the end of the string
    jz   puts_loop_exit
    int  10h        ; call BIOS standard video service’s function
    jmp  puts_loop
puts_loop_exit:
    cli             ; disable interrupts before halting the processor
    hlt             ; halt the processor
    ;jmp  $         ; alternatively to hlt we could run an infinite loop

 
message db ‘Hello World!’, 0
finish:
    ; The size of a disk sector is 512 bytes. Boot sector signature occupies the two last bytes.
    ; The gap between the end of the source code and the boot sector signature is filled with zeroes.
    times 510finish+start db 0
    db 55h, 0AAh    ; boot sector signature

Компиляция исходного кода

Объяснения того, как работает программа — потом, сначала давайте скомпилируем исходный код. Компилировать будем ассемблером Flat Assembler (FASM). Скачиваем с официального сайта архив с дистрибутивом FASM. Внутри архива находится компилятор fasm.exe. Я рекомендовал бы распаковать архив в папку C:FASM и добавить путь к этой папке в переменную окружения PATH. Далее запускаем командную строку и компилируем исходный код:

fasm HelloWorld.asm HelloWorld.bin

HelloWorld.asm — это приведенный выше файл с исходным кодом, HelloWorld.bin — это скомпилированный машинный код.

Создание образа дискеты

Операционная система должна загружаться с какого-то носителя (жесткого диска, CD-ROM, флешки, дискеты и т. д.). Создадим образ загрузочной дискеты с нашим машинным кодом. Виртуальная машина сможет загрузиться с виртуальной дискеты, используя этот образ. Создавать образ дискеты умеет unix’овая утилита под названием dd. Существует версия этой утилиты под Windows. Если вы пользуетесь средой Cygwin, то утилита dd есть в ее составе. Итак, запускаем командную строку:

dd if=»/dev/zero» of=»floppy.img» bs=1024 count=1440
dd if=»HelloWorld.bin» of=»floppy.img» conv=notrunc

1-я команда создает образ дискеты floppy.img и заполняет его нулями, 2-я — записывает в самое начало образа нашу программу.

Установка и запуск виртуальной машины Bochs

Я имел дело всего с тремя виртуальными машинами: Oracle VM VirtualBox, VMware Workstation Player и Bochs. Bochs хотя и обладает очень скромным графическим интерфейсом, хорош тем, что он легкий и может выполнять машинный код пошагово, т. е. с ним можно производить отладку исходного кода программы. Скачиваем с официального сайта программу установки (файл с расширением .exe) и запускаем его (все настройки я оставлял по-умолчанию). После установки запускаем Bochs. Возникает диалоговое окно Bochs Start Menu. В списке Edit Options выбираем Disk & Boot и нажимаем кнопку Edit. Открывается диалог Bochs Disk Options. На вкладке Floppy Options в группе First Floppy Drive устанавливаем следующие настройки:

Type of floppy drive 3.5 1.44M
First floppy image/device жмем Browse и выбираем ранее созданный нами файл floppy.img
Type of floppy media 1.44M
Write Protection галка снята
Status inserted

Жмем кнопку OK. В окне Bochs Start Menu жмем кнопку Start (предварительно можно сохранить сделанные нами настройки в текстовом файле с расширением .bxrc нажав кнопку Save, впоследствии их можно будет загрузить, нажав кнопку Load). Открывается окно, в котором мы видим надпись

Особенности FASM

Прежде всего обратите внимание на документацию Flat Assembler Documentation and Tutorials, которая также поставляется в дистрибутиве FASM в виде файла PDF. Синтаксис FASM имеет особенности, которые отличают его например от MASM, которым мне доводилось пользоваться до сих пор. Особенности касаются обращений к памяти и работы с метками:

MASM FASM

metka dword 1234h

metka dd 1234h

mov eax, metka

mov eax, [metka]
mov eax, ptr metka

mov eax, offset metka

mov eax, metka

mov ax, word ptr metka

mov ax, word ptr metka
mov ax, word [metka]

Объяснение исходного кода программы HelloWorld

Теперь в двух словах о том, как работает программа HelloWorld. Когда процессор выполняет нашу программу, он считывает машинные команды из оперативной памяти. Откуда в памяти возьмется наша программа? Ответ: с одного из носителей, коими могут быть жесткий диск, дискета, флешка, компакт-диск или локальная сеть. Кто загрузит программу с носителя в оперативную память? Ответ: процессор, который сразу после включения начинает выполнять программу, записанную в BIOS — микросхеме памяти, которая хранит программу, которая проецируется на адресное пространство процессора и которая заставляет процессор перебирать все носители и искать на них т. н. загрузочный сектор — блок данных размером 512 байт, в последних двух байтах которого содержится т. н. сигнатура загрузочного сектора. Найдя загрузочный сектор, процессор копирует его с носителя в оперативную память по адресу 0x7C00 и переходит к выполнению машинной инструкции, расположенной по этому адресу. Чтобы рассчитать адреса меток, компилятор должен знать, по какому адресу будет расположена наша программа — для этого мы используем директиву

Заметим, что директива org никак не влияет на расположение кода внутри двоичного файла, она влияет только на адреса, на которые указывают метки.
Чтобы поместить в последние два байта (всего в секторе 512 байт) сектора сигнатуру загрузочного сектора (байты 55h, AAh), используем в конце файла директиву times:

finish:
    times 510finish+start db 0
    db 55h, 0AAh ; boot sector signature

После включения процессор работает в 16-разрядном режиме (т. н. реальный режим работы процессора). Две ассемблерные команды, имеющие одно и то же мнемоническое обозначение, будут иметь разные машинный код в зависимости от режима работы процессора (режимов на сегодняшний день существует три: реальный (16-разрядный), защищенный (32-разрядный) и длинный (64-разрядный)). Поэтому компилятор должен знать, какой машинный код ему генерировать, 16-, 32- или 64-разрядный — для этого мы используем директиву

Настройка сегментных регистров:

start:
    jmp far dword 0x0000:entr ; makes CS=0, IP=0x7c00
entr:
    xor  ax, ax     ; ax = 0
    mov  ds, ax     ; setup data segment ds=ax=0
    mov  ss, ax     ; setup stack segment ss=ax=0
    mov  sp, 0x7C00 ; stack will grow starting from 0x7C00 memory address

Зачем помещать нули в сегментные регистры? Дело в том, что мы не знаем, каковы значения этих регистров в момент запуска программы. Единственное, что мы знаем — это что наша программа будет загружена по адресу 0x7C00 и что первая машинная команда будет выполнена. В то же время, наша программа рассчитана на то, что в сегментных регистрах cs и ds будут нули, т. е. все метки в программе обозначают смещения относительного базового адреса, равного нулю. Стек мы пока не используем, но это пока — нелишним будет проинициализировать и регистры ss и sp.

Как вывести текстовую строку на экран. После старта процессора в реальном режиме в оперативную память из BIOS загружена таблица прерываний и загружены обработчики прерываний. Среди них есть программные прерывания — те, обработчики которых можно вызвать командой int. Эти прерывания по сути являются функциями, которые BIOS предоставляет нашей программе, и эти функции позволяют осуществлять ввод-вывод, в том числе на экран компьютера. Функции BIOS могут принимать параметры через регистры. Например функция int 10h принимает два параметра: параметр ah=0Eh уточняет задачу функции — «записать символ в видеопамять»; параметр al — ASCII-код символа, который надо записать в видеопамять. И мы последовательно в цикле помещаем в регистр al символы строки «Hello World!» и вызываем прерывание int 10h:

puts_loop:
    lodsb           ; load to al the next charachter located at [si] address in memory (si is incremented automatically because direction flag DF = 0)
    test al, al     ; zero in al means the end of the string
    jz   puts_loop_exit
    int  10h        ; call BIOS standard video service’s function
    jmp  puts_loop
puts_loop_exit:

Некоторые функции BIOS приведены на странице OsDev.org — BIOS.
Заканчивается программа инструкцией hlt, которая останавливает работу процессора. Из этого состояния он может быть выведен только прерыванием (как немаскируемым, так и маскируемым) или перезагрузкой (reset).

В следующей заметке я расскажу о настройке проекта osdevlearning, который размещен на BitBucket, и в который я буду помещать наши эксперименты по изучению архитектуры Intel x86-64.

Понравилась статья? Поделить с друзьями:
  • Дельта знак как найти
  • Как найти свой дискорд айди
  • Как найти давно удаленные файлы
  • Как найти косинус угла абс треугольнике
  • Телеграм как найти закрепленное сообщение