Skip to content

Latest commit

 

History

History
434 lines (281 loc) · 84.5 KB

INFO.md

File metadata and controls

434 lines (281 loc) · 84.5 KB

INFO FROM https://youtube.com/@AlekOS

_ Как работает оперативная память _

Бит - минимальная единица памяти

Байт - минимальная ячейка адресации

Для хранения boolean необходимо 1Б, несмотря на то, что он занимает 1 бит

У ячейки Байта есть адрес(байтовая адресация), по которому к нему обращается процессор. У некоторых компьютеров размер ячейки памяти зависит от машинного слова(сколько процессор за раз может обработать байт). У 32-разрядных процессоров(они для мощных компьютеров, которые работают с большими числами) машинное слово длинной 32 бита. Если процессор обращается к машинному слову, а не к байту, то такая адресация называется "словесная". В обычных компьютерах одна ячейка памяти равняется 1 Байту

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

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

Для работы со строками лучше подходит байтовая адресация, с числами - словесная

Адресация ячеек оперативной памяти происходит в регистрах процессора. Процессор с регистром на n бит может дать адрес 2^n ячейкам(байтам). Таким образом, 32-битный процессор можно работать только с 2^32 Б == 2^22 Кб == 2^12 Мб == 2^2 Гб == 4 Гб

Хранение отрицательных чисел происходит с помощью дополнительного кода: число без знака представляется в двоичном виде, приписываетя бит знака, инвертируются биты без бита знака, прибавляется 1 бит к результату. Оставшиеся биты будут заполнены битом знака(!)

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

_ Как работает процессор _

Архитектуры: x86 - ПК ARM - мобильные устройства AVR - микроконтроллеры встроенных систем(машины, телевизоры итд)

Каждая архитектура поддерживает свой набор команд

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

Процессоры моделей 8086-80286 имели 16-битную разрядность, модели 80386-... (эти модели называются x86) - 32-битные, модели AMD-64(x86-64) имеют 64-битную разрядность. У Intel 64-разрядные процессоры называются EMT64T

У разных моделей процессоров разное количество регистров. Регистры можно разделить на 2 вида: регистры специального назначения, регистры общего назначения(используются программистами по их усмотрению; в этих регистрах могут храниться переменные, параметры, результаты вычислений). Нам доступны не все регистры

При увеличении размера регистра, "старые" регистры стали располагаться внутри "новых".

В регистрах специального назначения хранятся данные разного характера: в сегментных регистрах(SS, CS, DS ...) хранятся адреса памяти регистры для работа со стеком(BP, SP ...) указывают на начало фрейма и на верхушку стека флаговые регистры(FLAGS) содержат разные биты, которые отражают состояние результата предыдущей операции указатель команд(IP ...) содержит адрес команды, которую нужно выполнить следующей

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

После включения компьютера процессор начинает выполнять команды по жесткозаданному адресу. По этому адресу располагается BIOS(Basic input/output system). Его задача - найти первое дисковое устройство, взять оттуда первый сектор, загрузить его в память и передать на него управление, то есть указать процессору на то, чтобы теперь он выполнял команды по адресу этой загруженной программы. Этой программой является Операционная Система, которая в дальнейшем переводит процессор в защищённый режим, далее реализуется многозадачность, работа с памятью, выставляются различные ограничения итд

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

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

Процессор может работать в нескольких режимах:

  1. Режим реальных адресов (16 бит) - в этот режим процессор переходит сразу после включения компьютера. Адреса, сформированные программой, и действительные адреса совпадают. В каждый момент может выполняться только одна программа. Вся память делится на сегменты в 64 Кб
  2. Защищённый (32 бит). В него можно перейти только из режима реальных адресов. Это делает операционная система, выставляя специальный флаг системного регистра и устанавливая разрешения для других программ. Этот режим реализует защиту следующим образом: разделяет приложение на разные уровни привилегий; процессор следит, чтобы программы не получали доступ к запретным участкам памяти и не использовали любые команды(работа привилегий). При нарушении поведения программы, процессор генерирует исключение и передаёт управление операционной системе, чтобы та приняла меры. Вся память делится на сегменты в 4 Гб. Работает механизм трансляции виртуальных адресов в физические
  3. 64-разрядный режим (64 бита). В него можно перейти только из защищённого режима. В нём используется та же защита. Отключена сегментация памяти. Доступно 2^52 Б физической памяти и 2^48 Б виртуальной памяти. Эти ограничения обусловлены архитектурой процессора и операционной системы

Для второго и третьего режима существует 4 уровня привилегий: 0 уровень - полный доступ к процессору. На нем работает операционная система 1 уровень 2 уровень 3 уровень - пользовательский Запреты верхних уровней распространяются на нижние

