company_banner

Пишем загрузчик на Ассемблере и C. Часть 1

Автор оригинала: Ashakiran Bhatter
  • Перевод
  • Tutorial


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

О чем пойдет речь?


Мы рассмотрим написание кода программы и его копирование в загрузочный сектор образа флоппи-диска, после чего с помощью эмулятора bochs (x86) для Linux научимся проверять работоспособность полученной дискеты с загрузчиком.

О чем речь не пойдет


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

Структура статьи


Начнем мы со знакомства с основами, после чего перейдем к написанию самого кода. В целом план будет такой:

• Знакомство с загрузочными устройствами.
• Знакомсто со средой разработки.
• Знакомство с микропроцессором.
• Написание кода на ассемблере.
• Написание кода на С.
• Создание мини-программы для отображения прямоугольников.

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

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

Знакомство с загрузочными устройствами


Что происходит при включении стандартного компьютера?


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

После запуска BIOS выполняет следующие задачи:
• Тестирование оборудования при подаче питания (Power On self Test).
• Проверка частоты и доступности шин.
• Проверка системных часов и аппаратной информации в CMOS RAM.
• Проверка настроек системы, предустановок оборудования и т.д.
• Тестирование подключенного оборудования, начиная с RAM, дисководов, оптических приводов, HDD и т.д.
• В зависимости от определенной в разделе загрузочных устройств информации выполняет поиск загрузочного диска и переходит к его инициализации.

К сведению: все ЦПУ с архитектурой x86 в процессе загрузки запускаются в реальном режиме (Real Mode).

Что такое загрузочное устройство?


Загрузочным называется устройство, содержащее загрузочный сектор или блок загрузки. BIOS считывает это устройство, начиная с загрузки этого загрузочного сектора в RAM для выполнения, после чего переходит далее.

Что такое сектор?


Сектор – это особый раздел загрузочного диска, размер которого обычно составляет 512 байт. Чуть позже я подробнее поясню о том, как измеряется память компьютера и приведу сопутствующую терминологию.

Что такое загрузочный сектор?


Загрузочный сектор или блок загрузки – это область загрузочного устройства, в которой содержится загружаемый в RAM машинный код, за что отвечает встроенная в ПК прошивка на стадии инициализации. На флоппи-диске размер сектора составляет 512 байт. Чуть позже о байтах будет сказано дополнительно.

Как работает загрузочное устройство?


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

Что такое начальный загрузчик?


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

Какие есть виды микропроцессоров?


Я приведу основные:
• 16 битные
• 32 битные
• 64 битные

Чем больше значение бит, тем к большему объему памяти имеют доступ программы, получая большую производительность в плане временного хранилища, обработки и пр. На сегодня микропроцессоры производят две основные компании – Intel и AMD. В этой же статьи я буду обращаться только к процессорам семейства Intel (x86).

В чем отличие процессоров Intel и AMD?


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

Знакомство со средой разработки


Что такое реальный режим?


Я уже упоминал, что все процессоры с архитектурой x86 при загрузке с устройства запускаются в реальном режиме. Это очень важно иметь в виду при написании загрузочного кода для любого устройства. Реальный режим поддерживает только 16-битные инструкции. Поэтому создаваемый вами код для загрузки в загрузочную запись или сектор должен компилироваться в 16-битный формат. В реальном режиме инструкции могут работать только с 16 битами одновременно. Например, в 16-битном ЦПУ конкретная инструкция будет способна складывать в одном цикле два 16-битных числа. Если же для процесса будет необходимо сложить два 32-битных числа, то потребуется больше циклов, выполняющих сложение 16-битных чисел.

Что такое набор инструкций?


Это гетерогенная коллекция сущностей, ориентированных на конкретную архитектуру микропроцессора, с помощью которых пользователь может взаимодействовать с ним. Здесь я подразумеваю коллекцию сущностей, состоящую из внутренних типов данных, инструкций, регистров, режимов адресации, архитектуры памяти, обработки прерываний и исключений, а также внешнего I/O. Обычно для семейства микропроцессоров создаются общие наборы инструкций. Процессор Intel-8086 относится к семейству 8086, 80286, 80386, 80486, Pentium, Pentium I, II, III, которое также известно как семейство x86. В этой статье я будут использовать набор инструкций, относящийся именно к этому типу процессоров.

Как написать код для загрузочного сектора устройства?


Для реализации этой задачи необходимо иметь представление о:
• Операционной системе (GNU Linux).
• Ассемблере (GNU Assembler).
• Наборе инструкций (x86).
• Написании инструкций на GNU Assembler для x86 микропроцессоров.
• Компиляторе (как вариант язык C).
• Компоновщике (GNU linker ld)
• Эмуляторе x86, например bochs, используемом для тестирования.

Что такое операционная система?


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

Отдельно отмечу, что все современные ОС работают в защищенном режиме.
Какие виды ОС бывают?
• Windows
• Linux
• MAC
• …

Что значит защищенный режим?


В отличие от реального режима, защищенный поддерживает 32-битные инструкции. Но вам об этом задумываться не стоит, так как нас не особо волнует процесс функционирования ОС.

Что такое Ассемблер?


Ассемблер преобразует передаваемые пользователем инструкции в машинный код.

Разве компилятор делает не то же самое?


На более высоком уровне да, но, фактически, внутри компилятора этим занимается именно ассемблер.

Почему компилятор не может генерировать машинный код напрямую?


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

Зачем нужна ОС для написания кода загрузочного сектора?


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

Какую ОС можно использовать?


Так как я писал загрузочные программы под Ubuntu, то и вам для ознакомления с данным руководством порекомендую именно эту ОС.

Какой следует использовать компилятор?


Я писал загрузчики при помощи GNU GCC и демонстрировать компиляцию кода я буду на нем же. Как протестировать рукописный код для загрузочного сектора? Я представлю вам эмулятор архитектуры x86, который помогает дорабатывать код, не требуя постоянной перезагрузки компьютера при редактировании загрузочного сектора устройства.

Знакомство с микропроцессором


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

Что такое регистры?


