Системное программирование
Выбери формат для чтения
Загружаем конспект в формате docx
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Системное программирование
Системное программное обеспечение — комплекс программ, которые обеспечивают управление компонентами компьютерной системы, такими как процессор, оперативная память, устройства ввода-вывода, сетевое оборудование, выступая как «межслойный интерфейс», с одной стороны которого аппаратура, а с другой — приложения пользователя. В отличие от прикладного программного обеспечения, системное не решает конкретные практические задачи, а лишь обеспечивает работу других программ, предоставляя им сервисные функции, абстрагирующие детали аппаратной и микропрограммной реализации вычислительной системы, управляет аппаратными ресурсами вычислительной системы.
Системное программирование — создание системного программного обеспечения.
Системный программист — программист, специализирующийся на системном программировании.
Отнесение того или иного программного обеспечения к системному условно, и зависит от соглашений, используемых в конкретном контексте. Как правило, к системному программному обеспечению относятся операционные системы[⇨], утилиты[⇨], системы программирования[⇨], системы управления базами данных[⇨], широкий класс связующего программного обеспечения.
Одним из языков системного программирования является Ассемблер.
Язык Ассемблер — это низкоуровневый язык программирования для компьютеров или других программируемых устройств, он специфичен для конкретной компьютерной архитектуры центрального процессора, что отличает его от большинства высокоуровневых языков программирования, которые обычно портативны среди разных систем. Язык Ассемблер преобразуется в исполняемый машинный код с помощью служебной программы, называемой ассемблером, такой как NASM, MASM и т. д.
Каждый персональный компьютер имеет микропроцессор, который управляет арифметической, логической и контрольной активностью.
Каждая семья процессоров имеет свой собственный набор инструкций для обработки различных операций, таких как получения ввода с клавиатуры, отображение информации на экране и выполнения различных других работ. Этот набор инструкций называется «инструкции машинного языка» ('machine language instructions').
Процессор понимает только инструкции машинного языка, которые являются строками из единиц и нулей. При этом машинный язык слишком непонятный и сложный для использования его в разработки программного обеспечения. И низкоуровневый язык Ассемблер предназначен для определённый групп процессоров, он представляет различные инструкции в символическом коде и более понятной форме.
Преимущества языка Ассемблер
Знание языка ассемблера позволяет понять:
• Как программы взаимодействуют с ОС, процессором и BIOS;
• Как данные представлены в памяти и других внешних устройствах;
• Как процессор обращается к инструкции и выполняет её;
• Как инструкции получают доступ и обрабатывают данные;
• Как программа обращается к внешним устройствам.
Другие преимущества использования ассемблера:
• Программы на нём требует меньше памяти и времени выполнения;
• Это упрощает сложные аппаратные задачи;
• Подходит для работ, в которых время выполнения является критичным;
• Он наиболее подходит для написания подпрограмм обработки прерываний и других программ, полностью находящихся в оперативной памяти.
Системы счисления
Основные характеристики аппаратной составляющей ПК
Каждый компьютер содержит процессор и оперативную память. Процессор содержит регистры — компоненты, которые содержат данные и адреса. Для выполнения программы, система копирует её с устройства постоянного хранения во внутреннюю память. Процессор выполняет инструкции программы.
Фундаментальной единицей компьютерного хранилища является бит. Он может быть в состоянии Включён (1) или Выключен (0). Группа из восьми связанных битов составляет байт, из которых семь бит используются для данных, а ещё один используется для контроля чётности. Согласно правилу чётности, количество битов, которые Включены (1) в каждом байте, всегда должно быть чётным. То есть бит чётности имеет значение 1, если у соответствующего байта количество 1-х битов нечётно. 0 — если иначе (чётно).
Таким образом, бит чётности используется для того, чтобы сделать количество битов в байте чётным. Если соотношение является нечётным, система предполагает, что произошла ошибка соотношения (хотя и редко), которая могла быть вызвана неисправностью оборудования или электрическими помехами.
Процессор поддерживает следующие размеры данных -
• Word: 2-байтовый элемент данных
• Doubleword: a 4-байтовый (32 бита) элемент данных
• Quadword: 8-байтовый (64 бита) элемент данных
• Paragraph: 16-байтовая (128 бита) область
• Kilobyte: 1024 байт
• Megabyte: 1,048,576 байт
Двоичная система счисления
В каждой системе счисления используются позиционные обозначения, то есть каждая позиция, в которой записана цифра, имеет различное позиционное значение. Каждая позиция — это степень базы, которая равна 2 для двоичной системы счисления, и эти степени начинаются с 0 и увеличиваются на 1.
В таблице приведены позиционные значения для 8-битного двоичного числа, где все биты установлены в положение ON (Включено).
Значение бита
1
1
1
1
1
1
1
1
Значение позиции как степень основания 2
128
64
32
16
8
4
2
1
Номер бита
7
6
5
4
3
2
1
Значение двоичного числа, как и в десятичном, зависит от составляющих его цифр и расположения этих цифр. Но в двоичном числе используются только цифры 1 и 0, и расположение цифр имеет другое значение степени. Первая цифра, как и в десятичном числе, может означать 0 или 1. Вторая цифра (смотрим число справа на лево) может означать 2 (если этот бит установлен на 1) или 0 (если бит установлен на 0). Третья цифра (смотрим число справа на лево) может означать 4 (если этот бит установлен на 1) или 0 (если бит установлен на 0). И так далее. В десятичном числе значение каждого символа нужно умножить на 10 в степени порядкового номера этой цифры за минусом единицы.
То есть число 1337 это 1 * 103 + 3 * 102 + 3 * 101 + 7 * 100 = 1337
В двоичной системе всё точно также, только вместо десятки в степени порядкового номера за минусом единицы, нужно использовать двойку — вот и всё!
Допустим число 110101 и нужно узнать, сколько это будет в десятичной системе счисления, для этого достаточно выполнить следующее преобразование:
1 * 25* + 1 * 24 + 0 * 23 + 1 * 22 + 0 * 21 + 1 * 20 = 1 * 32 + 1 * 16 + 0 * 8 + 1 * 4 + 0 * 2 + 1 * 1 = 53
Итак, значение бинарного числа основывается на наличии битов 1 и их позиционном значении. Поэтому значение числа 11111111 в двоичной системе является:
1 + 2 + 4 + 8 +16 + 32 + 64 + 128 = 255
Кстати, это то же самое, что и 28 - 1.
Шестнадцатеричная система счисления
Шестнадцатеричная система счисления использует основание 16. Цифры в этой системе варьируются от 0 до 15. По соглашению, буквы от A до F используются для представления шестнадцатеричных цифр, соответствующих десятичным значениям с 10 по 15.
Шестнадцатеричные числа в вычислениях используются для сокращения длинных двоичных представлений. По сути, шестнадцатеричная система счисления представляет двоичные данные, деля каждый байт пополам и выражая значение каждого полубайта. В следующей таблице приведены десятичные, двоичные и шестнадцатеричные эквиваленты –
Десятичное число
Двоичный вид
Шестнадцатеричный вид
1
1
1
2
10
2
3
11
3
4
100
4
5
101
5
6
110
6
7
111
7
8
1000
8
9
1001
9
10
1010
A
11
1011
B
12
1100
C
13
1101
D
14
1110
E
15
1111
F
Чтобы преобразовать двоичное число в его шестнадцатеричный эквивалент, разбейте его на группы по 4 последовательные группы в каждой, начиная справа, и запишите эти группы в соответствующие цифры шестнадцатеричного числа.
Пример — двоичное число 1000 1100 1101 0001 эквивалентно шестнадцатеричному - 8CD1
Чтобы преобразовать шестнадцатеричное число в двоичное, просто запишите каждую шестнадцатеричную цифру в её 4-значный двоичный эквивалент.
Пример - шестнадцатеричное число FAD8 эквивалентно двоичному - 1111 1010 1101 1000
Отрицательные двоичные числа
Компьютерные процессы действуют по своей логике и своим алгоритмам. И привычные операции вычитания, деления, умножения выполняются удобным для микропроцессора способом.
Удобством для арифметических действий в процессоре обусловлено то, как записываются отрицательные двоичные числа. Вы должны помнить из курса информатики, что в одном байте содержится 8 бит. Но старший бит используется для установки знака. Чтобы правильно прочесть число, а также правильно поменять его знак, нужно выполнять следующие правила:
Во-первых, если старшие биты (крайние слева), равны нулю, то их иногда не записывают. Например, восьмибитное число 10 (в десятичной системе счисления оно равно 2), также можно записать как 0000 0010. Обе эти записи означают число 2.
Если старший бит равен нулю, то это положительное число. Например, число 110. В десятичной системе счисления это 6. Данное число является положительным или отрицательным? На самом деле, однозначно на этот вопрос можно ответить только, зная разрядность числа. Если это восьмиразрядное число, то его полная запись будет такой: 0000 0110. Как можно увидеть, старший бит равен нулю, следовательно, это положительное число.
Для трёхбитовых чисел было бы справедливо следующее:
Десятичное значение
Двоичное значение трёхбитового числа со знаком
(в представлении Дополнительный код)
000
1
001
2
010
3
011
-4
100
-3
101
-2
110
-1
111
Как можно понять после анализа предыдущей таблицы, для смены знака недостаточно просто поменять единицу на ноль — для преобразования числа в отрицательное, а также для чтения отрицательного числа существуют особые правила.
Отрицательные двоичные числа записываются без знака минус и для получения этого же числа со знаком минус (то есть для получения числа в Дополненном коде) нужно выполнить два действия:
1. нужно переписать его полную форму с противоположным значением битов (то есть для единиц записываются нули, а для нулей записываются единицы)
2. и затем добавить к этому числу 1.
Пример:
Число 53
00110101
Замена битов на противоположные
11001010
Добавляем 1
00000001
Число -53
11001011
На русском языке такая форма записи называется Дополнительный код, в англоязычной литературе это называется Two's complement.
Примеры восьмибитного двоичного числа в Дополнительном коде (старший бит указывает на знак):
Десятичное значение
Двоичное значение трёхбитового числа со знаком
(в представлении Дополнительный код)
0000 0000
1
0000 0001
2
0000 0010
126
0111 1110
127
0111 1111
−128
1000 0000
−127
1000 0001
−126
1000 0010
−2
1111 1110
−1
1111 1111
Ещё примеры:
Десятичное
представление
Двоичное представление (8 бит)
(в виде Дополнительного кода)
127
0111 1111
1
0000 0001
0
0000 0000
-0
---
-1
1111 1111
-2
1111 1110
-3
1111 1101
-4
1111 1100
-5
1111 1011
-6
1111 1010
-7
1111 1001
-8
1111 1000
-9
1111 0111
-10
1111 0110
-11
1111 0101
-127
1000 0001
-128
1000 0000
Числа в дополненном коде удобно применять для вычитания — это будет показано далее.
Для преобразования отрицательного числа, записанного в дополнительном коде, в положительное число, записанное в прямом коде, используется похожий алгоритм.
Рассмотрим пример с числом -5. Запись отрицательного восьмибитного числа:
1111 1011
Инвертируем все разряды отрицательного числа -5, получая таким образом:
0000 0100
Добавив к результату 1 получим положительное число 5 в прямом коде:
0000 0101
И проверка через сложение с дополнительным кодом
0000 0101 + 1111 1011 = 1 0000 0000, десятый разряд выбрасывается, то есть получается 0000 0000, то есть 0. Следовательно, преобразование выполнено правильно, так как 5 + (-5) = 0.
Двоичная арифметика
Следующая таблица иллюстрирует четыре простых правила для двоичного сложения:
(i)
(ii)
(iii)
(iv)
1
1
1
1
+0
+0
+1
+1
=0
=1
=10
=11
Эту таблицу нужно читать по столбцам сверху вниз. В первом столбце складываются 0 и 0 — в результате получается 0. Во втором примере складываются 1 и 0 (или 0 и 1 — без разницы), в результате получается 1. В третьем столбце складываются две единицы — в результате в текущей позиции получается 0, но на одну позицию влево добавляется единица. Если в этой позиции уже есть единица — то применяется это же правило, то есть в позиции пишется 0, и 1 передаётся влево. В четвёртом примере складываются три единицы — в результате, в текущей позиции записывается 1, и ещё одна 1 передаётся влево.
Пример:
Десятичные
Двоичные
60
00111100
+42
00101010
102
01100110
Вычитание.
Для вычитания число, которое вычитается, записывается в форме Дополнительного кода, а затем эти два числа складываются.
Пример: Вычесть 42 из 53
Число 53
00110101
Число 42
00101010
Инвертируем биты 42
11010101
Добавляем 1
00000001
Число -42
11010110
Выполняем операцию: 53 - 42 = 11
00110101 + 11010110 = 100001011, то есть = 00001011
Бит который вызывает переполнение — крайней левый, девятый по счёту, просто отбрасывается.
Адресация данных в памяти
Процесс, посредством которого процессор управляет выполнением инструкций, называется циклом fetch-decode-execute (выборки-декодирования-выполнения) или циклом выполнения (execution cycle). Он состоит из трёх непрерывных шагов -
• Извлечение инструкции из памяти
• Расшифровка или идентификация инструкции
• Выполнение инструкции
Процессор может одновременно обращаться к одному или нескольким байтам памяти. Давайте рассмотрим шестнадцатеричное число 0725H (буква H означает, что это шестнадцатеричное число). Для этого числа потребуется два байта памяти. Байт старшего разряда или старший значащий байт — 07, а младший байт — 25.
Процессор хранит данные в последовательности обратного байта, то есть байт младшего разряда хранится в низком адресе памяти и байт старшего разряда в старшем адресе памяти. Таким образом, если процессор переносит значение 0725H из регистра в память, он сначала перенесёт 25 на нижний адрес памяти и 07 на следующий адрес памяти.
Когда процессор получает числовые данные из памяти для регистрации, он снова переворачивает байты. Есть два вида адресов памяти:
• Абсолютный адрес — прямая ссылка на конкретное место.
• Адрес сегмента (или смещение) — начальный адрес сегмента памяти со значением смещения.
Основы синтаксиса Ассемблера
Программу на языке Ассемблер можно разделить на три раздела:
• Раздел data
• Раздел bss
• Раздел text
Раздел data
Раздел data используется для объявления инициализированных данных или констант. Эти данные не изменяются во время выполнения. В этом разделе вы можете объявить различные постоянные значения, имена файлов или размер буфера и т. д.
Синтаксис объявления раздела data:
1
section.data
Раздел BSS
Секция bss используется для объявления переменных. Синтаксис объявления раздела bss:
1
section.bss
Раздел text
Раздел text используется для хранения самого кода. Этот раздел должен начинаться с объявления global _start, которое сообщает ядру, где начинается выполнение программы.
Синтаксис объявления раздела text:
1
2
3
section.text
global _start
_start:
Комментарии
Комментарий на ассемблере начинается с точки с запятой (;). Он может содержать любой печатный символ, включая пробел. Он может появиться в строке сам по себе, например:
1
; Эта программа отображает сообщение на экране
или в той же строке вместе с инструкцией, например:
1
add eax, ebx ; добавляет ebx к eax
Операторы Ассемблера
Программы на ассемблере состоят из трёх типов операторов:
• Исполняемые инструкции или инструкции,
• Директивы ассемблера или псевдооперации (pseudo-ops), и
• Макросы.
Исполняемые инструкции или просто инструкции говорят процессору, что делать. Каждая инструкция состоит из кода операции (opcode). Каждая исполняемая инструкция генерирует одну инструкцию на машинном языке.
Директивы ассемблера или псевдооперации говорят ассемблеру о различных аспектах процесса сборки. Они не являются исполняемыми и не генерируют инструкции машинного языка.
Макросы — это в основном механизм подстановки текста.
Синтаксис операторов ассемблера
Операторы языка ассемблера вводятся по одной инструкции в каждой строке. Каждое утверждение имеет следующий формат:
1
[label] мнемоника [операнды] [;комментарий]
Поля в квадратных скобках являются необязательными. Основная инструкция состоит из двух частей: первая — это имя инструкции (или мнемоника), которая должна быть выполнена, а вторая — операнды или параметры команды.
Ниже приведены некоторые примеры типичных операторов языка ассемблера.
1
2
3
4
5
6
7
8
9
10
11
12
13
INC COUNT ; Увеличить переменную памяти COUNT
MOV TOTAL, 48 ; Перемести значение 48 в
; переменную памяти TOTAL
ADD AH, BH ; Добавить содержимое регистра
; BH в регистр AH
AND MASK1, 128 ; Выполнить операцию AND на переменной
; MASK1 и 128
ADD MARKS, 10 ; Добавить 10 к переменной MARKS
MOV AL, 10 ; Перенести значение 10 в регистр AL
Пример
section .text
global _start ;должно быть объявлено для линкера (linker) (ld)
_start: ;показывает линкеру точку входа
mov edx,len ;длина сообщения
mov ecx,msg ;сообщение для записи
mov ebx,1 ;файловый дескриптор (stdout - стандартный вывод)
mov eax,4 ;номер системного вызова (sys_write)
int 0x80 ;вызов ядра
mov eax,1 ;номер системного вызова (sys_exit)
int 0x80 ;вызов ядра
section .data
msg db 'Всем привет!', 0xa ;строка для печати
len equ $ - msg ;длина строки
Сегменты памяти
Модель сегментированной памяти делит системную память на группы независимых сегментов, на которые ссылаются указатели, расположенные в регистрах сегментов. Каждый сегмент используется для хранения данных определённого типа. Один сегмент используется для хранения кодов команд, другой — для хранения элементов данных, а третий — для программного стека.
В свете вышеизложенного можно выделить различные сегменты памяти, такие как:
• Сегмент Data. Он представлен разделом .data и .bss. Раздел .data используется для объявления области памяти, где хранятся элементы данных для программы. Этот раздел не может быть расширен после объявления элементов данных, и он остаётся статическим во всей программе.
Раздел .bss также является разделом статической памяти, который содержит буферы для данных, которые будут объявлены позже в программе. Эта буферная память заполнена нулями.
• Сегмент Code. Он представлен разделом .text. Он определяет область в памяти, в которой хранятся коды команд. Это также фиксированная зона.
• Stack — этот сегмент содержит значения данных, передаваемые функциям и процедурам в программе.
Ассемблер: регистры (Registers)
Операции процессора в основном связаны с обработкой данных. Эти данные могут быть сохранены в памяти и доступны оттуда. Однако чтение данных из памяти и её сохранение в памяти замедляет процессор, поскольку включает сложные процессы отправки запроса данных через шину управления и в блок хранения памяти и получения данных по одному и тому же каналу.
Для ускорения работы процессора процессор включает в себя несколько мест хранения внутренней памяти, называемых регистрами (registers).
Регистры хранят элементы данных для обработки без необходимости доступа к памяти. Ограниченное количество регистров встроено в чип процессора.
Регистры процессора
В архитектуре IA-32 имеется десять 32-разрядных и шесть 16-разрядных процессорных регистров. Регистры сгруппированы в три категории:
• Общие регистры,
• Регистры управления и
• Сегментные регистры.
Общие регистры далее делятся на следующие группы:
• Регистры данных,
• Регистры указателя и
• Индексные регистры.
Регистры данных
Четыре 32-битных регистра данных используются для арифметических, логических и других операций. Эти 32-битные регистры можно использовать тремя способами:
• Как полные 32-битные регистры данных: EAX, EBX, ECX, EDX.
• Нижние половины 32-битных регистров могут использоваться как четыре 16-битных регистра данных: AX, BX, CX и DX.
• Нижняя и верхняя половины вышеупомянутых четырёх 16-битных регистров могут использоваться как восемь 8-битных регистров данных: AH, AL, BH, BL, CH, CL, DH и DL.
Некоторые из этих регистров данных имеют конкретное применение в арифметических операциях.
AX — основной аккумулятор; он используется во вводе/выводе и большинстве арифметических инструкций. Например, в операции умножения один операнд сохраняется в регистре EAX или AX или AL в соответствии с размером операнда.
BX известен как базовый регистр, поскольку его можно использовать при индексированной адресации.
CX известен как регистр подсчёта, так как регистры ECX, CX хранят счётчик циклов в итерационных операциях.
DX известен как регистр данных. Он также используется в операциях ввода/вывода. Он также используется с регистром AX вместе с DX для операций умножения и деления с большими значениями.
Регистры указателя
Регистры указателя являются 32-разрядными регистрами EIP, ESP и EBP и соответствующими 16-разрядными правыми частями IP, SP и BP. Есть три категории регистров указателей:
• Указатель инструкций (IP) — 16-битный регистр IP хранит адрес смещения следующей команды, которая должна быть выполнена. IP вместе с регистром CS (как CS:IP) даёт полный адрес текущей инструкции в сегменте кода.
• Указатель стека (SP) - 16-разрядный регистр SP обеспечивает значение смещения в программном стеке. SP в сочетании с регистром SS (SS:SP) относится к текущей позиции данных или адреса в программном стеке.
• Базовый указатель (BP) — 16-битный регистр BP в основном помогает ссылаться на переменные параметра, передаваемые подпрограмме. Адрес в регистре SS объединяется со смещением в BP, чтобы получить местоположение параметра. BP также можно комбинировать с DI и SI в качестве базового регистра для специальной адресации.
Индексные регистры
32-разрядные индексные регистры ESI и EDI и их 16-разрядные крайние правые части. SI и DI, используются для индексированной адресации и иногда используются для сложения и вычитания. Есть два набора указателей индекса:
• Исходный индекс (SI) — используется в качестве исходного индекса для строковых операций.
• Указатель назначения (DI) — используется как указатель назначения для строковых операций.
Регистры управления
Регистр указателя 32-битной инструкции и регистр 32-битных флагов рассматриваются как регистры управления.
Многие инструкции включают сравнения и математические вычисления и изменяют состояние флагов, а некоторые другие условные инструкции проверяют значение этих флагов состояния, чтобы перенести поток управления в другое место.
Популярные биты флага:
• Флаг переполнения (OF) — указывает на переполнение старшего бита (крайнего левого бита) данных после арифметической операции со знаком.
• Флаг направления (DF) — определяет направление влево или вправо для перемещения или сравнения строковых данных. Когда значение DF равно 0, строковая операция принимает направление слева направо, а когда значение равно 1, строковая операция принимает направление справа налево.
• Флаг прерывания (IF) — определяет, будут ли игнорироваться или обрабатываться внешние прерывания, такие как ввод с клавиатуры и т. д. Он отключает внешнее прерывание, когда значение равно 0, и разрешает прерывания, когда установлено значение 1.
• Trap Flag (TF) — позволяет настроить работу процессора в одношаговом режиме. Программа DEBUG устанавливает флаг прерывания, чтобы можно было пошагово пройтись по инструкциям — по одной инструкции за раз.
• Флаг знака (SF) — показывает знак результата арифметической операции. Этот флаг устанавливается в соответствии со знаком элемента данных после арифметической операции. Знак указывается старшим левым битом. Положительный результат очищает значение SF до 0, а отрицательный результат устанавливает его в 1.
• Нулевой флаг (ZF) — указывает результат арифметической операции или операции сравнения. Ненулевой результат очищает нулевой флаг до 0, а нулевой результат устанавливает его в 1.
• Вспомогательный флаг переноса (AF) — содержит перенос с бита 3 на бит 4 после арифметической операции; используется для специализированной арифметики. AF устанавливается, когда 1-байтовая арифметическая операция вызывает перенос из бита 3 в бит 4.
• Флаг чётности (PF) — указывает общее количество 1-битов в результате, полученном в результате арифметической операции. Чётное число 1-бит очищает флаг чётности до 0, а нечётное число 1-битов устанавливает флаг чётности в 1.
• Флаг переноса (CF) — содержит перенос 0 или 1 из старшего бита (крайнего слева) после арифметической операции. Он также хранит содержимое последнего бита операции shift или rotate.
В следующей таблице указано положение битов флага в 16-битном регистре флагов:
Флаг:
O
D
I
T
S
Z
A
P
C
Номер бита:
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
Сегментные регистры
Сегменты — это специальные области, определённые в программе для хранения данных, кода и стека. Есть три основных сегмента:
• Сегмент Code — содержит все инструкции, которые должны быть выполнены. 16-битный регистр сегмента кода или регистр CS хранит начальный адрес сегмента кода.
• Сегмент Data — содержит данные, константы и рабочие области. 16-битный регистр сегмента данных или регистр DS хранит начальный адрес сегмента данных.
• Сегмент Stack — содержит данные и адреса возврата процедур или подпрограмм. Он реализован в виде структуры данных стека. Регистр сегмента стека или регистр SS хранит начальный адрес стека.
Помимо регистров DS, CS и SS существуют и другие регистры дополнительных сегментов — ES (дополнительный сегмент), FS и GS, которые предоставляют дополнительные сегменты для хранения данных.
При программировании на ассемблере программе необходим доступ к ячейкам памяти. Все области памяти в сегменте относятся к начальному адресу сегмента. Сегмент начинается с адреса, равномерно делимого на 16 или в шестнадцатеричном виде числа 10. Таким образом, крайняя правая шестнадцатеричная цифра во всех таких адресах памяти равна 0, что обычно не сохраняется в регистрах сегментов.
Сегментные регистры хранят начальные адреса сегмента. Чтобы получить точное местоположение данных или инструкции в сегменте, требуется значение смещения (или смещение). Чтобы сослаться на любую ячейку памяти в сегменте, процессор объединяет адрес сегмента в регистре сегмента со значением смещения местоположения.
Ассемблер: Переменные
NASM предоставляет различные директивы определения (define directives) для резервирования места для хранения переменных. Директива определения ассемблера используется для выделения пространства хранения. Его можно использовать для резервирования, а также для инициализации одного или нескольких байтов.
Выделение пространства хранения для инициализированных данных
Синтаксис для оператора распределения памяти для инициализированных данных:
1
[имя-переменной] директива-определения начальное-значение [,начальное-значение]...
Где имя-переменной — это идентификатор для каждого пространства хранения. Ассемблер связывает значение смещения для каждого имени переменной, определённого в сегменте данных.
Существует пять основных форм директивы определения:
Директива
Цель
Размер хранения
DB
Определить Byte
выделяет 1 байт
DW
Определить Word
выделяет 2 байта
DD
Определить Doubleword
выделяет 4 байта
DQ
Определить Quadword
выделяет 8 байта
DT
Определить Ten Bytes
выделяет 10 байта
Ниже приведены некоторые примеры использования директив определения.
1
2
3
4
5
6
choice DB 'y'
number DW 12345
neg_number DW -12345
big_number DQ 123456789
real_number1 DD 1.234
real_number2 DQ 123.456
Внимание:
• Каждый байт символа хранится как его значение ASCII в шестнадцатеричном формате.
• Каждое десятичное значение автоматически преобразуется в его 16-разрядный двоичный эквивалент и сохраняется в виде шестнадцатеричного числа.
• Процессор использует little-endian порядок байтов.
• Отрицательные числа преобразуются в его представление Дополнительный код.
• Короткие и длинные числа с плавающей запятой представлены с использованием 32 или 64 бит соответственно.
Следующая программа показывает использование директивы определения:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
section .text
global _start ;нужно указать для линкера (gcc)
_start: ;показывает линкеру точку входоа
mov edx,1 ;длина сообщения
mov ecx,choice ;сообщение для записи
mov ebx,1 ;файловый дескриптор (stdout)
mov eax,4 ;номер системного вызова (sys_write)
int 0x80 ;вызов ядра
mov eax,1 ;номер системного вызова (sys_exit)
int 0x80 ;вызов ядра
section .data
choice DB 'y'
Когда приведённый выше код компилируется и выполняется, он даёт следующий результат:
1
y
Выделение дискового пространства для неинициализированных данных
Директивы резервирования используются для запаса места для неинициализированных данных. Директивы резервирования принимают один операнд, который определяет количество единиц пространства, которое будет зарезервировано. Каждая директива определения имеет связанную директиву резервирования.
Существует пять основных форм директив резервирования:
Директива
Цель
RESB
Зарезервировать Byte
RESW
Зарезервировать Word
RESD
Зарезервировать Doubleword
RESQ
Зарезервировать Quadword
REST
Зарезервировать 10 байт
Множественность определений
Можете иметь несколько операторов определения данных в программе. Например:
1
2
3
choice DB 'Y' ;ASCII of y = 79H
number1 DW 12345 ;12345D = 3039H
number2 DD 12345679 ;123456789D = 75BCD15H
Ассемблер выделяет непрерывную память для нескольких определений переменных.
Множественность инициализаций
Директива TIMES позволяет выполнить несколько инициализаций к одному и тому же значению. Например, массив с именем marks размера 9 может быть определён и инициализирован на начальное значение ноль с помощью следующего оператора:
1
marks TIMES 9 DW 0
Директива TIMES полезна при определении массивов и таблиц.
Пример на повторение
section .text
global _start ;нужно указать для линкера (gcc)
_start: ;говорит линкеру о точке входа
;первая строка текста
mov edx,len ;длина сообщения
mov ecx,msg ;сообщение для записи
mov ebx,1 ;файловый дескриптор (stdout)
mov eax,4 ;номер системного вызова (sys_write)
int 0x80 ;вызов ядра
;вторая строка текста с повторением
mov edx,len1 ;длина сообщения
mov ecx,msg1 ;сообщение для записи
mov ebx,1 ;файловый дескриптор (stdout)
mov eax,4 ;номер системного вызова (sys_write)
int 0x80 ;вызов ядра
mov eax,1 ;номер системного вызова (sys_exit)
int 0x80 ;вызов ядра
section .data
msg db 'Сейчас будет повторение',0xa ;сообщение
len equ $ - msg ;длина сообщения
msg1 times 6 db 'hello!!! ' ;сообщение с количеством повторений
len1 equ $ - msg1 ;длина сообщения
Ассемблер: Режимы адресации
Большинство инструкций на ассемблере требуют обработки операндов. Адрес операнда предоставляет место, где хранятся данные, подлежащие обработке. Некоторые инструкции не требуют операнда, в то время как некоторые другие инструкции могут требовать один, два или три операнда.
Когда инструкции требуется два операнда, первый операнд обычно является пунктом назначения, который содержит данные в регистре или ячейке памяти, а второй операнд является источником. Источник содержит либо данные для доставки (немедленная адресация), либо адрес (в регистре или памяти) данных. Как правило, исходные данные остаются неизменными после операции.
Три основных режима адресации:
• Адресации на регистр
• Немедленная адресация
• Адресация на память
Адресации на регистр
В этом режиме адресации регистр содержит операнд. В зависимости от инструкции регистр может быть первым операндом, вторым операндом или обоими.
Например:
1
2
3
MOV DX, TAX_RATE ; Регистр в первом операнде
MOV COUNT, CX ; Регистр во втором операнде
MOV EAX, EBX ; Оба операнда в регистрах
Поскольку обработка данных между регистрами не требует памяти, она обеспечивает самую быструю обработку данных.
Немедленная адресация
Непосредственный операнд имеет постоянное значение или выражение. Когда инструкция с двумя операндами использует немедленную адресацию, первый операнд может быть регистром или ячейкой памяти, а второй операнд является непосредственной константой. Первый операнд определяет длину данных.
Например:
1
2
3
4
BYTE_VALUE DB 150 ; Определена величина byte
WORD_VALUE DW 300 ; Определена величина word
ADD BYTE_VALUE, 65 ; Добавлен немедленный операнд 65
MOV AX, 45H ; Немедленная константа 45H передана на AX
Адресация на память
Когда операнды указываются в режиме адресации на память, требуется прямой доступ к основной памяти, обычно к сегменту данных. Этот способ адресации приводит к более медленной обработке данных. Чтобы найти точное местоположение данных в памяти, нужен начальный адрес сегмента, который обычно находится в регистре DS, и значение смещения. Это значение смещения также называется действующим адресом (effective address).
В режиме прямой адресации значение смещения указывается непосредственно как часть инструкции, обычно указывается именем переменной. Ассемблер вычисляет значение смещения и поддерживает таблицу символов, в которой хранятся значения смещения всех переменных, используемых в программе.
При прямой адресации в памяти один из операндов ссылается на ячейку памяти, а другой операнд ссылается на регистр.
Например:
1
2
ADD BYTE_VALUE, DL ; Добавляет регистр в ячейку памяти
MOV BX, WORD_VALUE ; Операнд из памяти добавлен в регистр
Прямая адресация со смещением
Этот режим адресации использует арифметические операторы для изменения адреса. Например, посмотрите на следующие определения, которые определяют таблицы данных:
1
2
BYTE_TABLE DB 14, 15, 22, 45 ; Таблица bytes
WORD_TABLE DW 134, 345, 564, 123 ; Таблица words
Следующие операции обращаются к данным из таблиц в памяти в регистрах:
1
2
3
4
MOV CL, BYTE_TABLE[2] ; Получает 3й элемент BYTE_TABLE
MOV CL, BYTE_TABLE + 2 ; Получает 3й элемент BYTE_TABLE
MOV CX, WORD_TABLE[3] ; Получает 4й элемент WORD_TABLE
MOV CX, WORD_TABLE + 3 ; Получает 4й элемент WORD_TABLE
Косвенная адресация на память
В этом режиме адресации используется способность компьютера Segment:Offset (Сегмент:Смещение). Обычно для этой цели используются базовые регистры EBX, EBP (или BX, BP) и регистры индекса (DI, SI), закодированные в квадратных скобках для ссылок на память.
Косвенная адресация обычно используется для переменных, содержащих несколько элементов, таких как массивы. Начальный адрес массива хранится, скажем, в регистре EBX.
В следующем фрагменте кода показано, как получить доступ к различным элементам переменной.
1
2
3
4
5
6
MY_TABLE TIMES 10 DW 0 ; Выделено 10 words (по 2 байта) каждое
; инициализировано на 0
MOV EBX, [MY_TABLE] ; Эффективный адрес MY_TABLE в EBX
MOV [EBX], 110 ; MY_TABLE[0] = 110
ADD EBX, 2 ; EBX = EBX +2
MOV [EBX], 123 ; MY_TABLE[1] = 123
Инструкция MOV
Инструкция MOV, используется для перемещения данных из одного пространства хранения в другое. Инструкция MOV принимает два операнда.
Синтаксис
Синтаксис инструкции MOV:
1
MOV пункт_назначения, источник
Инструкция MOV может иметь одну из следующих пяти форм:
1
2
3
4
5
MOV регистр, регистр
MOV регистр, непосредственное_значение
MOV память, непосредственное_значение
MOV регистр, память
MOV память, регистр
Обратите внимание, что:
• Оба операнда в операции MOV должны быть одинакового размера
• Значение исходного операнда остаётся неизменным
Инструкция MOV порой вызывает двусмысленность. Например, посмотрите на утверждения:
1
2
MOV EBX, [MY_TABLE] ; Эффективный адрес MY_TABLE в EBX
MOV [EBX], 110 ; MY_TABLE[0] = 110
Не ясно, нужно ли переместить байтовый эквивалент или словесный эквивалент числа 110. В таких случаях целесообразно использовать спецификатор типа (type specifier).
В следующей таблице приведены некоторые общие спецификаторы типов:
Спецификатор типа
Байты
BYTE
1
WORD
2
DWORD
4
QWORD
8
TBYTE
10
Пример
section .text
global _start ;нужно объявить для линкера
_start: ;указывает компоновщику точку входа
;пишем имя 'Kate Rose'
mov edx,10 ;длина сообщения
mov ecx, name ;сообщение для записи
mov ebx,1 ;файловый дескриптор (stdout)
mov eax,4 ;номер системного вызова (sys_write)
int 0x80 ;вызов ядра
mov [name], dword 'Mary Rose' ; изменение имени Nuha Ali
;запись имени 'Mary Rose'
mov edx,9 ;длина сообщения
mov ecx,name ;сообщение для записи
mov ebx,1 ;файловый дескриптор (stdout)
mov eax,4 ;номер системного вызова (sys_write)
int 0x80 ;вызов ядра
mov eax,1 ;номер системного вызова (sys_exit)
int 0x80 ;вызов ядра
section .data
name db 'Kate Rose'
Ассемблер: Константы
NASM предоставляет несколько директив, определяющих константы. Особое внимание обратим на три директивы:
• EQU
• %assign
• %define
Директива EQU
Директива EQU используется для определения констант. Синтаксис директивы EQU следующий:
1
ИМЯ_КОНСТАНТЫ EQU выражение
Пример:
1
TOTAL_STUDENTS equ 50
Затем можно использовать это постоянное значение в коде, например:
1
2
mov ecx, TOTAL_STUDENTS
cmp eax, TOTAL_STUDENTS
Операндом оператора EQU может быть выражение:
1
2
3
LENGTH equ 20
WIDTH equ 10
AREA equ length * width
Приведённый фрагмент кода определит AREA как 200.
Пример
SYS_EXIT equ 1
SYS_WRITE equ 4
STDIN equ 0
STDOUT equ 1
section .text
global _start ;нужно продекларировать чтобы использовать gcc
_start: ;говорит линкеру точку входа
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, msg1
mov edx, len1
int 0x80
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, msg2
mov edx, len2
int 0x80
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, msg3
mov edx, len3
int 0x80
mov eax,SYS_EXIT ;номер системного вызова (sys_exit)
int 0x80 ;вызов ядра
section .data
msg1 db 'Привет, студент!',0xA,0xD
len1 equ $ - msg1
msg2 db 'Начинается знакомство с ', 0xA,0xD
len2 equ $ - msg2
msg3 db 'программированием на Ассемблере! '
len3 equ $- msg3
Ассемблер: Управление файлами
Система рассматривает любые входные или выходные данные как поток байтов. Есть три стандартных файловых потока:
• Стандартный ввод (stdin),
• Стандартный вывод (stdout) и
• Вывод ошибок (stderr).
Файловый дескриптор
Файловый дескриптор — это 16-разрядное целое число, назначаемое файлу в качестве идентификатора файла. Когда создаётся новый файл или открывается существующий файл, дескриптор файла используется для доступа к файлу.
Файловый дескриптор стандартных файловых потоков — stdin, stdout и stderr — равны 0, 1 и 2 соответственно.
Файловый указатель
Файловый указатель определяет местоположение для последующей операции чтения/записи в файле в виде байтов. Каждый файл рассматривается как последовательность байтов. Каждый открытый файл связан с указателем файла, который задаёт смещение в байтах относительно начала файла. Когда файл открыт, указатель файла устанавливается в ноль.
Системные вызовы обработки файлов
В следующей таблице кратко описаны системные вызовы, связанные с обработкой файлов.
%eax
Имя
%ebx
%ecx
%edx
2
sys_fork
struct pt_regs
-
-
3
sys_read
unsigned int
char *
size_t
4
sys_write
unsigned int
const char *
size_t
5
sys_open
const char *
int
int
6
sys_close
unsigned int
-
-
8
sys_creat
const char *
int
-
19
sys_lseek
unsigned int
off_t
unsigned int
Шаги, необходимые для использования системных вызовов:
• Поместите номер системного вызова в регистр EAX.
• Сохраните аргументы системного вызова в регистрах EBX, ECX и т. д.
• Вызовите соответствующее прерывание (80h).
• Результат обычно возвращается в регистр EAX.
Создание и открытие файла
Для создания и открытия файла выполните следующие задачи:
• Поместите номер 8 системного вызова sys_creat() в регистр EAX.
• Поместите имя файла в регистр EBX.
• Поместите права доступа к файлу в регистр ECX.
Системный вызов возвращает дескриптор файла созданного файла в регистр EAX, в случае ошибки код ошибки находится в регистре EAX.
Открытие существующего файла
Чтобы открыть существующий файл, выполните следующие задачи:
• Поместите номер 5 системного вызова sys_open() в регистр EAX.
• Поместите имя файла в регистр EBX.
• Поместите режим доступа к файлу в регистр ECX.
• Поместите права доступа к файлу в регистр EDX.
Системный вызов возвращает дескриптор файла созданного файла в регистре EAX, в случае ошибки код ошибки находится в регистре EAX.
Среди режимов доступа к файлам чаще всего используются: только чтение (0), только запись (1) и чтение-запись (2).
Чтение из файла
Для чтения из файла выполните следующие задачи:
• Поместите номер 3 системного вызова sys_read() в регистр EAX.
• Поместите дескриптор файла в регистр EBX.
• Поместите указатель на входной буфер в регистр ECX.
• Поместите размер буфера, то есть количество байтов для чтения, в регистр EDX.
Системный вызов возвращает количество байтов, считанных в регистре EAX, в случае ошибки код ошибки находится в регистре EAX.
Запись в файл
Для записи в файл выполните следующие задачи:
• Поместите номер 4 системного вызова sys_write() в регистр EAX.
• Поместите дескриптор файла в регистр EBX.
• Поместите указатель на выходной буфер в регистр ECX.
• Поместите размер буфера, т.е. количество байтов для записи, в регистр EDX.
Системный вызов возвращает фактическое количество байтов, записанных в регистр EAX, в случае ошибки код ошибки находится в регистре EAX.
Закрытие файла
Для закрытия файла выполните следующие задачи:
• Поместите номер 6 системного вызова sys_close() в регистр EAX.
• Поместите дескриптор файла в регистр EBX.
Системный вызов возвращает, в случае ошибки, код ошибки в регистре EAX.
Обновление файла
Для обновления файла выполните следующие задачи:
• Поместите номер 19 системного вызова sys_lseek() в регистр EAX.
• Поместите дескриптор файла в регистр EBX.
• Поместите значение смещения в регистр ECX.
• Поместите референтную позицию для смещения в регистр EDX.
Исходная позиция может быть:
• Начало файла — значение 0
• Текущая позиция — значение 1
• Конец файла — значение 2
Системный вызов возвращает, в случае ошибки, код ошибки в регистре EAX.
Пример
Следующая программа создаёт и открывает файл с именем myfile.txt и записывает текст «Привет!» в этом файле. Далее программа читает файл и сохраняет данные в буфере с именем info. Наконец, он отображает текст как сохранённый в info.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
section .text
global _start ;нужно декларировать для использования gcc
_start: ;точка входа для линкера
; создаём файл
mov eax, 8
mov ebx, file_name
mov ecx, 0777 ;чтение, запись и выполнение всеми
int 0x80 ;выхов ядра
mov [fd_out], eax
; пишем в файл
mov edx,len ;количество байт
mov ecx, msg ;сообщение для записи
mov ebx, [fd_out] ;файловый дескриптор
mov eax,4 ;номер системного вызова (sys_write)
int 0x80 ;вызов ядра
; закрываем файл
mov eax, 6
mov ebx, [fd_out]
int 0x80 ;вызов ядра
; вывод сообщения о том, что закончена запись в файл
mov eax, 4
mov ebx, 1
mov ecx, msg_done
mov edx, len_done
int 0x80
; открываем файл для чтения
mov eax, 5
mov ebx, file_name
mov ecx, 0 ;доступ только для чтения
mov edx, 0777 ;чтение, запись и выполнение для всех
int 0x80
mov [fd_in], eax
; читаем из файла
mov eax, 3
mov ebx, [fd_in]
mov ecx, info
mov edx, 200
int 0x80
; закрываем файл
mov eax, 6
mov ebx, [fd_in]
int 0x80
; выводим info
mov eax, 4
mov ebx, 1
mov ecx, info
mov edx, 200
int 0x80
mov eax,1 ;номер системного выхова (sys_exit)
int 0x80 ;выхов ядра
section .data
msg db 'Привет!'
len equ $-msg
msg_done db 'Записано в файл', 0xa
len_done equ $-msg_done
file_name db 'myfile.txt'
section .bss
fd_out resb 1
fd_in resb 1
info resb 200
Результат выполнения программы:
1
2
Записано в файл
Привет от HackWare.ru!
Ассемблер: Управление памятью
Системный вызов sys_brk() предоставляется ядром для выделения памяти без необходимости её перемещения позже. Этот вызов выделяет память прямо за изображением приложения в памяти. Эта системная функция позволяет вам установить максимальный доступный адрес в разделе данных.
Этот системный вызов принимает один параметр, который является наибольшим адресом памяти, который необходимо установить. Это значение сохраняется в регистре EBX.
В случае ошибки sys_brk() возвращает -1 или возвращает отрицательный код ошибки. В следующем примере демонстрируется динамическое распределение памяти.
Пример
Следующая программа выделяет 16 КБ памяти с помощью системного вызова sys_brk() -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
section .text
global _start ;нужно декларировать для использования gcc
_start: ;указываем линкеру точку входа
mov eax, 45 ;sys_brk
xor ebx, ebx
int 80h
add eax, 16384 ;количество байт для резервирования
mov ebx, eax
mov eax, 45 ;sys_brk
int 80h
cmp eax, 0
jl exit ;выйи, если ошибка
mov edi, eax ;EDI = самый высокий доступный адрес
sub edi, 4 ;указание на последнее DWORD
mov ecx, 4096 ;количество выделенных DWORD
xor eax, eax ;очищаем eax
std ;назад
rep stosd ;повторить для всей выделенной области
cld ;поместить DF флаг в нормальное состояние
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 80h ;печатаем сообщение
exit:
mov eax, 1
xor ebx, ebx
int 80h
section .data
msg db "Выделено 16 кб памяти!", 10
len equ $ - msg
Результат работы приведённого выше кода:
1
Выделено 16 кб памяти!