Вся оперативная память поделена на условные сегменты - участки памяти определенного размера. Размер зависит от режима процессора. Адрес участка представляется программами в специальной форме(для режима реальных адресов): логический адрес = адрес начала сегмента(16 бит) : смещение в сегменте(16 бит)

Этот адрес преобразовывается в физический адрес: физический адрес(20 бит) = адрес начала сегмента << 4 + смещение в сегменте Может адресовать 2^20 адресов, то есть 1 Мб физической памяти.

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

  1. Абстракция: Логический адрес предоставляет программам абстракцию от физической памяти компьютера. Это позволяет программам работать с данными, не зная о конкретных физических адресах и местоположении данных в памяти. Это облегчает разработку программ и повышает их переносимость между различными системами.

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

  3. Управление памятью: Операционная система может эффективно управлять физической памятью, используя логический адрес. Она может использовать механизмы виртуальной памяти, такие как страничная трансляция, для оптимального распределения и управления доступом к данным в памяти.

  4. Поддержка большего объема памяти: Логический адрес может быть больше, чем физический адрес, что позволяет адресовать больший объем памяти, чем доступно физической памяти на компьютере. Это особенно полезно в системах с ограниченным объемом физической памяти или в случае использования виртуальной памяти.

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

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

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

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

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

В защищенном режиме логический адрес формируется следующим образом: логический адрес = селектор дескриптора(16 бит) : смещение в сегменте(32 бита)

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

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

  1. индекс в каталоге страниц(из которого выделяется физический адрес таблицы страницы)
  2. индекс в таблице страниц(из которого выделяется физический адрес страницы)
  3. смещение в странице Затем с помощью механизма трансляции адресов происходит преобразование виртуального адреса в 32-битный физический. Можно адресовать 2^32 Б = 2^22 Кб = 2^12 Мб = 2^2 Гб = 4 Гб

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

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

_ Как работает стек _

Вся оператиная память делится на стек и кучу

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

Каждый элемент стека занимает одно машинное слово. SP - stack pointer - указатель на верхушку стека. BP - base pointer - указатель на начало фрейма стека, используется для получения параметров в стеке, на протяжении выполнения функции статичен (если не вызывались подрограммы). Оба указателя хранятся в соответствующих регистрах

Для работы со стеком есть две основыные команды: push - поместить данные на верхушку стека, pop - извлечь данные с верхушки стека

Например, в стек нужно поместить число 3. У указателя SP нужно отнять величину машинного слова, затем в "передвинутый" SP поместить число 3. Команда pop работает в обратном порядке. pop ax - вернуть данные с вершины стека в регистр ax, а к указателю SP нужно прибавить величину машинного слова

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

Для перехода между подпрограммами в Ассемблере есть несколько основных команд: jmp - прыгает из одного участка кода в другой по указанному в операнде адресу call - перед тем, как перескочить, записывает в стек адрес возврата, который указан в операнде ret - используется для возврата внутри вызываемой функции после того, как та отработала. Она вытаскивает из стека адрес возврата и переходит по нему

Область конкретной функции в стеке называется "фреймом функции", внутри которой находится её локальное окружение

Перед вызовом подпрограммы в стек сохраняется старый адрес BP (push bp), чтобы после выполнения вызываемой функции мы продолжили выполнение исходной функции. Далее мы должны поднять BP на самый верх, копируя в него SP (mov bp,sp). Дальше в стек будем добавлять локальные переменные функции (push 4, push 5.5, push str). BP по-прежнему в ячейке, где хранится адрес старого BP

При окончании выполнения подпрограммы переносим SP в BP (mov sp,bp). Затем мы извлекаем bp с вершины стека (pop bp) и перемещаем его на старый адрес. SP всё это время перемещается по верхам стека. Таким образом, мы восстановили состояние регистров sp, bp до того момента, как была вызвана подпрограмма (SP указывает на адрес возврата(который был помещен туда командой call), BP на начало фрейма исходной функции). Осталось вызвать команду ret, которая извлечет из стека адрес возврата и перейдет по нему

Бесконечная рекурсия вызывает переполнение стека, так как постоянно будут сохраняться адрес возврата и старый адрес BP

Передачу параметров можно осуществлять с помощью стека. В основном используют принцип call std, когда параметры передаются в обратном порядке(push 3, push 2, push 1), а затем записывает адрес возврата(call f) и старый адрес ВP(push bp, mov bp,sp). В следствии чего для получения параметров(в 16 битном режиме процессора) используем следующие команды: mov ax,[bp+4], mov bx,[bp+6], mov cx,[bp+8]