Регистры подобны утилитам микропроцессора, служащим для временного хранения данных и управления ими согласно нашим потребностям. Предположим, пользователь задает операцию сложения 2 и 3, для чего компьютер сохраняет число 3 в одном регистре, а 2 в другом, после чего складывает содержимое этих регистров. В итоге ЦПУ помещает результат в еще один регистр, который и представляет нужный пользователю вывод. Регистры разделяются на четыре основных типа:

• регистры общего назначения;
• сегментные регистры;
• индексные регистры;
• регистры стека.

Я дам краткое пояснение по каждому типу.

Регистры общего назначения используются для хранения временных данных, необходимых программе в процессе выполнения. Каждый такой регистр имеет емксоть 16 бит или 2 байта.
• AX – регистр сумматора;
• BX – регистр базового адреса;
• CX – регистр-счетчик;
• DX – регистр данных.

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

Пример: у нас есть байт, представляющий значение “X” и расположенный в 10-й позиции от начала блока памяти со стартовым адресом 0x7c00. В данной ситуации мы выразим сегмент как 0x7c00, а смещение как 10.
Абсолютным адресом тогда будет 0x7c00 + 10.

Здесь я хочу выделить четыре категории:
• CS – сегмент кода;
• SS – сегмент стека;
• DS – сегмент данных;
• ES – расширенный сегмент.

При этом нужно учитывать ограничения этих регистров, а именно невозможность прямого присваивания адреса. Вместо этого нам приходится копировать адрес сначала в регистры общего назначения, после чего снова копировать его уже в сегментные. Например, для решения задачи обнаружения байта “X” мы делаем следующее:

movw $0x07c0, %ax
movw %ax    , %ds
movw (0x0A) , %ax 


Здесь происходит:

• загрузка значения 0x07c0 * 16 в AX;
• загрузка содержимого AX в DS;
• установка 0x7c00 + 0x0a в AX.

Регистры стека:
• BP – базовый указатель;
• SP – указатель стека.

Индексные регистры:
• SI: регистр индекса источника.
• DI: регистр индекса получателя.
• AX: используется ЦПУ для арифметических операций.
• BX: может содержать адрес процедуры или переменной (это также могут SI, DI и BP) и использоваться для выполнения арифметических операций и перемещения данных.
• CX: выступает в роли счетчика цикла при повторении инструкций.
• DX: содержит старшие 16 бит произведения при умножении, а также задействуется при делении.
• CS: содержит базовый адрес всех выполняемых инструкций программы.
• SS: содержит базовый адрес стека.
• DS: содержит предустановленный адрес переменных.
• ES: содержит дополнительный базовый адрес переменных памяти.
• BP: содержит предполагаемое смещение из регистра SS. Часто используется подпрограммами для обнаружения переменных, переданных в стек вызывающей программой.
• SP: содержит смещение вершины стека.
• SI: используется в инструкциях перемещения строк. При этом на исходную строку указывает регистр SI.
• DI: выступает в роли места назначения для инструкций перемещения строк.

Что такое бит?


В вычислительных средах бит является наименьшей единицей данных, представляющей их в двоичном формате, где 1 = да, а 0 = нет.

Дополнительно о регистрах:

Ниже описано дальнейшее подразделение регистров:
• AX: первые 8 бит AX обозначаются как AL, последние 8 бит как AH.
• BX: первые 8 бит BX обозначаются как BL, последние 8 как как BH.
• CX: первые 8 бит CX обозначаются как CL, последние 8 бит как CH.
• DX: первые 8 бит DX обозначаются как DL, последние 8 бит как DH.

Как обращаться к функциям BIOS?


BIOS предоставляет ряд функций, позволяющих распределять приоритеты ЦПУ. Доступ к этим возможностям BIOS можно получить с помощью прерываний.

Что такое прерывания?


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

Какое прерывание будем использовать мы?


Прерывание INT 0x10.

Написание кода на Ассемблере


Какие типы данных доступны в GNU Assembler?


Типы данных определяют их характеристики и могут быть следующими:

• байт;
• слово;
• Int;
• ASCII;
• ASCIIZ.

Байт: состоит из восьми бит и считается наименьшей единицей хранения информации при программировании.
Слово: единица данных, состоящая из 16 бит.

Int: целочисленный тип данных, состоящий из 32 бит, которые могут быть представлены четырьмя байтами или двумя словами.
Прим. Справедливости ради, стоит отметить, что размер Int зависит от архитектуры и может составлять от 16 до 64 бит (а на некоторых системах даже 8 бит). То, очем говорит автор — это тип long. Подробнее о типах С можно прочесть по ссылке.

ASCII: представляет группу байтов без нулевого символа.

ASCIIZ: выражает группу байтов, завершающуюся нулевым символом.

Как генерировать код для реального режима в Ассемблере?


В процессе запуска ЦПУ в реальном режиме (16 бит) мы можем задействовать только встроенные функции BIOS. Я имею в виду, что с помощью этих функций можно написать собственный код загрузчика, поместить его в загрузочный сектор и выполнить загрузку. Давайте рассмотрим написание на Ассемблере небольшого фрагмента программы для генерации 16-битного кода ЦПУ через GNU Assembler.

Файл-образец: test.S

.code16                   #генерирует 16-битный код
.text                     #расположение исполняемого кода
     .globl _start;
_start:                   #точка входа
     . = _start + 510     #перемещение из позиции 0 к 510-му байту 
     .byte 0x55           #добавление сигнатуры загрузки
     .byte 0xaa           #добавление сигнатуры загрузки


Пояснения:

.code16: это директива, отдаваемая ассемблеру для генерации не 32-, а 16-битного кода. Зачем это нужно? Ассемблер вы будете использовать через операционную систему, а код загрузчика будете писать с помощью компилятора. Но вы также наверняка помните, что ОС работает в защищенном 32-битном режиме. Поэтому, по умолчанию ассемблер в такой ОС будет производить 32-битный код, что не соответствует нашей задаче. Данная же директива исправляет этот нюанс, и мы получаем 16-битный код.
.text: этот раздел содержит фактические машинные инструкции, составляющие вашу программу.
.globl _start: .global <символ> делает символ видимым для компоновщика. При определении символа в подпрограмме его значение становится доступным для других связанных подпрограмм. Иначе говоря, символ получает атрибуты от символа с таким же именем, находящегося в другом файле, связанном с этой программой.
_start: точка входа в основной код, а также предустановленная точка входа для компоновщика.
= _start + 510: переход от начальной позиции к 510-му байту.
.byte 0x55: первый байт, определяемый как часть сигнатуры загрузки (511-й байт).
.byte 0xaa: последний байт, определяемый как часть сигнатуры загрузки (512-й байт).