После отработки функции параметры больше не нужны и их может очистить(переместить SP на то место, где он был до передачи параметров) вызываемая или вызванная подпрограмма: -вызываемая программа: mov sp,bp, pop bp, ret 6. Тут 6 - обозначает сколько байт нужно удалить из стека после удаления адреса возврата(у нас три параметра по 2 байта каждый) -вызванная программа(когда нужно выполнить несколько команд с одинаковым набором параметров): после команды ret SP будет указывать на последний переданный параметр. Поэтому необходимо его сдвинуть на количество битов, занимаемые параметрами(add sp,6)

Обычно локальные переменные хранятся в стеке. Область, которая под них отводится, называется "стековый фрейм". При вызове функции формируется этот самый стековый фрейм путем перемещения BP на вершину стека. Функция выделяет себе память в стеке с помощью сдвига SP на нужное количество байт. Адресация к локальным переменным производится с помощью указателя BP(тот же метод, что и для параметров). Например, чтобы задать локальную переменную 10, нужно выполнить команду mov [bp-2],10. Если вторую хотим str, то mov [bp-4],str. При заверешении функции эти переменные удаляться путем сдвига SP

_ Язык Ассемблера _ Видео

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

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

Язык Ассемблера = Я хочу разобраться, как работает мой код!!!

Программа на Ассемблере - это программа без абстракций. С помощью этого языка, можно понять, как работает компьютер

Ассемблер по умолчанию содержит команды для процессора 8086. В нем нельзя использовать команды для процессоров "старше" без указания директивы, обозначающей модель процессора - .8086, .186, .286 итд

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

Есть два синтаксиса для Языка Ассемблера: Intel: mov ax,5 AT&T: movw 5,%ax (b - (byte) операнды размером 1 байт, w - (word) размером 1 слово, l - (ling) размером 4 байта)

Программа в памяти поделена на 3 сегмента: сегмент кода (инструкции программы) сегмент данных (глобальные переменные) сегмент стека

Распологаются сегменты в памяти не по порядку. Их адреса назначает операционная система, а сами сегменты располагает компоновщик. Эти адреса мы можем использовать в коде для взаимодействия сегментов друг с другом. Например, из сегмента кода, мы хотим прочитать данные из сегмента данных. Тогда необходимо сформировать логический адрес - адрес сегмента и смещенение в этом сегменте. Адреса сегментов хранятся в регистрах CS(code segment), DS(data segment), SS(stack segment), ES GS FS(сегменты доп данных). Смещение представляет из себя название метки, что-то в роде названия переменной. Адреса 16-битные(работаем в режиме реальных адресов), поэтому адреса являются 16-битными числами. В следствие этого, в каждый сегмент помещается максимум 64 Кб

Чтобы процессор смог обратиться к реальной ячейки памяти, он формирует физический адрес, путем смещения адреса сегмента на 4 бита влево (умножение на 16) и прибавляя смещение. У процессора 8086 была 20-битная шина адреса, поэтому физический адрес имеет такой вид. С помощью 20 бит можно адресовать 1 Мб памяти. Каждый сегмент имеет минимальный размер в 16 байт из-за сдвига сегментного адреса на 4 бита, если использовать нулевые смещения

Создаются сегменты с помощью директивы SEGMENT(первый способ). Сначало мы задаём имя сегмента, которое в дальнейшем будет использовано для получения сегментного адреса, далее ключевое слово SEGMENT. После - могут идти 4 необязательных параметра(конфигурация хранения в памяти) - разрядность, выравнивание, класс, тип. Далее ставится закрывающаяся конструкция code ends. Между ними пишется код, предназначенный для данного сегмента.

Разрядность - размер регистров, адресов. Этот параметр может принимать два значения: USE16(по умолчанию для процессоров ниже 80386) - 16 бит(именно в этом случае размер сегмента не превысит 64 Кб), USE32(по умолчанию для процессоров выше 80386 включая) - 32 бита(размер сегмента может быть до 4 Гб)

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

byte - по любому адресу word - кратны 2 dword - кратны 4 para - кратны 16 page - кратны 256

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

Тип - говорит компоновщику, как комбинировать сегменты с одинаковым именем. PUBLIC - все сегменты с одинаковым именем будут объединены в один. В одном модуле все сегменты с одинаковым именем и одинаковым типом обрабатываются так, как если бы мы объявляли один сегмент с таким именем. Под модулем подразумевается отдельно взятый файл программы на языке Ассемблера. Программа может состоять из нескольких файлов, то есть модулей. В таком случае параметр "тип" определяет, как сегменты из разных модулей должны объединяться между собой. Сегменты с типом PRIVATE, который используется по умолчанию, не будут объединяться в один. Тип STACK определяет сегмент стека программы и по аналогии с типом PUBLIC все одноименные сегменты с типом STACK будут объединяться в один

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

data SEGMENT data ends

Неверно: mov ds, data

Верно:
mov ax, data mov ds, ax

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

data1 SEGMENT ... data1 ends


data2 SEGMENT ... data2 ends


data3 SEGMENT ... data3 ends

Их адреса мы можем раскидывать по сегментным регистрам данных, либо объединить в одну группу, задав ей имя: DGROUP group data1, data2, data3 Далее mov ax, DGROUP mov ds, ax В таком случае адреса сегментов будут вычисляться относительно начала этой группы

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

code SEGMENT ASSUME CS:code, DS:data, SS:stack ... code ends data SEGMENT ... data ends stack SEGMENT ... stack ends

ASSUME используется как подсказка. То есть, указав, что DS:data, мы в дальнейшем не перезапишем этот регистр. Например, у нас объявлены два сегмента данных - d1, d2. d1 в регистре ds, d2 в es. В сегменте кода мы обращаемся к данным из d2 - к переменной var, указывая только имя метки(mov dl, var). var - смещение данных в втором сегменте данных. Ассемблер обнаруживает метку с таким названием, но чтобы к ней обратиться, компилятору нужно знать адрес сегмента. Он это возьмет как раз из ASSUME(ASSUME CS:code, DS:d1, ES:d2)

Если в ходе программы меняем адреса в сегментных регистрах, то необходимо повторно использовать директиву ASSUME

Мы может создать несколько сегментов для кода, данных или стека. Все это формирует модель памяти, которую будет использовать программа. С целью автоматического комбинирования сегментов памяти, в компилятор была добавлена директива .model. В качестве операнда она принимает одну из предложенных названий моделей(tiny | small | compact | medium | large | huge | flat). Некоторые из них были актуальны, пока использовалась сегментация памяти. Сейчас в 32-битных системах программы(код, данные и стек) находятся в одном большом сегменте памяти, где 32-битные сегменты(до 4 Гб) и адреса - модель flat. В dos её использовать нельзя, так как находимся в 16-битном режиме. Используем tiny - аналог flat для dos. Используя её, весь код программы, данные и стек будут помещены в один сегмент, который не превышает 64 Кб. Остальные модели - различные комбинирования сегментов памяти. Модель small позволяет использовать в программе несколько сегментов. По одному на сегмент кода, данные и стек, но последние два в группу, то есть DS и SS указывает на один и тот же сегмент. Использование моделей позволяет объявить сегменты вторым способом.

Директивы: .code ;сегмент кода программы .stack size ;сегмент стека(size == 1 Кб по умолчанию) .data ;ближние инициализированные данные(глобальные переменные, которым сразу были присвоены значения) .data? ;ближние неинициализированные данные(на момент исполнения программы данные не определены или имеют значения, оставшиеся после прошлой программы) .const ;для неизменяемых данных

Для формирования программой логического адреса указывается как и адрес сегмента, так и смещение - mov ax, DS:VAR. При этом в ds помещен адрес сегмента данных: mov ax, data; mov ds, ax. Это называется "дальним адресом". Этим способом можно обратиться к любому участку памяти

Однако, можно указать только 16-битное смещение. Это называется ближним адресом. Предполагается, что в сегменте данных уже находится адрес целевого сегмента(ASSUME ds:data; mov ax, data; mov ds, ax) и сам компилятор знает, какому сегменту соответствует этот адрес. Именно поэтому в таком логическом адресе сегмент можно не указывать. Если это не так, то перед смещение сегмент указывать нужно

Для дальних данных: .fardata ;дальние инициализированные данные .fardata? ;дальние неинициализированные данные

Каждая из этих директив - это укороченный аналог объявление сегментов через SEGMENT(первый способ): .code == _TEXT segment word public 'CODE' .stack size == STACK segment para public 'STACK' .data == _DATA segment word public 'DATA' .data? == _BSS segment word public 'BSS' .const == CONST segment word public 'CONST' .fardata == FAR_DATA segment word private 'FAR_DATA' .fardata? == FAR_BSS segment word private 'FAR_BSS'

BSS == block starting symbol

Эти директивы не нужно закрывать. Сегмент будет считаться законченным, когда будет найдена следующая директива SEGMENT или конец программы. Используя, например, _TEXT, мы получим адрес сегмента .code

Используя сокращенные директивы, сегменты объединяются в группы, имена которых тоже может быть использовано для получения сегментного адреса. Во всех моделях памяти все директивы связанные с данными и стеком будут объединены в одну группу. Название этой группы будет "FLAT", если использовано .model flat, иначе "DGROUP". Если используется .model tiny, то в это группу входит и сегмент кода, т.к. в этой модели памяти все сегменты находятся в одном

Используя директиву .model tiny, сегментные регистры DS, SS, CS автоматически ссылаются на группу DGROUP, позволяя не использовать директиву ASSUME