Как скомпилировать программу ассемблера?


Сохраните код в файле test.S и введите в командной строке:

as test.S -o test.o
ld –Ttext 0x7c00 --oformat=binary test.o –o test.bin

Что означают эти команды?


as test.S –o test.o: преобразует заданный код в промежуточную объектную программу, которая затем преобразуется уже в машинный код.
--oformat=binary сообщает компоновщику, что выходной двоичный файл должен быть простым двоичным образом, т.е. не иметь кода запуска, связывания адресов и пр.
–Ttext 0x7c00 сообщает компоновщику, что для вычисления абсолютного адреса нужно загрузить адрес “text” (сегмент кода) в 0x7c00.

Что такое сигнатура загрузки?


Давайте вспомним о загрузочном секторе, используемом BIOS для запуска системы, и подумаем, как BIOS узнает о наличии такого сектора на устройстве? Тут нужно пояснить, что состоит он из 512 байт, в которых для 510-го байта ожидается символ 0x55, а для 511-го символ 0xaa. Исходя из этого, BIOS проверяет соответствие двух последний байт загрузочного сектора этим значениям и либо продолжает загрузку, либо сообщает о ее невозможности. При помощи hex-редактора можно просматривать содержимое двоичного файла в более читабельном виде, и ниже в качестве примера я привел снимок этого файла.

Как скопировать исполняемый код на загрузочное устройство и протестировать его?


Чтобы создать образ для дискеты размером 1.4Мб, введите в командную строку следующее:
dd if=/dev/zero of=floppy.img bs=512 count=2880

Чтобы скопировать этот код в загрузочный сектор файла образа, введите:
dd if=test.bin of=floppy.img

Для проверки программы введите:
bochs

Если bochs не установлен, тогда можно ввести следующее:
sudo apt-get install bochs-x

Файл-образец: bochsrc.txt

megs: 32
#romimage: file=/usr/local/bochs/1.4.1/BIOS-bochs-latest, address=0xf0000
#vgaromimage: /usr/local/bochs/1.4.1/VGABIOS-elpin-2.40
floppya: 1_44=floppy.img, status=inserted
boot: a
log: bochsout.txt
mouse: enabled=0 


В результате должно отобразиться стандартное окно эмуляции bochs:



Просмотр:
Если теперь заглянуть в файл test.bin через hex-редактор, то вы увидите, что сигнатура загрузки находится после 510-го байта:



Пока что вы просто увидите сообщение “Booting from Floppy”, так как в коде мы еще ничего не прописали. Давайте рассмотрим пару других примеров создания кода на ассемблере.

Файл-образец: test2.S

.code16                    #генерирует 16-битный код
.text                      #расположение исполняемого кода
     .globl _start;
_start:                    #точка входа

     movb $'X' , %al       #выводимый символ
     movb $0x0e, %ah       #выводимый служебный код bios
     int  $0x10            #прерывание цпу

     . = _start + 510      #перемещение из позиции 0 к 510-му байту
     .byte 0x55            #добавление сигнатуры загрузки
     .byte 0xaa            #добавление сигнатуры загрузки


После ввода этого кода сохраните его в test2.S и выполните действия согласно прежней инструкции, изменив имя исходного файла. После компиляции, копирования кода в загрузочный сектор и выполнения bochs вы должны увидеть следующий экран, где теперь отображается прописанная нами в коде буква X.



Поздравляю, ваша первая программа в загрузочном секторе работает!

Просмотр:
В hex-редакторе вы увидите, что символ X находится во второй позиции от начального адреса.



Теперь давайте выведем на экран текст побуквенно.

Файл-образец: test3.S

.code16                  #генерирует 16-битный код
.text                    #расположение исполняемого кода
     .globl _start;

_start:                  #точка входа

     #выводит 'H' 
     movb $'H' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит 'e'
     movb $'e' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит 'l'
     movb $'l' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит 'l'
     movb $'l' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит 'o' 
     movb $'o' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит ','
     movb $',' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит ' '
     movb $' ' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит 'W'
     movb $'W' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит'o'
     movb $'o' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит 'r'
     movb $'r' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит 'l'
     movb $'l' , %al
     movb $0x0e, %ah
     int  $0x10

     #выводит 'd'
     movb $'d' , %al
     movb $0x0e, %ah
     int  $0x10

     . = _start + 510    #перемещение из позиции 0 к 510-му байту
     .byte 0x55            #добавление сигнатуры загрузки
     .byte 0xaa            #добавление сигнатуры загрузки


Сохраните файл как test3.S. После компиляции и всех сопутствующих действий перед вами отобразится следующий экран:



Просмотр:



Хорошо. Теперь давайте напишем программу, выводящую на экран фразу “Hello, World”.
При этом мы также определим функции и макросы, с помощью которых и будем выводить эту строку.

Файл-образец: test4.S

#генерирует 16-битный код
.code16
#расположение исполняемого кода
.text
.globl _start;
#точка входа загрузочного кода
_start:
      jmp _boot                           #переход к загрузочному коду
      welcome: .asciz "Hello, World\n\r"  #здесь мы определяем строку

     .macro mWriteString str              #макрос, вызывающий функцию вывода строки
          leaw  \str, %si
          call .writeStringIn
     .endm

     #функция вывода строки
     .writeStringIn:
          lodsb
          orb  %al, %al
          jz   .writeStringOut
          movb $0x0e, %ah
          int  $0x10
          jmp  .writeStringIn
     .writeStringOut:
     ret

_boot:
     mWriteString welcome

     #перемещение от начала к 510-му байту и присоединение сигнатуры загрузки
     . = _start + 510
     .byte 0x55
     .byte 0xaa  


Сохраните файл как test4.S. Теперь после компиляции и всего за ней следующего вы увидите:



Отлично! Если вы поняли все проделанные мной действия и успешно создали аналогичную программу, то я вас поздравляю еще раз!

Просмотр:



Что такое функция?


Функция – это блок кода, имеющий имя и переиспользуемое свойство.

Что такое макрос?