В зависимости от модели памяти определяется тип исполняемого файла - .com или .exe. В первом используется тип памяти tiny, содержится только скомпилированный код без дополнительной информации о программе, размер программы не превышает 64 Кб. Размер файлов .exe неограничен, используется модель памяти small, содержится код программы и заголовки(имеено по ним dos определяет тип исполняемого файла - по первым двум байтам находится метка MZ)

Про память: в начале отводимого под программу блока памяти(программа типа .com) или в отдельном сегменте(.exe) создается структура данных PSP(program segment prefix) размером 256 байт(100h). Далее dos заполняет поля этого блока адресами обработчиков прерывания, переданными через командную строку параметрами (от 00h до 0Ah - команда int 20, от 0Ah до 2Ch - адрес обработчика int 22h, от 2Ch до 50h - переменные среды(PATH), от 50h до 52h - команда int 21h, от 52h до 80h - команда RETF(команда RETF аналогична команде RET, за исключением того, что из стека извлекается дальний адрес возврата: первым извлекается значение сегмента, затем - смещение в сегменте. PS. команда RET читает из стека адрес возврата и заносит его в регистр IP, передавая таким образом управление), от 80h до 100h - параметры из командной строки)

Со смещения 100h начинается программа(адрес сегмента == PSP), если программа типа .com. Нужно явно указать компилятору, что программа начинается с таким смещением, используя директиву ORG - origin(в данном случае ORG 100h)

В случае модели tiny все сегменты программы, включая PSP блок, будут находиться в одном сегменте памяти. Следовательно, все сегментные регистры и PSP будут равны между собой

В случае .exe программы, все будет находиться в отдельных сегментах, поэтому директива ORG не нужна. Чтобы получить адрес PSP сегмента можно воспользоваться 62 функцией 21-го прерывания, которая вернет нужный адрес в регистр BX

По умолчанию, сегменты, объявленные через SEGMENT будут располагаться в памяти в той последовательности, в которой они были объявлены в коде. Сегменты, объявляемые сокращенными директивами, используя .model small, будут идти в порядке "code-data-stack"

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

Во время загрузки программы в память, dos помещает в стек 16-битное значение, состоящее из нулей, которое является смещением на самое начало сегмента программы. Затем выполняется переход на её начало. Регистр SP будет содержать смещение(0FFFEh) на начало стека в этом сегменте. Его начало находится в конце, поэтому изначально он будет указывать на адрес последнего слова в сегменте

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

Чтобы процессор начал выполнение нашей программы, операционная система записывает в CS адрес сегмента кода нашей программы(00h). В регистре IP будет смещение относительно этого сегмента(PSP:100h для программы .com, для .exe - соответствующая точка входа). То есть, изменяя значения в CS и IP, происходит передача управления

При запуске каждой программы dos создает копию своего окружения(PATH). Используя её, программы могут находить файлы по пути, который в ней был указан. dos заносит эти переменные в формате ASCIIZ-строк в отдельный сегмент памяти, адрес на которую помещается в блок PSP по смещению 2C

По смещению 80h находится длина командной строки(0, если не было передано). Начиная со смещенения 81h и до 100h, находятся переданные параметры, которые мы указали после имени передаваемого файла. Все это заканчивается символом клавиши "Enter". Длина командной строки не должна превышать 127 байт, так как дальше в памяти начинается программа

Ассемблер читает программу дважды, так как могут встречаться действия с метками до их объявления(jmp start ... start:) - проблема опережающей ссылки. При первом проходе анализируется каждая строка: в ней определяется код операции, набор операндов, различные директивы, символические имена, происходит расширение макросов итд. К конце первого прохода получается несколько таблиц: таблица кодов операции, директив, сиволических имен, константная таблица и другие. Второй проход происходит по этим таблицам, в результате чего получаются объектные модули(.obj или .o). Он содержит:

  1. Идентификация
  2. Таблица точек входа
  3. Таблица внешних ссылок
  4. Машинные команды и константы
  5. Словарь перераспределения
  6. Конец модуля

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

_ Язык Ассемблера. Часть 2 _ Видео

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

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

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

Компилятор masm предлагает на выбор несколько директив для резервирования памяти: db - выделить 1 байт dw - 2 байта dd - 4 байта df - 6 байт dq - 8 байт dt - 10 байт

Для примера выделим 2 байта, но чтобы к ним можно было обратиться, нужен адрес этой области. Поэтому перед директивой придумываем символьную метку, которая указывает на первый байт выделяемого места. В ходе компиляции эта метка будет заменена обычным адресом. Справа от директивы мы можем сразу проинициализивать переменную(myVar dw 58). Чтобы оставить неинициализированной, мы должны написать знак вопроса(noInitVar dw ?). Значения можно ввести через запятую, тем самым проинициализировать последовательность ячеек(подобие массива что ли? - да!).

Вернемся к примеру из уже написанной программы. В ней мы определили msg db 'hello world!$'. Здесь мы не перечисляли через запятую ASCII-символы, а записали однобайтовые символы внутри кавычек. Тем не менее, вариант с запятыми тоже будет прекрасно работать.

Есть ещё один способ инициализации нескольких ячеек подряд - директива DUP(num db 3 DUP(5)), перед которой мы указываем количество создаваемых значения, а в аргумент передаем сами значения.

Теперь поговорим об адресации. Первый вид адресации - это адресация, использующая непосредственные значения, то есть когда мы указываем копируемое значение напрямую(mov ax,5). Важно помнить, чтобы копируемое значение не превышало размер регистра. Второй вид адресации - регистровый, это когда информация копируется в регистр из другого регистра. Следующие действия приведут к ошибкам: mov ax, 65536; mov ax,bh; mov ds,cs; mov ds,2.

Остальные виды адресации связаны с формированием адреса до конкретного участка памяти. Подобные адреса подразделяются на прямые и косвенные. Косвенный адрес - это логический адрес, состоящий из сегмента и смещения(дальний адрес). Когда мы обращаемся к сегменту в котором находимся, то можем указать только смещение(ближний адрес). Сам же адрес сегмента подставится автоматически из регистра DS. Если в качестве смещения мы используем число, то скопируется столько байт, сколько поместится в приёмник. В случае с переменной - подобный случай. Если же размеры переменной и регистра не совпадают, то нужно вручную указать, сколько байт нужно скопировать, используя директиву ptr(mov ah,byte ptr num), слева от которой указывается, сколько байт скопировать, справа - откуда скопировать.

А что, если мы хотим скопировать данные из регистра в память, например, перезаписать значение нашей переменной? Во-первых, копирование данных из одной части память в другую запрещено(mov n1,n2; n1 db 1; n2 db 2). Во-вторых, это делается через директиву ptr, только в обратную сторону(mov word ptr num,ax).

Как скопировать именно адрес ячейки? Для этого используется директива offset перед нужным адресом. Этот вид адресации называется косвенным. Она позовляет нам хранить адрес в отдельном регистре, который мы затем помещаем в квадратные скобки, что позволит нам взять значение по этому адресу. До 386 модели процессора для этой операции позволялось использовать только 4 регистра: bx, si, di, bp. Однако, у такой адресации есть и огромный плюс: она позволяет применять к адресам арифметические операции([bp+2])

ADD ax,bx == ax=ax+bx

Процедуры - кусок кода, расположенный в памяти, на адрес начала которого мы переходим каждый раз, когда нам надо её вызвать. В языке Ассемблера существует специальная конструкция proc-endp(sum proc; ...; sum endp) внтури которой пишется код процедуры. Слева от proc и слева от endp пишется символьная метка процедуры. Как и функции, процедуры можно вызвать из любого места программы, можем передать какие-то значения и вернуть результат.

В качестве примера напишем процедуру сложения двух чисел. На вход ей будет передавать два числа. С большой вероятностью в функции в качестве локальных переменных будем использовать регистры общего назначения, в которых уже могут быть какие-то данные. Поэтому перед вызовом функции сохраним эти данные в стек(push ax), а после выполнения функции вернем обратно. Теперь обсудим, как передать процедуре её параметры. Первый способ - регистровый. В этом случае мы передаём значения в заранее оговоренные регистры, с которыми процедура будет работать. Второй способ - std call. Тут мы передаём параметры через стек, занося их туда в обратном порядке. При этом стоит учесть, что помещать значения в стек напрямую запрещено. Поэтому параметры пойдут в стек транзитом через регистры

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

Так как в нашем примере используется короткий переход, то переходим с помощью jmp sum // sum == cs:sum, но после выполнения процедуры нам нужно знать, куда вернуться. Поэтому в стек нужно сохранить метку(...; mov cx,N; push cx; jmp sum; N:). Всю эту конструкцию можно заменить командой call sum. Далее, уже в прологе процедуры(в её начале) настроим метку bp(push bp; mov bp,sp). Теперь получим параметры из стека: mov ax,[bp+4]; mov bx,[bp+6] - всё это к конкретно нашему случаю, когда два параметры и ячейки по 2 байта. PS: по адресу [bp+2] находится адрес возврата. После этих действий мы можем сложить числа: add ax,bx.

Нам осталось написать эпилог процедуры, в котором мы возвращаем bp(pop bp) и записываем в него старый сохранённый адрес. Далее команда ret передаст управление обратно в программу. Причем компилятор сам заменяет её на retn, если переход короткий, или retf, если переход длинный. Справа от команды ret можно указать количества байт, которые нужно очистить из стека после считывания адреса возврата. Это используется для удаления параметров, которые ранее мы передали процедуре.