Макрос – это фрагмент кода с присвоенным именем, на место использования которого подставляется содержимое этого макроса.

В чем синтаксическое отличие функции от макроса?


Для вызова функции используется следующий синтаксис:

push <аргумент>
call <имя функции>

А для макроса такой:

macroname <аргумент>

И поскольку синтаксис вызова и применения макроса проще, чем функции, я предпочел использовать в основном коде именно его.

Написание кода в компиляторе С



Что такое C?


С – это язык программирования общего назначения, разработанный сотрудником Bell Labs Деннисом Ритчи в 1969-1973 годах.

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

Что нужно для написания кода на С?


Мы будем использовать компилятор GNU C под названием GCC и разберем написание в нем программы на примере.

Файл-образец: test.c

__asm__(".code16\n");
__asm__("jmpl $0x0000, $main\n");

void main() {
} 


Файл: test.ld

ENTRY(main);
SECTIONS
{
    . = 0x7C00;
    .text : AT(0x7C00)
    {
        *(.text);
    }
    .sig : AT(0x7DFE)
    {
        SHORT(0xaa55);
    }
} 


Для компиляции программы введите в командной строке:

gcc -c -g -Os -march=i686 -ffreestanding -Wall -Werror test.c -o test.o
ld -static -Ttest.ld -nostdlib --nmagic -o test.elf test.o
objcopy -O binary test.elf test.bin

Что значат эти команды?


Первая преобразует код C в промежуточную объектную программу, которая в последствии преобразуется в машинный код.
gcc -c -g -Os -march=i686 -ffreestanding -Wall -Werror test.c -o test.o:

Что значат эти команды?


-c: используется для компиляции исходного кода без линковки.
-g: генерирует отладочную информацию для отладчика GDB.
-Os: оптимизация размера кода.
-march: генерирует код для конкретной архитектуры ЦПУ (в нашем случае i686).
-ffreestanding: в среде отдельных программ может отсутствовать стандартная библиотека, а инструкции запуска программы не обязательно располагаются в “main”.
-Wall: активирует все предупреждающие сообщения компилятора. Рекомендуется всегда использовать эту опцию.
-Werror: активирует трактовку предупреждений как ошибок.
test.c: имя входного исходного файла.
-o: генерация объектного кода.
test.o: имя выходного файла объектного кода.

С помощью всей этой комбинации флагов мы генерируем объектный код, помогающий нам в обнаружении ошибок и предупреждений, а также создаем более эффективный код для данного типа ЦПУ. Если не указать march=i686, будет сгенерирован код для используемой вами машины. В связи с этим нужно указывать, для какого именно типа ЦПУ он создается.

ld -static -Ttest.ld -nostdlib --nmagic test.elf -o test.o:
Эта команда вызывает компоновщик из командной строки, и ниже я поясню, как именно мы его используем.

Что значат эти флаги?


-static: не линковать с общими библиотеками.
-Ttest.ld: разрешить компоновщику следовать командам из его скрипта.
-nostdlib: разрешить компоновщику генерировать код, не линкуя функции запуска стандартной библиотеки C.
--nmagic: разрешить компоновщику генерировать код без фрагментов _start_SECTION и _stop_SECTION.
test.elf: имя выходного файла (соответствующий платформе формат хранения исполняемых файлов. Windows: PE, Linux: ELF)
-o: генерация объектного кода.
test.o: имя входного файла объектного кода.

Что такое компоновщик?


Он выполняет последний этап компиляции. ld (компоновщик) получает один или более объектных файлов либо библиотек и совмещает их в один, как правило, исполняемый файл. В ходе этого процесса он обрабатывает ссылки на внешние символы, присваивает конечные адреса процедурам/функциям и переменным, а также корректирует код и данные для отражения актуальных адресов.
Не забывайте, что мы не используем в коде стандартные библиотеки и сложные функции.

objcopy -O binary test.elf test.bin
Эта команда служит для генерации независимого от платформы кода. Обратите внимание, что в Linux исполняемые файлы хранятся не так, как в Windows. В каждой системе свой способ хранения, но мы создаем всего-навсего небольшой загрузочный код, который на данный момент не зависит от ОС.

Зачем в программе C использовать инструкции ассемблера?


В реальном режиме к функциям BIOS можно легко обратиться через прерывания при помощи именно инструкций ассемблера.

Как скопировать код на загрузочное устройство и проверить его?


Чтобы создать образ для дискеты размером 1.4Мб, введите в командную строку:

dd if=/dev/zero of=floppy.img bs=512 count=2880
Чтобы скопировать код в загрузочный сектор файла образа, введите:
dd if=test.bin of=floppy.img
Для проверки программы введите:
bochs

Должно отобразиться стандартное окно эмуляции:



Что мы видим: как и в первом нашем примере, пока что здесь отображается только сообщение “Booting from Floppy”.

Для вложения инструкция ассемблера в программу C мы используем ключевое слово __asm__.
• Дополнительно мы задействуем __volatile__, указывая компилятору, что код нужно оставить как есть, без изменений.

Такой способ вложения называется встраивание ассемблерного кода.
Рассмотрим еще несколько примеров написания с помощью компилятора.

Пишем программу для вывода на экран ‘X’


Файл-образец: test2.c

__asm__(".code16\n");
__asm__("jmpl $0x0000, $main\n");

void main() {
     __asm__ __volatile__ ("movb $'X'  , %al\n");
     __asm__ __volatile__ ("movb $0x0e, %ah\n");
     __asm__ __volatile__ ("int $0x10\n");
}


Написав код, сохраните файл как test2.c и скомпилируйте его согласно все тем же инструкциям, изменив исходное имя. После компиляции, копирования кода в загрузочный сектор и выполнения команды bochs вы снова увидите экран, где отображается буква X:



Теперь напишем код для показа фразы “Hello, World”


Для вывода данной строки мы также определим функции и макросы.

Файл-образец: test3.c

/* генерирует 16-битный код */
__asm__(".code16\n");
/* переходит к точке входа загрузочного кода */
__asm__("jmpl $0x0000, $main\n");