Компилятор заменит ret, опираясь на параметры после sum proc: NEAR(для tiny,small,compact)|FAR язык(для высокоуровневых языков) USES ax bx(используемые процедурой регистры). Регистры, упомянутые в USES, компилятор сам поместит в стек, а в самом конце вернет из стека.

Процедура sum вернёт результат в регистр ax, поэтому сразу после вызова перенесём это значение в регистр cx(mov cx,ax), чтобы не затереть тем, что вернётся "на место" из стека.

Рассмотрим важный момент. Регистров общего назначения не так много, поэтому принято хранить локальные переменные в стеке сразу над старым значением bp, заранее отодвигая sp на необходимое количество байт(sub sp,4). Обращаться к локальным переменным будет также через регистр bp. В таком случае после сложения значений нужно убрать локальные переменные из стека(mov sp,bp).

Теперь поговорим об условных конструкциях. Весь секрет их успеха в регистре FLAGS и команды условного перехода. Каждый бит регистра FLAGS представляет собой определённый флаг. Из 16 возможных флагов, в процессоре 8086 нам доступно только 9. Единственный флаг с единицей изначально - IF(флаг разрешения прерываний от внешних устройств). 1 - разрешено, 0 - запрещено. Флаг TF(флаг трассировки) используется для отладки программы. То есть при его установки процессор генерирует прерывания 1h после каждой выполненной команды, позволяя выполнять её по шагам, чтобы проверять работу каждой инструкции. Флаг DF(флаг направления) используется строковыми командами. Он определяет в какую сторону будут обрабатываться строки(0 - в сторону старших адресов, 1 - младших). Часть из них можно вручную поменять с помощью команд, но большинство из них устанавливается в ходе выполнения арифметических операций(сложение, вычитание итд.). Например, после сложения двух чисел флаг PF(флаг чётности) будет установлен в 1, если количество единиц в результате четно, 0 - в противном случае.

Причём же тут условные конструкции? Например, чтобы определить, равны ли операнды между собой или первый операнд меньше ли другого, достаточно вычесть из одного числа другое. Если мы сравниваем числа 5 и 5, то, вычтя одно из другого, в 1 выставится ZF(zero flag) ввиду нулевого результата. После этого программа использует команду условного перехода je(jump if equal) и перейдёт в нужную часть кода по метке(je T; ...; T:). Всё это будет происходить автоматически благодаря команде cmp. Она принимает два операнда, которые мы хотим сравнить, и, вычитая из первого второй, выставляет нужные флаги и осуществляет переходы. При этом результат нигде не сохраняется. В качестве операндов могут выступать любые значения(регистры, переменные, непосредственные значения), кроме двух областей памяти(cmp var1,var2). В Ассемблере существует много однотипных команд для условных переходов. Так, команда jz(jump if zero), как и команда je, соверишит переход только в том случае, если флаг ZF==1(операнды команды cmp оказались равны). ZF == 0 означает неравенство операндов, и на этот случай существуют команды jne(jump if not equal) и jnz(jump is not zero), которые осуществляют переход.

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

Беззнаковые числа: Например, команды jb(jump if below) и jnae(jump if not above or equal") совершат переход, если только выставлен(==1) флаг CF(флаг переноса). Этот флаг будет равен единице, если первый операнд команды cmp будет меньше(<) второго. Почему эти команды корректно отработают только на беззнаковых числах, проиллюстрирует следующий пример: флаг CF будет выставлен в единицу, если результат сложения не поместился в регистр, но останется нулем если не поместился результат вычитания(произошёл заём). Следующие команды по-разному отразятся на выставлении флага CF: mov ah,3;add ah,-1 и mov ah,3;sub ah,2. В первом случае флаг будет занят 1, во втором - 0. К слову, флаг AF(вспомогательный флаг переноса) делает то же самое, но с 4-ым битом. Команды не работают со знаковыми числа, так как в случае, когда числа совпадают знаком флаг CF выставляется в 1, а если различаются, то в 0. Из-за того, что каждое отрицательное число преобразовывается в дополнительный код, в котором старшие биты всегда заполняются единицами, оно, будучи положительным числом для процессора, в некоторых случаях будет больше, чем второй операнд. Поэтому флаг CF не выставляется. Но! Это логично только для беззнаковых чисел. Мы это положительное число для себя считаем отрицательным. И следовательно при сравнениях нужно ориентироваться на другие флаги, которые будут проверять другие команды условного перехода уже для знаковых чисел.

Несложно догадаться, что если SF==1, когда первый операнд меньше второго, то не меньше(≥), когда SF==0. Именно это проверяют следующие две команды: jae(jump if above or equal) и jnb(jump if not below). Первый операнд больше второго(>), когда CF==0 && ZF==0. Этому соответствуют команды ja(jump if above) и jnbe(jump if not below or equal). Последнее сравнение - это меньше или равно(≤). Это будет при CF==1 || ZF==1. Соответствующие команды - jbe(jump if below or equal) и jna(jump if not above).

Знаковые числа: Команды для сравнения знаковых чисел будут проверять флаги иначе. Флаг SF(флаг знака числа) будет 1, если получили число с единицей в старшем разряде, то есть число для нас отрицательное. Флаг OF(флаг переполнения) выставляется в 1, если у операндов один знак, а у результата другой. Для того, чтобы первый операнд был меньше второго(<), необходимо, чтобы SF!=OF. Команды jl(jump if less) и jnge(jump if not greater or equal). Если флаги SF и OF равны, то первый операнд не меньше, чем второй, то есть больше или равен(≥). Команды: jge(jump if greater or equal) и jnl(jump if not less). Для (≤) SF!=OF || ZF==1. Команды: jle(jump if less or equal) и jng(jump if not greater). Для (>) SF==OF && ZF==0. Команды: jg(jump if greater) и jnle(jump if not less or equal).

Всё это является аналогом if-ов. Блоком кода для else будет служить код, расположенный после перехода по метки(выполняется, если условие не выполнилось). Иначе блок для else мы просто перепрыгнем и не выполним. Все разобранные команды, связанные с регистром FLAGS не поддерживают дальних переходов. Если мы хотим совершить дальний переход, то пишем условие, противоположное нашему, чтобы после этого совершить переход через jmp.

Циклы. У нас есть счётчик и кусок кода, который выполняется до тех пор, пока счётчик соответствует какому-то условию. В качестве счётчика в Ассемблере принято использовать регистр CX, который будет крутиться от изначального числа до нуля. Далее будем прыгать на код, расположенный ниже по метки, пока счётчик не станет нулём.

Для примеры выведем на экран все буквы слова hello(msg db 'hello'). Чтобы узнать, сколько букв в слове, можно вычесть из адреса, следующего после её конца, адрес её начала. Это делается с помощью директивы equ(len equ $-msg). Знак доллара - это текущий адрес. С предыдущей главы нам известно 21 прерывание. У него много функций, номер которых указывается в регистре ah. Функция под номером 2 выведем символы на экран. Организуем код нашего цикла следующим образом: mov cx, len ; кол-во символов == кол-во итераций mov ah, 2 ; функция вывода символа на экран mov si, offset msg ; адрес 1-го символа строки mov bx, 0 ; индекс выводимого символа L: mov dl, [si+bx] ; адрес выводимого символа inc bx int 21h dec cx сmр сх, 0 ; проверяем счётчик на равенство нулю jne L ; перепригивает, если он ненулевой msg db 'hello' len equ $ - msg

Команды dec, cmp и jne можно заменить одной командой loop(loop L - операнд-это метка, куда перейти, если cx>0). Эта команда совершает короткий переход, поэтому метка должна находиться в пределах от -128 до 127 байт от этой команды. Символы напечатаются на одной строке, так как мы не указывали перевод строки.

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

Прерывания делятся на прерывания BIOS, с номерами от 00h до 1Fh: int 00h ; переполнение при делении int 10h ; ah • OFh текущий видеорежим( al текущий режим ah число текстовых колонок на экране ah номер активной видеостраницы) • 05h установить видеостраницу(al номер видеостраницы) int 1Fh ; указатель графических символов

Прерывания DOS, с номерами от 20h до 2Fh: int 20h ; завершить программу int 21h ; ah • 01h ввод с клавиатуры(al полученный символ) • 02h вывод на экран(dl выводимый символ) • 39h создать каталог( ds:dx адрес строки ASCIIZ с названием ах код ошибки, если CF=1) int 2Fh ; мультиплексное прерывание

После их вызова в стек автоматически сохраняются данные регистров FLAGS, CS и IP. После чего передаётся управление обработчику прерывания с указанным номером. В режиме реальных адресов адреса обработчиков прерывания находятся в специальной структуре, расположенной по адресу 00h. Большинство из них занимают по 4 байта. Сам обработчик представляет из себя подпрограмму.

Есть программные прерыания(вызывает программа), а есть аппаратные прерывания(вызывает, например, нажатие по клавише клавиатуры). Сами программы могут на время блокировать вызовы аппаратных прерываний, используя команду cli(clear interrupt flag). Эта команда выставляет флаг IF, который показывает, разрешены ли прерывания, в 0. Это значит, что процессор будет игнорировать прерывания от внешних устройств. Обратное разрешение устанавливается командой sti(set interrupt flag). Это было необходимо в старых моделях процессора при изменении регистров стека SS и SP.

Вызовы прерывания можно перехватывать, можно писать свои обработчики, можно работать с видеокартой итд. Ассемблер - это целый необъятный мир!