void main() {
     /* выводит 'H' */
     __asm__ __volatile__("movb $'H' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит 'e' */
     __asm__ __volatile__("movb $'e' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит 'l' */
     __asm__ __volatile__("movb $'l' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит 'l' */
     __asm__ __volatile__("movb $'l' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит 'o' */
     __asm__ __volatile__("movb $'o' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит ',' */
     __asm__ __volatile__("movb $',' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит ' ' */
     __asm__ __volatile__("movb $' ' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит 'W' */
     __asm__ __volatile__("movb $'W' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит 'o' */
     __asm__ __volatile__("movb $'o' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит 'r' */
     __asm__ __volatile__("movb $'r' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит 'l' */
     __asm__ __volatile__("movb $'l' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");

     /* выводит 'd' */
     __asm__ __volatile__("movb $'d' , %al\n");
     __asm__ __volatile__("movb $0x0e, %ah\n");
     __asm__ __volatile__("int  $0x10\n");
}


Сохраните этот код в файле test3.c и следуйте уже знакомым вам инструкциям, изменив имя исходного файла и скопировав скомпилированный код в загрузочный сектор дискеты. Теперь на этапе проверки должна отобразиться надпись «Hello, World»:



Напишем на C программу для вывода строки “Hello, World”


При этом мы определим функцию, выводящую эту строку на экран.

Файл-образец: test4.c

/*генерирует 16-битный код*/
__asm__(".code16\n");
/*переход к точке входа в загрузочный код*/
__asm__("jmpl $0x0000, $main\n");

/* пользовательская функция для вывода серии знаков, завершаемых нулевым символом*/
void printString(const char* pStr) {
     while(*pStr) {
          __asm__ __volatile__ (
               "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0007)
          );
          ++pStr;
     }
}

void main() {
     /* вызов функции <code>printString</code> со строкой в качестве аргумента*/
     printString("Hello, World");
} 


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



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

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

Мини-проект отображения прямоугольников


Файл-образец: test5.c

/* генерирует 16-битный код                                                 */
__asm__(".code16\n");
/* переход к главной функции или программному коду                                */
__asm__("jmpl $0x0000, $main\n");

#define MAX_COLS     320 /* количество столбцов экрана               */
#define MAX_ROWS     200 /* количество строк экрана                  */

/* функция вывода строки*/
/* input ah = 0x0e*/
/* input al = <выводимый символ>*/
/* прерывание: 0x10*/
/* мы используем прерывание 0x10 с кодом функции 0x0e для вывода байта из al*/
/* эта функция получает в качестве аргумента строку и выводит символ за символом, пока не достигнет нуля*/

void printString(const char* pStr) {
     while(*pStr) {
          __asm__ __volatile__ (
               "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0007)
          );
          ++pStr;
     }
}

/* функция, получающая сигнал о нажатии клавиши на клавиатуре */
/* input ah = 0x00*/
/* input al = 0x00*/
/* прерывание: 0x10*/
/* эта функция регистрирует нажатие пользователем клавиши для продолжения выполнения */
void getch() {
     __asm__ __volatile__ (
          "xorw %ax, %ax\n"
          "int $0x16\n"
     );
}

/* функция вывода на экран цветного пикселя в заданном столбце и строке */
/* входной ah = 0x0c*/
/* входной al = нужный цвет*/
/* входной cx = столбец*/
/* входной dx = строка*/
/* прерывание: 0x10*/
void drawPixel(unsigned char color, int col, int row) {
     __asm__ __volatile__ (
          "int $0x10" : : "a"(0x0c00 | color), "c"(col), "d"(row)
     );
}

/* функции очистки экрана и установки видео-режима 320x200 пикселей*/
/* функция для очистки экрана */
/* входной ah = 0x00 */
/* входной al = 0x03 */
/* прерывание = 0x10 */
/* функция для установки видео режима */
/* входной ah = 0x00 */
/* входной al = 0x13 */
/* прерывание = 0x10 */
void initEnvironment() {
     /* очистка экрана */
     __asm__ __volatile__ (
          "int $0x10" : : "a"(0x03)
     );
     __asm__ __volatile__ (
          "int $0x10" : : "a"(0x0013)
     );
}

/* функция вывода прямоугольников в порядке уменьшения их размера */
/* я выбрал следующую последовательность отрисовки: */
/* из левого верхнего угла в левый нижний, затем в правый нижний, оттуда в верхний правый и в завершении в верхний левый край */
void initGraphics() {
     int i = 0, j = 0;
     int m = 0;
     int cnt1 = 0, cnt2 =0;
     unsigned char color = 10;

     for(;;) {
          if(m < (MAX_ROWS - m)) {
               ++cnt1;
          }
          if(m < (MAX_COLS - m - 3)) {
               ++cnt2;
          }

          if(cnt1 != cnt2) {
               cnt1  = 0;
               cnt2  = 0;
               m     = 0;
               if(++color > 255) color= 0;
          }

          /* верхний левый -> левый нижний */
          j = 0;
          for(i = m; i < MAX_ROWS - m; ++i) {
               drawPixel(color, j+m, i);
          }
          /* левый нижний -> правый нижний */
          for(j = m; j < MAX_COLS - m; ++j) {
               drawPixel(color, j, i);
          }

          /* правый нижний -> правый верхний */
          for(i = MAX_ROWS - m - 1 ; i >= m; --i) {
               drawPixel(color, MAX_COLS - m - 1, i);
          }
          /* правый верхний -> левый верхний */
          for(j = MAX_COLS - m - 1; j >= m; --j) {
               drawPixel(color, j, m);
          }
          m += 6;
          if(++color > 255)  color = 0;
     }
}

/* эта функция является загрузочным кодом и вызывает следующие функции: */
/* вывод на экран сообщения, предлагающего пользователю нажать любую клавишу для продолжения. После нажатия клавиши происходит отрисовка прямоугольников в порядке убывания их размера */
void main() {
     printString("Now in bootloader...hit a key to continue\n\r");
     getch();
     initEnvironment();
     initGraphics();
}


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

Теперь в качестве результата вы увидите:



Нажмите любую клавишу.






Просмотр:
Если внимательно рассмотреть содержимое исполняемого файла, то можно заметить, что в нем практически закончилось свободное пространство. Поскольку размер загрузочного сектора ограничен 512 байтами, мы смогли вместить только несколько функций, а именно выполнить инициализацию среды и вывод цветных прямоугольников. Вот снимок содержимого файла:



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

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

RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

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

    +7

    "Каждый такой регистр имеет 16 бит в ширину и 2 байта в длину."


    Это как?

    +1
    Про линковщик в примере на C:

    • test.elf: имя входного файла (соответствующий платформе формат хранения исполняемых файлов. Windows: PE, Linux: ELF)
    • -o: генерация объектного кода.
    • test.o: имя выходного файла объектного кода.

    слова «входной» и «выходной» надо бы поменять местами.
      +1
      Благодарю за наблюдательность. Ошибка автора, исправил. Будьте добры личными сообщениями в будущем подобные правки присылать. Здесь они только внимание читателей отвлекают.
      +10
      данным и чтения дискет

      20 год на дворе и не слова про uefi, gpt, адресации и прочие блага цивилизации.
        +2
        Это помешает восприятию статьи, так как это совершенно о другом.
          +6
          О ностальгии? я этим занимался 17 лет назад, вспомнил, всплакнул. И кстати, на для рисования на x86 намаплен фреймбуфер начиная с адреса 0xA0000. Сейчас я не уверен, что ноутбуки вообще поддерживают legacy-mode. А MBR не покрывает более 1Tb диска.
            +1
            поправка:
            А MBR не покрывает более 1Tb диска.


            классическая таблица разделов содержит в себе величины DWORD для описания смещений и длин разделов в секторах. 32 бита без знака позволяют описать от 0 до 4 294 967 295 секторов

            При размере сектора 512 байт — это будет ровненько 2ТБ
            беря в расчет менее привычные случаи, например сектор 4096 байт, получим поддерживаемую емкость в 16ТБ.
              0
              Дисков без эмуляции 512к чуть менее чем ноль, так что больше 2ТБ нам не светит.
                0
                Не забывайте про различные USB коробочки, которые вносят свою лепту. И при таком раскладе устройств, где для ОС сектор более 512 байт становится значительно больше.

                А так да, в случае различных 2,5" SATA накопителей очень мало устройств у которых нет эмуляции 512 байт в секторе.
              0
              И кстати, на для рисования на x86 намаплен фреймбуфер начиная с адреса 0xA0000.


              Крайне любопытно, не покидаете ссылочек что почитать?
                0
                Вот раздел Use, например. Если погуглить, то можно найти примеры исходников.
            +5
            20 год на дворе

            21)))
              +1
              Тогда, если верить этой статье, legacy загрузчики больше не будут работать на новом железе.
                0
                Спасибо за ссылку.
            +16

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


            TL;NR


            1. Берете EDK2
            2. Следуете гайду и делаете HelloWorld
            3. Полученный efi переименовываете как bootx64.efi и кладете в /EFI/BOOT на любом fat32 носителе.
            4. ??? грузитесь с него
            5. Profit!

            Распространенные ответы:
            Q: Я просто хотел понять, как это все работает "ближе к железу", без этих ваших выскоуровневых EFI
            A: legacy-режим — это не ближе к железу, а слой совместимости

              +8

              А черт его знает. С одной стороны даже не смешно. В 2021 году не все даже знают о дискетах. И Legacy слой действительно своего рода HAL. С другой стороны браться за x64 не представляя себе работу x86 так себе идея.


              В целом согласен — лучше силы на какой-нить микроконтроллер потратить. С другой стороны современная архитектура x64 это такой монстр, что даже слой UEFI не сильно спасает.


              А вот что статей про "Hello, World" на UEFI, да с графикой действительно не хватает. Больше того — я таких вообще не припомню. А вот про Legacy было очень много. Хотя, мое мнение, по этой части руководства для вирусописателей времён MSDOS и Window 3.11 много полезнее и информативные. Впрочем, они подразумевают знание ассемблера и базовой x86 обвязки, что сегодня уже редкость. Потому какую-то ценность эти статьи имеют. Не стоит, наверное, гнать их в шею. Пусть пока остаются. Мало ли кого реально заинтересует.

                +11
                А интересны ли статьи про потроха относительно современных x86? Многоядерность, старт/стоп процессоров, LAPIC/X2APIC и другие «встроенные» устройства, особенности 64-битного режима и т.д.? С одной стороны, я таких почти не встречал, но может это почти никому и не надо?
                  +3
                  Очень интересно, если есть возможность, пишите!
                    +2

                    Весьма и весьма интересны!

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

                      p.s. про статью подумаю, все никак не выберу тему, мне все знакомые темы кажутся избитыми и неинтересными, но может это потому что я в них и варюсь? :)
                        +2
                        Очень много задач бринг-апа х86 вообще не освещено на русском нигде, совсем. Да тот же, прости рандом, IRQ routing, про который как раз и можно рассказать, заодно рассказав про PIC/APIC/X2APIC, или про MSI (и их связь с SIPI). Про PCIe можно вообще книгу писать, и не одну, то же самое про CPU power management, то же самое про ACPI, и т.п. Вот это все — оно интересное очень, но действительно удел узкой группы людей, и продолжит таковым быть, если не писать про это.
                          0
                          Согласен, много из того, что вы перечислили это достаточно узкая специфика. Поэтому и возникает вопрос — а нужна ли она такая? Все-таки книги писать на эти темы — это требует приличного времени (да даже статью на хабр). Вот вы, наверное, профильный специалист, но много ли вы знаете еще людей разбирающихся на низком уровне хоть как-то в современных x86? Тема совершенно не популярна, про какой-нибудь веб-фреймворк сейчас больше пишут, чем про MSIX или ACPI… А еще при написании надо не задеть ни чьего NDA, что сложно, когда они есть от всех ведущих производителей :)
                            0
                            JerleShannara CodeRush

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

                            Так что пишите!
                            0
                            Вот если наберётся народа, которому это интересно, то можно написать статью про PIC/PIR, APIC, MSI и компанию, правда там гарантировано вылезут vendor specific части, коие я в основном по AMD только подробно знаю.
                            Правда практической ценности от неё будет мало, т.к. PIC уже можно похоронить, да и MSI уже начнает сдавать позиции на фоне PME, хотя девайсы на нулевой виртуально шине ещё живут в APIC спокойно.
                            Хотя когда я ставил эксперименты с различными укуренным вариантами загрузки (типа «мне надо SMP, но чтобы только PIC» или «APIC но без ACPI»), то современные платы весьма часто не могли загрузиться, т.к. такие комбинации уже мертвы. По большому счёту сейчас живы только три варианта: адов легаси одноядерный PIC, многоядерный APIC+ACPI ну и полный фарш.
                              +2
                              Да пишите уже, пофиг, достаточно там народа или нет, народ сам понемногу подтянется из поисковых систем, если ему интересно будет. Понятно, что создание контента — это тяжелый труд, но тут получается известная уловка 22, и разорвать цикл «нет контента» -> «нет читателей» -> «нет смысла писать» -> «нет контента» можно только на этапе «нет смысла писать», выдумав себе этот смысл. Я в свое время три десятка статей так написал (и посмотрите до чего они меня довели!), и очень постараюсь продолжить писать, когда снова время найду и НДА спадут.
                                0
                                Поддерживаю, интересно было бы почитать, даже если и не пригодится в жизни :)
                                0
                                titbit, JerleShannara — я не могу говорить за весь хабр, но за себя скажу.

                                Мое понимание внутреннего устройство ПК закончилось где-то на 386-ом/486-ом. Тогда мне это было нтересно, а начиная с «пентиумов» у меня началось исключительно прикладное использование компьютера. В том смысле, что драйвера для шинных устройств под разные оси — да, а внутреннее устройство BIOS исключительно по необходимости. Не, я в курсе про ACPI (как минимум частично), но объективно мои познания здесь весьма обрывочны и целостной картины (даже в первом приближении) не содержат. Потому я бы с удовольствием почитал. Маловероятно, что мне это понадобится в работе, но… пожалуй именно формат статей на хабре и был бы идеальным форматом для восполнения пробелов. Если вдруг заинтересует всегда можно копнуть глубже. Да и часть важных и интересных моментов из x86/x64 порастеклась. Даже в тот же ARM/aARM64. В первую очередь, конечно, PCI с обвязкой. Потому однозначно интересно.
                          +1
                          Само собой! Я сюда зашёл вспомнить детство и первый резидент на асме, который очень помогал играть в леммингов на Искре. (для создателей раскладки JCUKEN приготовлен отдельный котёл в аду, я уверен! ;-)
                            +1

                            Да норм, мне всего года два-три понадобилось для набора того же темпа на клавиатуре qwerty потом… Зато сейчас посади за jcuken — помру наверное :)

                              0
                              Всё хуже. Попробуйте на jcuken поиграть в леммингов, у которых управление qaop. ;-)
                            +1
                            Если вы думаете, что это никому не надо, то где-то грустит один джун)
                          +1
                          У меня вопрос, если статья не нравится, вы знаете что не хватает такой статьи, как вы описали, то почему же вы сами её не напишите? Либо не найдёте того, кто может написать?

                          Я не просто так спрашиваю, обычно когда такое вижу, я не бегу писать возмущённый комментарий, а просто пишу недостающую статью. Так как от комментария пользы не много (статья не появится), а вот от статьи польза есть.
                          +1
                          У меня с EDK2 совсем не сложилось, даже по гайдам не получилось скомпилить приложение, не то, что по официальной документации. Поэтому я начал разбираться, как это делается без сторонних монструозных библиотек, и написал пример, который должен быть понятен новичкам. Графика там тоже есть, кстати: github.com/VioletGiraffe/UEFI-Bootloader

                          Точнее, сам код примера я тоже не писал, где-то подсмотрел, я только слепил его с заголовочными файлами UEFI и заставил компилироваться (и работать на голом железе), что оказалось на удивление легко.
                          +6
                          Дествительно, статья запоздала лет на… цать. И странно, ничего не сказано, что у ассемблера х86 есть два варианта написания, от Intel и от AT&T.
                            +3
                            У ассемблера именно x86 есть только один правильный вариант написания и это от Intel. Наличие же вырвиглазного синтаксиса AT&T это прихоть GNU'шно-DEC'овского мира, которые почему-то считают (тут перехожу на high level language) что «int 5 = i;» это более естественно чем «int i = 5;»
                              +1

                              int i = 5 появилось позже чем mov 5, r0 (move 5 to r0)

                                +1
                                Я же говорил:
                                У ассемблера именно x86...

                                И речь тут не идёт про процессоры с r0...r15. Вернее про DEC'овские процессоры. Потому как на ARM тоже r0...r15, но там направление приёмник-источник тоже привычное большинству народа, как на x86, i.e. сначала приёмник, а после источник. Привычное именно большинству народа сейчас, а не малой кучке динозавров (правда я и сам не молод), которые начинали с DEC ALGOL, COBOL, PL/I и им привычнее сначала источник, а после приёмник.
                                0
                                В конце концов, мнемоника ассемблера должна облегчать написание и понимание написанного на интуитивном уровне, и если оно MOV EAX, EBX, то неподготовленному читателю можно же ждать именно «Move EAX to EBX», а не наоборот :)
                                Сравним тот же Mикрочиповский MOVLW = «Move Literal to W» и MOVWF x = «Move W to Fx», и тут же рвущие шаблон интуитивно понятные SUBLW = «Subtract W from Literal» (или наоборот?) и SUBWF x = «Subtract W from Fx» (или наоборот?), которые, низачто не догадаетесь, делают абсолютно противоположные вещи: «L-W» (нелогично) и «Fx-W» (логично).
                                Или 8051ый: MOV R0, #0, тут уже спокойно заменяем запятую знаком "=", и SUBB A, #01h, заменяем запятую минусом.
                                Или z80: LD A,n это именно A=n, а SUB A это именно А-А.
                                Или вообще древний IBM704-ый: LDQ A будет «Load MQ from A», а STQ A «Store MQ to A»
                                  +2
                                  Вот именно, на интуитивном уровне. Интуитивно как раз и ожидается что «i = 5», а не «5 = i».
                                    +2
                                    Нет, про соответствие "," = "=«я не знаю, когда читаю текст. Я читаю „переместить что куда“, а не „переместить куда что“, „сложить а и б“, „вычесть б из а (или наоборот? все время путаю!)“, „прыгнуть на строку, если не ноль“, „сдвинуть влево“ (и опять х86 с арифметическими и логическими сдвигами, поясните непонятливому читателю — в чем разница, двигаем же бит в байте!).
                                    Также, для верхнего уровня текстов программ, „если а равно б то с“, „для каждого И от 1 до 10 с шагом 2“, „делать, пока не ноль“.
                                    А про то, что запятую надо знаком равенства заменять — это меня заранее авторы должны предупредить, тогда это уже и нельзя прочитать „переместить а в б“, а „переместить в а из б“, а это уже сложнее языковая конструкция :)
                                      0
                                      интуитивно? Это дело привычки, но в массовость вошло, что i = 5, а не 5 = i И идти по другому пути лишь бы отличаться чем-то — это фиаско для AT&T
                                        +1
                                        Да, привычки, согласен. Но раз в массовость вошло, раз с младых ногтей учат что приёмник слева, а источник справа, то это значит что это уже лет с пяти как минимум именно интуитивно. Не знаю как там у тех кто пишет справа налево (арабы, евреи и т.д.), но у всех остальных интуитивно «i = 5», а не «5 = i».
                                      0

                                      А зачем неподготовленному читателю, а не программисту, вообще читать ассемблер? Вроде уже не 60-е годы прошлого века, когда всё только зарождалось. Сейчас во всех языках программирования (кроме редкой экзотики) присваиваемое значение является левым операндом. Вот ассемблер не должен выпендриваться.


                                      Там ещё более необычные конструкции возможны:


                                      addl  %edx, 8(%esi,%edi,4)

                                      И как человек интуитивно догадается, что это значит:


                                      add [esi + edi * 4 + 8], edx
                                        0
                                        Неподготовленный чиататель, это человек, знакомый, допустим, с одним ассемблеромязыком программирования и читающий программу на другом.
                                        Обычно, в рамках одной идеологии, проблем возникнуть не должно (кроме разве что функций в Си после комментариев Паскаля). Тонкости про [esi + edi * 4 + 8] уже смотрим в документации.
                                        Не возникнет же проблем понять назначение, например, таких инструкций:
                                        MOV ACC,@VarA << #10
                                        SUB ACC,@VarB << #6

                                        разве что сдвиг надо сделать в первую очередь, а запятую на, соответственно, равно и минус поменять во вторую.
                                    0
                                    Нет никаких вариантов. Синтаксис команд ассемблера определяется исключительно разработчиком микропроцессора! Не нравится — выберите другого разработчика.
                                      +2
                                      Написать свой транслятор своего ассемблера еще проще, чем компилятор. Нужно только время, терпение и талмуд с ISA.
                                        +1

                                        Разработчиком компилятора. А процессор обрабатывает двоичный код, а не команды ассемблера.

                                      +2
                                      Сама по себе тема написания загрузчика интересна сама по себе, но на сегодня, с практической точки зрения, было бы интересно почитать про загрузчик на AOSP, как стартует ОС, что при этом происходит, более детально. Ну и более специфичные вопросы, например: как исполняется код BIOS? На простых микроконтроллерах код может исполняться сразу в ROM, но я не уверен, что в старших семействах это возможно, соответственно а как исполняемый код из ROM попадает в RAM и затем получает управление. Еще было бы интересно понять, в какую RAM попадает код из ROM BIOS? В динамическую или статическую? Как инициализировать контроллер динамической памяти, ведь после (ре)старта системы, динамическая память недоступна.
                                      +1
                                      Вчера ремонтировал Gigabyte на H110, а это мать 2015 года!!! И там просто отсутствует legacy загрузка как класс, нет IDE эмуляции… кроме спортивного интереса разве есть смысл СЕГОДНЯ во всём этом?
                                        +2
                                        Только исторический. Более того, судя по тому, что творится сейчас со свежими AMD, скоро real mode останется в прошлом — новая рязань, если я правильно понял всю котовасию, стартует уже в защищенном режиме и с настроенной памятью.
                                          0
                                          Хитрый план, т.е. раньше мы кормили PSP настройками из PEI, а теперь будем их класть прямо на его ФС, чтобы он память до отпускания ресета тренировал? Забористое курят, не отнять…
                                            0
                                            Ага, я сам прифигел с того, что для втуливания рязани в coreboot народу пришлось вытащить куски поддержкий Intel FSP. И да, теперь PSP настраивает память, снимает сброс и родные х86 ядра стартуют с уже настроенной памятью (и наверняка почти всем PCI пространством) как во времена 8086/80286. Телефончиком диллера правда почему-то нее делятся, а жаль, я тоже хочу такое творить.
                                              0

                                              》настраивает память, снимает сброс и родные х86 ядра стартуют с уже настроенной памятью 


                                              Ну, Интел на Атоме (N250?) этим баловался ещё лет 10-15 назад. Называлось настройка СNC кода, вроде.

                                        +3
                                        Бог услышал мои молитвы! Автор — спасибо!
                                          +2
                                          я понимаю, что это перевод, но что все таки означает: «написание кода на компиляторе»?
                                            +1
                                            Да, согласен, я устал за автором править подобные «особые» формулировки. Если почитать оригинал, то можно во многих местах сделать О_О.
                                            0
                                            void initGraphics() {
                                                 ...
                                                 for(;;) { ... }
                                            

                                            этот цикл разве не бесконечный? Где там условие выхода?
                                              0

                                              Выхода куда?

                                                0
                                                выхода из цикла
                                                  0

                                                  Не откуда, а куда.
                                                  Это я к тому, что из загрузчика некуда выходить — он либо грузит ОС, либо зависает.

                                                    0
                                                    Либо грузит OS, либо пишет «Press any key to reboot...», ждёт любую клавишу и делает jmp 0xf000:0xfff0. Просто зависать ему негоже.
                                                      +3
                                                      Загрузчик в случае неполадки в качестве последнего средства должен жахнуть int 18h чтобы вернуть управление в BIOS.

                                                      В каноничном PC или PC/XT это закончилось бы Бейсиком, но в текущих реалиях — перезагрузкой или зависанием (в порядке убывания вероятностей).
                                                        +2
                                                        Ну да, согласен. Про int 18h я как-то забыл уже. Посыпаю голову пеплом.
                                              0
                                              Сектор – это особый раздел загрузочного диска

                                              На этом чтение данной статьи можно закончить )))
                                                0
                                                Это трудности перевода.

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

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