Segread читать сегментные регистры


Что такое сегментные регистры? Не очень понял

04.11.2020, 01:20

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

Что такое регистры микропроцессора? Перечислите регистры общего назначения
Помогите пожалуйста решить задания по TASM 1) Что такое регистры микропроцессора? Перечислите.

сегментные регистры
Добрый вечер! Возник вопрос по сегментным регистрам. Вроде бы когда мы программируем под Windows в.

Зачем используют сегментные регистры в Windows
Вот например кусок кода: .data? CurrDir db MAX_PATH dup (?) buff db 4 * 3 dup (?) .

Собирался покупать gtx 580, но по разным статьям в инете понял, что GTX 760 мощнее? Или я неправильно понял?
Привет всем. Сегодня решил собрать новый системный блок. ПРоц уже есть — Core i5-2380P, ОЗУ 8 гб.

04.11.2020, 07:43 2

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


Физический адрес
^^^^^^^^^^^^^^^^^^^^^^^

У первых плат 8086 шина-адреса была 20-битная. Такая ширина позволяет адресовать всего FFFFFh байт памяти. Запускаем виндовый калькулятор, переводим его с BIN, и введём 20 единиц. Так выглядит шина(А) на физическом уровне:

На аппаратном уровне линией(А20) управляет чипсет через порт(92h) системной логики, но ей можно управлять и программно: через прерывания BIOS, или порт(60h) контроллёра клавиатуры. Нужно сказать, что в современной архитектуре чипсет избавили от этой задачи, и управление взял на себя сам ЦП через ножку (А20-Mask). Проц пользуется ею при входе/выходе из защищённого режима.


Линейная и сегментная адресация
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Любая память имеет линейную структуру. То есть это прямая линия от нуля до N байт. Эту прямую можно логически разделить на некоторое кол-во отрезков. Размер памяти от этого не изменится, но зато изменится её лог/структура. Логический адрес привязан к типу памяти: ROM, RAM, Flash, HDD.

Чтобы осветить сегментный адрес, рассмотрим такой пример..
Допустим, нам нужна ячейка 46. В виде [Seg:Offs] это будет 0040:0006, ..а так-же 0030:0016, 0010:0036 и т.д.. Всё зависит от того, сколько бит выделяется для смещения. Линейный-же адрес не делится на две составляющих. Это просто целое число, а не сумма двух чисел.

Посмотрим на линейный адрес А7FFFh.
В сегментном виде это А700:0FFFh, но не только.


Сегментные регистры
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

В реальном режиме работы ЦП имеются всего 4 сегментных регистров: CS(Code), DS(Data), SS(Stack), ES(Enhanced — вспомогательный). Позже было добавлено ещё два — GS/FS, но в реальном режиме они не используются. Соответственно, каждый из сегментных регистров адресует свой сегмент памяти.

Файлы типа(COM) всегда грузятся в один сегмент, поэтому значения этих регистров у них имеют одно и тоже значения. Файлы типа(EXE) могут иметь размер больше 64КБ, поэтому загрузчик ОС разбрасывает его содержимое по-нескольким сегментам. В этом случае, все данные программы (константы, переменные, текстовые строки) находятся в сегменте данных на который указывает регистр(DS), а сам код находится в другом сегменте памяти, на который указывает(CS). Так-же и стек находится в третьем/произвольном сегменте(SS). Их содержимое можно посмотреть в отладчиках:

04.11.2020, 23:46 3 08.11.2020, 21:26 4
08.11.2020, 21:26
08.11.2020, 21:26

Регистры разных ядер процессора — такое существует?
Вобщем необходимо понять как работает assembler на разных ядрах, как переместить из регистра одного.

не очень понял про динамическое выделение памяти.
У меня есть массив строк состоящий из 100 элементов string *строки; строки = new string ; .

Сегментные регистры

Понятие программной модели IA-32.

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

Программную модель микропроцессора Intel составляют: 1 — пространство адресуемой памяти;

2 — набор регистров для хранения данных общего назначения;

3 — набор сегментных регистров;

4 — набор регистров состояния и управления;

5 — набор регистров устройства вычислений с плавающей точкой (сопроцессора);

6 — набор регистров целочисленного MMX-расширения, отображенных на регистры сопроцессора;

7 — набор регистров SSE-расширения с плавающей точкой;

8 — программный стек. Это специальная информационная структура, работа с которой предусмотрена на уровне машинных команд.

Регистры общего назначения.

Регистры общего назначения используются в программах для хранения:

1 — операндов логических и арифметических операций;

2 — компонентов адреса;

3 — указателей на ячейки памяти.

Все регистры этой группы позволяют обращаться к своим «младшим» частям. Использовать для самостоятельной адресации можно только младшие 16- и 8-битные части этих регистров. Старшие 16 битов этих регистров как самостоятельные объекты недоступны.

Регистры, относящиеся к группе регистров общего назначения:

— EAX/AX/AH/AL (Accumulator register) – аккумулятор. Применяется для хранения промежуточных данных. В некоторых командах использование этого регистра обязательно;

— EBX/BX/BH/BL (Base register) – базовый регистр. Применяется для хранения базового адреса некоторого объекта в памяти;

— ECX/CX/CH/CL (Count register) – регистр-счетчик. Применяется в командах, производящих некоторые повторяющиеся действия (например команда организации цикла LООР, кроме передачи управления команде, находящейся по некоторому адресу, анализирует и уменьшает на единицу значение регистра ЕСХ).


— EDX/DX/DH/DL (Data register) – регистр данных. Так же как и регистр ЕАХ/AX/AH/AL, он хранит промежуточные данные. В некоторых командах его использование обязательно; для некоторых команд это происходит неявно.

Регистры ESI и EDI используются для поддержки так называемых цепочечных операций, то есть операций, производящих последовательную обработку цепочек элементов, каждый из которых может иметь длину 32, 16 или 8 бит:

— ESI/SI (Source Index register) – индекс источника. Этот регистр в цепочечных операциях содержит текущий адрес элемента в цепочке-источнике;

Для работы со стеком в системе команд микропроцессора есть специальные команды, а в программной модели микропроцессора для этого существуют следующие специальные регистры:

— ESP/SP (Stack Pointer register) – регистр указателя стека. Содержит указатель вершины стека в текущем сегменте стека (регистр ESP не следует использовать явно для хранения каких-либо операндов программы, так как в нем хранится указатель на положение вершины стека программы);

— EBP/BP (Base Pointer register) – регистр указателя базы кадра стека. Предназначен для организации произвольного доступа к данным внутри стека.

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

В программной модели микропроцессора имеется шесть сегментных регистров: CS, SS, DS, ES, GS, FS. Сегментные регистры предназначены для обеспечения доступа к оперативной памяти.

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

— Сегмент кода. Содержит команды программы. Для доступа к этому сегменту служит регистр CS (code segment register) – сегментный регистр кода. Он содержит адрес сегмента с машинными командами, к которому имеет доступ микропроцессор (то есть эти команды загружаются в конвейер микропроцессора);

— Сегмент данных. Содержит обрабатываемые программой данные. Для доступа к этому сегменту служит регистр DS (data segment register) – сегментный регистр данных, который хранит адрес сегмента данных текущей программы;

— Сегмент стека. Этот сегмент представляет собой область памяти, называемую стеком. Для доступа к этому сегменту служит регистр SS (stack segment register) – сегментный регистр стека, содержащий адрес сегмента стека;

— Дополнительный сегмент данных. Неявно алгоритмы выполнения большинства машинных команд предполагают, что обрабатываемые ими данные расположены в сегменте данных, адрес которого находится в сегментном регистре DS. Адреса дополнительных сегментов данных содержатся также в регистрах ES, GS, FS (extension data segment registers).

Не нашли то, что искали? Воспользуйтесь поиском:

Лучшие изречения: На стипендию можно купить что-нибудь, но не больше. 8987 — | 7235 — или читать все.

Сегментные регистры

Регистры даннях

В них можно хранить любые данные (числа, адреса и пр.).

В верхнем ряду Таблицы (AX (аккумулятор), BX (база), CX (счетчик), DX (регистр данных)) находятся шестнадцатиразрядные регистры, которые могут хранить числа от 0 до 65.535 (от 0h до FFFFh в шестнадцатеричной системе (вспоминаем прошлый выпуск)). Под ним идет ряд восьмиразрядных регистров (AH, AL, BH, BL, CH, CL, DH, DL), которые могут хранить максимальное число 255 (FFh). Это половинки (старшая или младшая) шестнадцатиразрядных регистров.

Регистры-указатели

Регистры SI (индекс источника) и DI (индекс приемника) используются в строковых операциях. Регистры BP и SP необходимы при работе со стеком.

Сегментные регистры

CS DS ES SS

Сегментные регистры необходимы для обращения к тому или иному сегменту памяти (например, видеобуферу).

3. В микропроцессорах i8086/88 сегментация памяти осуществляется простым способом. Все адресное пространство в 1 Мбайт разбивается на несколько (от 16 до 65536) смежных блоков памяти. Каждый такой блок может иметь размер от 16 байт до 64 Кбайт и выравнивается на шестнадцатибайтной границе. Блок памяти длиной в 16 байт и выравненный на 16-ти байтной границе называется параграфом. Для обращения к любому адресу в памяти необходимо знать его физический адрес, который в микропроцессорах i8086/88 и реальном режиме работы микропроцессоров x86 совпадает с его линейным адресом. Система команд в микропроцессорах x86 устроена так, что для образования эффективного адреса необходимо обязательное присутствие только одного его компонента. Таким образом любой компонент: база, индекс, смещение — или даже оба из них могутбыть опущены.

Регистры сегментной адресации CS, DS, SS, ES используются для хранения начальных адресов полей памяти (сегментов), отведенных в программах для хранения:

команд программы (сегмент кода — CS);

данных (сегмент данных — DS);

стековой области памяти (сегмент стека — SS);

дополнительной области памяти данных при межсегментных пересылках (рас­ширенный сегмент — ES), поскольку размер сегмента в реальном режиме рабо­ты МП ограничен величиной 64 Кбайт.

4. Все регистры этой группы позволяют обращаться к своим «младшим» частям. Использовать для самостоятельной адресации можно только младшие 16– и 8-битные части этих регистров. Старшие 16 бит этих регистров как самостоятельные объекты недоступны.

Перечислим регистры, относящиеся к группе регистров общего назначения. Так как эти регистры физически находятся в микропроцессоре внутри арифметико-логического устройства (АЛУ), то их еще называют регистрами АЛУ:

1) eax/ax/ah/al (Accumulator register) – аккумулятор. Применяется для хранения промежуточных данных. В некоторых командах использование этого регистра обязательно;

2) ebx/bx/bh/bl (Base register) – базовый регистр. Применяется для хранения базового адреса некоторого объекта в памяти;

3) ecx/cx/ch/cl (Count register) – регистр-счетчик. Применяется в командах, производящих некоторые повторяющиеся действия. Его использование зачастую неявно и скрыто в алгоритме работы соответствующей команды.

4) edx/dx/dh/dl (Data register) – регистр данных.

Так же, как и регистр eax/ax/ah/al, он хранит промежуточные данные. В некоторых командах его использование обязательно; для некоторых команд это происходит неявно.

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

1) esi/si (Source Index register) – индекс источника.

Этот регистр в цепочечных операциях содержит текущий адрес элемента в цепочке-источнике;

2) edi/di (Destination Index register) – индекс приемника (получателя). Этот регистр в цепочечных операциях содержит текущий адрес в цепочке-приемнике.

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

1) esp/sp (Stack Pointer register) – регистр указателя стека. Содержит указатель вершины стека в текущем сегменте стека.


2) ebp/bp (Base Pointer register) – регистр указателя базы кадра стека. Предназначен для организации произвольного доступа к данным внутри стека.

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

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

Для организации ветвлений в pic-контроллерах можно использовать 4 команды: две оперирующие с байтами (decfsz, incfsz) и две оперирующие с битами (btfsc, btfss). Работают эти команды следующим образом:

decfsz f,d — вычесть 1 из регистра f, если результат =0, то пропустить следующую команду (d определяет, куда сохранять результат. Если d=0 — результат сохраняется в аккумуляторе W, если d=1 — результат сохраняется в регистре f)

incfsz f,d — добавить 1 к регистру f, если результат =0, то пропустить следующую команду

btfsc f,b — проверить бит b в регистре f, если он =0, то пропустить следующую команду

btfss f,b — проверить бит b в регистре f, если он =1, то пропустить следующую команду

все перечисленные команды позволяют установить счётчик команд либо на следующий адрес, либо через один.

6. Для организации цикла предназначена команда LOOP. У этой команды один операнд — имя метки, на которую осуществляется переход. В качестве счётчика цикла используется регистр CX. Команда LOOP выполняет декремент CX, а затем проверяет его значение. Если содержимое CX не равно нулю, то осуществляется переход на метку, иначе управление переходит к следующей после LOOP команде.

Содержимое CX интерпретируется командой как число без знака. В CX нужно помещать число, равное требуемому количеству повторений цикла. Понятно, что максимально может быть 65535 повторений. Ещё одно ограничение связано с дальность перехода. Метка должна находиться в диапазоне -127…+128 байт от команды LOOP (если это не так, FASM сообщит об ошибке).

7. Флаг – это бит, принимающий значение 1 («флаг установлен»), если выполнено некоторое условие, и значение 0 («флаг сброшен») в противном случае. Процессор имеет регистр флагов, содержащий набор флагов, отражающий текущее состояние процессора. Значение флагов CF, DF и IF можно изменять напрямую в регистре флагов с помощью специальных инструкций (например, CLD для сброса флага направления), но нет инструкций, которые позволяют обратиться к регистру флагов как к обычному регистру. Однако можно сохранять регистр флагов в стек или регистр AH и восстанавливать регистр флагов из них с помощью инструкций LAHF, SAHF, PUSHF, PUSHFD, POPF и POPFD.

Флаги состояния (биты 0, 2, 4, 6, 7 и 11) отражают результат выполнения арифметических инструкций, таких как ADD, SUB, MUL, DIV. Флаги состояния позволяют одной и той же арифметической инструкции выдавать результат трёх различных типов: беззнаковое, знаковое и двоично-десятичное (BCD) целое число. Если результат считать беззнаковым числом, то флаг CF показывает условие переполнения (перенос или заём), для знакового результата перенос или заём показывает флаг OF, а для BCD-результата перенос/заём показывает флаг AF. Флаг SF отражает знак знакового результата, флаг ZF отражает и беззнаковый, и знаковый нулевой результат.

Флаг направления DF (бит 10 в регистре флагов) управляет строковыми инструкциями (MOVS, CMPS, SCAS, LODS и STOS) – установка флага заставляет уменьшать адреса (обрабатывать строки от старших адресов к младшим), обнуление заставляет увеличивать адреса. Инструкции STD и CLD соответственно устанавливают и сбрасывают флаг DF. Системные флаги и поле IOPL управляют операционной средой и не предназначены для использования в прикладных программах.

8. Строка – упорядоченная последовательность символов. Количество символов в строке называется ее длиной. Длина строки может лежать в диапазоне от 0 до 255. Каждый символ строковой величины занимает 1 байт памяти и имеет числовой код в соответствии с таблицей кодов ASCII.

Var : string[ ] Например:

Var s1: string[10];

Для описания строковых переменных в Паскале существует предопределенный тип string.

Copy (S, poz, n) выделяет из строки S, начиная с позиции poz, подстроку из n символов. Здесь S – любое строковое выражение, poz, n – целочисленные выражения.

Concat (s1, s2. sn) выполняет слияние строк s1, s2. sn в одну строку.

Length(S) определяет текущую длину строкового выражения S. Результат – значение целого типа.

Pos(subS, S) определяет позицию первого вхождения подстроки subS в строку S. Результат – целое число, равное номеру позиции, где находится первый символ искомой подстроки.

Delete (S, poz, n) удаляет из строки S, начиная с позиции poz, подстроку из n символов.

Insert(subS, S, poz) вставляет в строку S, начиная с позиции poz, подстроку subS.

Str(x, S) преобразует число x в строковый формат.

Val(S, x, kod) преобразует строку символов S в число x.

В С++ существует 2 типа строк.

Первый из них — это массив переменных типа char. Переменная типа char хранит в себе 1 символ. Размер такой строки равняется размеру массива — 1, т.к. последний элемент содержит NULL (пустая переменная без значения), который обозначает символ конца строки. char name[50];

Второй из вариантов, более удобный — это специальный класс string

Для его работы необходимо в начале программы подключить заголовочный файл string:

Для создания строки вам необходимо в начале программы написать using namespace std;

Теперь чтоб создать строку достаточно написать:

s.append(str) — добавляет в конец строки строку str. Можно писать как s.append(переменная), так и s.append(«строка»);

s.assign(str) — присваивает строке s значение строки str. Аналогично записи s=str;

int i=s.begin() — записывает в i индекс первого элемента строки

int i=s.end() — аналогично, но последнего

s.clear() — как следует из названия, отчищает строку. Т.е. удаляет все элементы в ней

s.compare(str) -сравнивает строку s со строкой str и возвращает 0 в случае совпадение (на самом деле сравнивает коды символов и возвращает из разность)

s.copy(куда, сколько, начиная с какого) — копирует из строки s в куда (там может быть как строка типа стринг, так и строка типа char). Последние 2 параметра не обязательные (можно использовать функцию с 1,2 или 3 параметрами)

bool b=s.empty() — если строка пуста, возвращает true, иначе false

s.erase(откуда, сколько) удаляет n элементов с заданной позиции

s.find(str,позиция) — ищет строку str начиная с заданной позиции

s.insert(позиция,str, начиная, beg, count) — вставляет в строку s начиная с заданной позиции часть строки str начиная с позиции beg и вставляя count символов


int len=s.length() — записывает в len длинну строки

s.push_back(symbol) — добавляет в конец строки символ

s.replace(index, n,str) — берет n первых символов из str и заменяет символы строки s на них, начиная с позиции index

str=s.substr(n,m) — возвращает m символов начиная с позиции n

s.swap(str) меняет содержимое s и str местами.

s.size() — возвращает число элементов в строке

9. Массив – это именованная группа однотипных данных, хранящихся в последовательных ячейках памяти.

A : array [1..10] of integer ;

For i:=1 to 10 do

For i :=1 to 10 do

using namespace std;

cout >N; //ввод размера массива

for (i=0; i >X[i]; //ввод элементов массива в цикле

cout в программе автоматически создаются виртуальные каналы связи cin для ввода с клавиатуры и cout для вывода на экран, а также операции помещения в поток >. С помощью объекта cin и операции >> можно присвоить значение любой переменной. Например, если переменная x описана как целочисленная, то команда cin>>x; означает, что в переменную x будет записано некое целое число, введенное с клавиатуры. Если необходимо ввести несколько переменных, то следует написать cin>>x>>y>>z;.

Объект cout и операция

11. Для того чтобы работать с файлами необходимо сначала их описать.

Далее необходимо ввести этот файл.

Открываем файл для чтения.

Открываем второй файл для записи, при этом всё что было в файле стирается.

Также можно открыть второй файл для дозаписи (дозапись производится в конец файла).

Считывание производится обычным оператором read с указанием файла из которого производится считывание.

Запись осуществляется обычным оператором write с указанием файла в который производится запись.

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

Для работы с файлами используются специальные типы данных, называемые потоками. Поток ifstream служит для работы с файлами в режиме чтения, а ofstream в режиме записи. Для работы с файлами в режиме как записи, так и чтения служит поток fstream. В программах на C++ при работе с текстовыми файлами необходимо подключать библиотеки iostream и fstream.

Для того чтобы начать работать с текстовым файлом, необходимо описать переменную типа ofstream. Например, так: ofstream F;

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

Файл может быть открыт в одном из следующих режимов:

ios::in — открыть файл в режиме чтения данных; режим является режимом по умолчанию для потоков ifstream;

ios::out — открыть файл в режиме записи данных (при этом информация о существующем файле уничтожается); режим является режимом по умолчанию для потоков ofstream;

ios::app — открыть файл в режиме записи данных в конец файла;

ios::ate — передвинуться в конец уже открытого файла;

ios::trunc — очистить файл, это же происходит в режиме ios::out;

ios::nocreate — не выполнять операцию открытия файла, если он не существует;

ios::noreplace — не открывать существующий файл.

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

12. if (условие) оператор_1; else оператор_2;

Здесь условие — это логическое выражение, переменная или константа.

Работает условный оператор следующем образом. Сначала вычисляется значения выражения, записанного в виде условия. Если оно имеет значение истина (true), выполняется оператор_1. В противном случае (значение ложное (false) ) оператор_2.

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

Оператор варианта switch

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

case значение_1: операторы_1; break;

case значение_2: операторы_2; break;


case значение_3: операторы_3; break;

case значение_n: операторы_n; break;

Оператор работает следующем образом. Вычисляется значение выражения. Затем выполняются операторы, помеченные значением, совпадающим со значением выражения. То есть если, выражение принимает значение_1, то выполняются операторы_1 и т.д..

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

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

for счетчик:=значение to конечное_значение do

for счетчик:=значение downto конечное_значение do

Цикл while является циклом с предусловием. В заголовке цикла находится логическое выражение. Если оно возвращает true, то тело цикла выполняется, если false – то нет.

Когда тело цикла было выполнено, то ход программы снова возвращается в заголовок цикла. Условие выполнения тела снова проверяется (находится значение логического выражения).

Цикл while может не выполниться ни разу, если логическое выражение в заголовке сразу вернуло false. Однако такая ситуация не всегда может быть приемлемой. Бывает, что тело цикла должно выполниться хотя бы один раз, не зависимо оттого, что вернет логическое выражение. В таком случае используется цикл repeat – цикл с постусловием.

В цикле repeat логическое выражение стоит после тела цикла. Причем, в отличие от цикла while, здесь всё наоборот: в случае true происходит выход из цикла, в случае false – его повторение.

Цикл с постусловием (do…while)(только с++)

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

do оператор while (выражение);

Работает цикл следующим образом. В начале выполняется оператор, затем вычисляется значение выражения. Если оно истинно, оператор тела цикла выполняется еще раз.

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

14. Кроме стандартных типов данных Паскаль поддерживает скалярные типы, определенные самим пользователем. К ним относятся перечислимые типы (когда непосредственно, в разделе описания типов, заранее записываются все значения для переменных этого типа) и интервальные (когда задаются границы диапазона значений для данной переменной), указательные тип (кроме Pointer), структурированные типы и процедурные типы.

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

Var Season: (winter,spring,summer,autum );

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

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

Stack=array[1..40] of real;

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

Type Tproc1=procedure (var x,y:real);

Переименование типов (typedef)

Для того чтобы сделать программу более ясной, можно задать типу новое имя с помощью ключевого слова typedef:

typedef тип новое_имя [ размерность ];

В данном случае квадратные скобки являются элементом синтаксиса. Размерность может отсутствовать.

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

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

Битовые поля — это особый вид полей структуры. Они используются для плотной упаковки данных, например, флажков типа «да/нет». Минимальная адресуемая ячейка памяти — 1 байт, а для хранения флажка достаточно одного бита. При описании битового поля после имени через двоеточие указывается длина поля в битах (целая положительная константа):

unsigned int shadow:2;

unsigned int palette:4;

15. Файл — именованная область данных на носителе информации. В различных операционных и/или файловых системах могут быть реализованы различные типы файлов; кроме того, реализация различных типов может различаться.

«Обыкновенный файл» — файл, позволяющий операции чтения, записи, перемещения внутри файла

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

Жёсткая ссылка (англ. hardlink, часто используется калька «хардлинк») — в общем случае, одна и та же область информации может иметь несколько имён. Такие имена называют жёсткими ссылками (хардлинками). После создания хардлинка сказать где «настоящий» файл, а где хардлинк невозможно, так как имена равноправны. Сама область данных существует до тех пор, пока существует хотя бы одно из имён. Хардлинки возможны только на одном физическом носителе.

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

Создание файла и запись данных в него

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

Процедура rewrite открывает файл в режиме записи, т.е. мы можем вводить данные в файл с помощью процедуры вывода из программы write. Если указанный файл отсутствует на диске, то он будет создан. Если файл существует и содержит данные, то все они будут удалены и заменены в дальнейшем новыми (перезаписаны).

Запись данных в файл – результат выполнения процедуры write (f, c), где f – файловая переменная, а с – выводимый из программы, но вводимый в файл символ.

В конце требуется закрыть файл и «освободить» переменную f. Это делается с помощью процедуры close.


Чтение данных из файла

Процедура reset открывает файл для чтения. Т.е. мы можем в дальнейшем в программе извлекать данные из файла с помощью процедуры read.

Данные извлекаются «порциями» базового типа. В данном примере – это char (символы).

Чтение данных из файла продолжается до тех пор, пока не будет достигнут конец файла. Функция eof проверяет достигнут ли конец файла, переданного ей в качестве аргумента и, если достигнут, возвращает true. Выражение not eof (f) проверяет обратное – то, что конец файла еще не достигнут.

16. Функция — это часть программы, которая вычисляет и возвращает значение.

Заголовок функции определяет идентификатор функции, формальные параметры (если есть), и тип результата функции. Допустимы порядковые, вещественные, строковые и указательные типы результата. Функция вызывается при оценке выражения, использующего данную функцию.

Заголовок функции сопровождается:

Разделом описаний, в котором объявляются локальные объекты

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

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

Function UpCaseStr(S : String) : String;

For I:=1 To Length(S) Do

If (S[I]>=’a’) And (S[I] ), если они не являются членами другого класса. Функция friend объявляется с помощью класса, который предоставляет доступ. Объявление friend можно поместить в любом месте объявления класса. На него не влияют ключевые слова управления доступом.

25. В C++ существует возможность перегрузить не только функции, но и операции внутри класса, например, можно добиться того, чтобы операция * при работе с матрицами осуществляла умножение матриц, а при работе с комплексными числами — умножение комплексных чисел.

Для перегрузки операций внутри класса нужно написать специальную функцию — метод класса. При перегрузке операций следует помнить следующее:

нельзя поменять приоритет операций;

нельзя изменить тип операции (из унарной операции нельзя сделать бинарную или наоборот);

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

нельзя создавать новые операции;

запрещено перегружать операции: . (доступ к членам класса), унарную операцию * (значение по адресу указателя), :: (расширение области видимости), ?: (операция if);

допустима перегрузка следующих операций: +, -, *, /, %, =, , +=, -=, *=, /=, &&, ||, ++, —, (), [], new, delete.

type operator symbols(type1 parametr)

Здесь type — тип возвращаемого операцией значения, operator — служебное слово, symbols — перегруженная операция, type1 — тип второго операнда, первым операндом является экземпляр текущего класса, parametr — имя переменной второго операнда.

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

тип имя_функции (список_переменных)

Заголовок функции содержит:

тип возвращаемого функцией значения, он может быть любым; если функция не возвращает значения, указывают тип void;

имя_функции, с которым она будет вызываться;

список_переменных — перечень передаваемых в функцию аргументов, которые отделяются друг от друга запятыми; для каждой переменной из списка указывается тип и имя;

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

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

/* Функция max для целых чисел */

int max(int num1, int num2)

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

/* Функция max для чисел с плавающей запятой */

double max(double num1, double num2)

Теперь, когда мы будет вызывать функцию max с целыми параметрами, то вызовется первая функция. А если с дробными — то вторая. Например:

// Здесь будет использоваться первый вариант функции max

int imax = max(1, 10);

// А здесь — второй

double dmax = max(1.0, 20.0);

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


Тут появляется проблема. Для динамических типов данных не объявляются переменные, иначе память бы выделялась под переменные. Как тогда обращаться к данным, записанным неизвестно где в памяти? Можно ввести переменные-указатели и при выделении памяти записывать адрес этой памяти в указатели. Но мы же не знаем, сколько памяти потребуется в процессе выполнения. Сколько вводить указателей? Проблема решается путем использования структур. Допустим, мы пишем программу, позволяющую вводить данные на сотрудников организации. Количество сотрудников неизвестно. Можно было бы создать массив записей с запасом. Однако, если данных о каждом сотруднике много, то каждая запись занимает много памяти; получается, что мы будем расходовать много памяти в пустую, если сотрудников мало. Чтобы извлечь данные из такого агломерата данных, надо пройтись по ссылкам начиная с переменной-указателя. Т.е. первой мы извлечем последнюю записанную структуру. Потом предпоследнюю и постепенно будем двигаться к структуре, которая была создана первой во времени. Такой динамический тип данных называется стеком. Объекты извлекаются из стека таким образом, что первым выбирается тот, который был помещен последним. Стек — это не единственный способ организации динамических данных, но наиболее простой.

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

В языке программирования C выделение памяти в процессе выполнения программы можно организовать с помощью функций malloc() и calloc(), освобождение памяти с помощью free(). Объявление этих функций находится в заголовочных файлах stdlib.h и malloc.h. К исходному коду можно подключить любой из них.

Функция malloc() принимает в качестве параметра число, обозначающее объем памяти, который требуется выделить. Если свободная память есть, и malloc() удается ее «захватить», то функция возвращает указатель на нее. Этот указатель не имеет типа, поэтому программист самостоятельно должен привести его к требуемому в программе типу данных.

Функция free() принимает указатель, и освобождает память по адресу, который он содержит.

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

Сегментные регистры по умолчанию

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

Что это за договоренность?

Считается, что регистр CS всегда указывает на начало области памяти, в которой размещены команды программы (эта область называется сегментом команд или сегментом кодов), и потому при ссылках на ячейки этой области регистр CS можно не указывать явно, он подразумевается по умолчанию. (Отметим попутно, что абсолютный адрес очередной команды, подлежащей выполнению, всегда задается парой CS: IP: в счетчике команд IP всегда находится смещение этой команды относительно адреса из регистра CS.) Аналогично предполагается, что регистр DS указывает на сегмент данных (область памяти с константами, переменными и другими величинами программы), и потому во всех ссылках на этот сегмент регистр DS можно явно не указывать, т.к. он подразумевается по умолчанию. Регистр SS, считается, указывает на стек — область памяти, доступ к которой осуществляется по принципу «последним записан — первым считан» (см. 1.7), и потому все ссылки на стек, в которых явно не указан сегментный регистр, по умолчанию сегментируются по регистру SS. Регистр ES считается свободным, он не привязан ни к какому сегменту памяти и его можно использовать по своему усмотрению; чаще всего он применяется для доступа к данным, которые не поместились или сознательно не были размещены в сегменте данных.

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

Здесь, правда, возникает такой вопрос: как по смещению определить, на какой сегмент памяти оно указывает? Точный ответ приведен ниже (см. 1.4.3), а в общих чертах он такой: ссылки на сегмент команд могут быть только в командах перехода, а ссылки практически во всех других командах (кроме строковых и стековых) — это ссылки на сегмент данных. Например, в команде пересылки MOV AX, X имя X воспринимается как ссылка на данное, а потому автоматически восстанавливается до адресной пары DS: X. В команде же безусловного перехода по адресу, находящемуся в регистре BX, JMP BX абсолютный адрес перехода определяется парой CS: [BX].

Итак, если в ссылке на какую-то ячейку памяти не указан явно сегментный регистр, то этот регистр берется по умолчанию. Явно же сегментные регистры надо указывать, только если по каким-то причинам регистр по умолчанию не подходит. Если, например, в команде пересылки нам надо сослаться на стек (скажем, надо записать в регистр AH байт стека, помеченный именем X), тогда нас уже не будет устраивать договоренность о том, что по умолчанию операнд команды MOV сегментируется по регистру DS, и потому мы обязаны явно указать иной регистр — в нашем случае регистр SS, т.к. именно он указывает на стек: MOV AH, SS: X Однако такие случаи встречаются редко и потому в командах, как правило, указываются только смещения.

Отметим, что в MASM сегментный регистр записывается в самой команде непосредственно перед смещением (именем переменной, меткой и т.п.), однако на уровне машинного языка ситуация несколько иная. Имеется 4 специальные однобайтовые команды, называемые префиксами замены сегмента (обозначаемые как CS:, DS:, SS: и ES:). Они ставятся перед командой, операнд-адрес которой должен быть просегментирован по регистру, отличному от регистра, подразумеваемому по умолчанию. Например, приведенная выше символическая команда пересылки — это на самом деле две машинные команды: SS: MOV AH, X

Сегментные регистры

Читайте также:

  1. Воздушные регистры
  2. Вопрос 9. Охарактеризуйте следующие объекты конфигурации «Бухгалтерия предприятия 8»: отчеты и регистры (виды, назначение, особенности построения).
  3. Документация, регистры и формы бухгалтерского учета
  4. Лекция 20. Учетные регистры бухгалтерского учета
  5. Лекция 4 РЕГИСТРЫ
  6. Машинные регистры
  7. Межсегментные интерфейсы
  8. Многосегментные концентраторы
  9. Параллельные регистры
  10. Порядок и техника записей в учетные регистры.
  11. Последовательные регистры
  12. Регистры
Илон Маск рекомендует:  Set - Ключевое слово Delphi

Регистры общего назначения

Процессор 8086 имеет восемь 16-разрядных регистров общего назначения, обозначаемых AX, BX, CX, DX, SI, DI, BP и SP.

Старший и младший байты регистров AX, BX, CX и DX могут использоваться отдельно. Они носят обозначения AH, BH, CH и DH для старших байтов и AL, BL, CL и DL для младших байтов.

Илон Маск рекомендует:  Функции шифровки mcrypt

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

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

Регистр BX используется при адресации операндов.

Регистр CX содержит счётчик повторения цикла или количество разрядов, на которое производится сдвиг операнда в инструкциях сдвига.

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

Регистр SI содержит смещение строки-источника в строковых операциях и может использоваться при адресации операндов.

Регистр DI содержит смещение строки-приёмника в строковых операциях и может использоваться при адресации операндов.

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

Регистр SP является указателем стека.

Процессор 8086 содержит четыре 16-разрядных сегментных регистра: CS, DS, ES и SS, называемые соответственно сегментными регистрами кода, данных, дополнительных данных и стека.

Процессор использует содержимое сегментных регистров (так называемые селекторы сегментов) для формирования 20-разрядных физических адресов памяти, о чём говорилось выше, в подразделе Ошибка! Источник ссылки не найден.Ошибка! Источник ссылки не найден.”. При обращениях к кодам команд всегда используется сегментный регистр CS, а при обращении к стеку (с использованием указателя стека или при адресации посредством регистра BP) – сегментный регистр SS, причём если адресация данных производится с помощью регистра BP, вместо сегментного регистра SS можно указать любой другой сегментный регистр. При обращении к данным в большинстве случаев по умолчанию используется регистр сегментный регистр DS, однако вместо него в этих ситуациях можно использовать любой другой сегментный регистр. Сегментный регистр ES используется при доступе к строкам-приёмникам в строковых операциях, в этом качестве он не может быть заменён каким-либо другим сегментным регистром.

Сегментные регистры не могут являться операндами арифметико-логических инструкций. В регистры DS, ES и SS значение может быть загружено из какого-либо регистра общего назначения с помощью инструкции MOV либо извлечено из вершины стека инструкцией POP. Регистр CS не может быть загружен с помощью инструкции MOV или POP, новое значение в него загружается только при выполнении операций длинных переходов (инструкции JMP, CALL и RET), при возврате из прерывания (инструкция IRET) и при возникновении прерывания. Короткие переходы не изменяют регистр CS. Содержимое всех четырёх сегментных регистров может быть переслано в любой регистр общего назначения инструкцией MOV или занесено в стек инструкцией PUSH.

Дата добавления: 2014-01-11 ; Просмотров: 516 ; Нарушение авторских прав? ;

Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет

Сегментные регистры

В программной модели микропроцессора имеется шесть сегментных регистров: cs, ss, ds, es, gs, fs. Их существование обусловлено спецификой организации и использования оперативной памяти микропроцессорами Intel. Она заключается в том, что микропроцессор аппаратно поддерживает структурную организацию программы в виде трех частей, называемых сегментами. Соответственно, такая организация памяти называется сегментной.

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

Сегмент кода. Содержит команды программы. Для доступа к этому сегменту служит регистр cs (code segment register) — сегментный регистр кода. Он содержит адрес сегмента с машинными командами, к которому имеет доступ микропроцессор (то есть эти команды загружаются в конвейер микропроцессора).

Сегмент данных. Содержит обрабатываемые программой данные. Для доступа к этому сегменту служит регистр ds (data segment register) — сегментный регистр данных, который хранит адрес сегмента данных текущей программы.

Сегмент стека. Этот сегмент представляет собой область памяти, называемую стеком. Работу со стеком микропроцессор организует по следующему принципу: последний записанный в эту область элемент выбирается первым. Для доступа к этому сегменту служит регистр ss (stack segment register) — сегментный регистр стека, содержащий адрес сегмента стека.

Дополнительный сегмент данных. Неявно алгоритмы выполнения большинства машинных команд предполагают, что обрабатываемые ими данные расположены в сегменте данных, адрес которого находится в сегментном регистре ds. Если программе недостаточно одного сегмента данных, то она имеет возможность использовать еще три дополнительных сегмента данных. Но в отличие от основного сегмента данных, адрес которого содержится в сегментном регистре ds, при использовании дополнительных сегментов данных их адреса требуется указывать явно с помощью специальных префиксов переопределения сегментов в команде. Адреса дополнительных сегментов данных должны содержаться в регистрах es, gs, fs (extension data segment registers).

Регистры состояния и управления

В микропроцессор включены несколько регистров (см. рис. 1), которые постоянно содержат информацию о состоянии как самого микропроцессора, так и программы, команды которой в данный момент загружены на конвейер. К этим регистрам относятся:


регистр флагов eflags/flags;

регистр указателя команды eip/ip.

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

eflags/flags (flag register) — регистр флагов. Разрядность eflags/flags — 32/16 бит. Отдельные биты данного регистра имеют определенное функциональное назначение и называются флагами. Младшая часть этого регистра полностью аналогична регистру flags для i8086. На рис. 2 показано содержимое регистра eflags.

Рис. 2. Содержимое регистра eflags

Исходя из особенностей использования, флаги регистра eflags/flags можно разделить на три группы:

8 флагов состояния. Эти флаги могут изменяться после выполнения машинных команд. Флаги состояния регистра eflags отражают особенности результата исполнения арифметических или логических операций. Это дает возможность анализировать состояние вычислительного процесса и реагировать на него с помощью команд условных переходов и вызовов подпрограмм. В табл. 1 приведены флаги состояния и указано их назначение;

1 флаг управления. Обозначается df (Directory Flag). Он находится в 10-м бите регистра eflags и используется цепочечными командами. Значение флага df определяет направление поэлементной обработки в этих операциях: от начала строки к концу (df = 0) либо наоборот, от конца строки к ее началу (df = 1). Для работы с флагом df существуют специальные команды: cld (снять флаг df) и std (установить флаг df). Применение этих команд позволяет привести флаг df в соответствие с алгоритмом и обеспечить автоматическое увеличение или уменьшение счетчиков при выполнении операций со строками;

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

Ассемблер. Сегменты памяти и регистры

Обновл. 29 Сен 2020 |

Мы уже рассматривали 3 секции из которых состоят программы на ассемблере. Эти секции также представляют различные сегменты памяти. Что интересно, если вы замените ключевое слово section на segment , то получите тот же результат. Например:

Результат выполнения программы выше:

Сегменты памяти

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

Сегмент данных (data segment) — представлен секциями .data и .bss. Секция .data используется для объявления области памяти, где хранятся элементы данных для программы. Эта секция не может быть расширена после объявления элементов данных, и она остаётся статической во всей программе. Секция .bss также является секцией статической памяти, которая содержит буферы для данных, которые будут объявлены в программе позже. Эта буферная память заполнена нулями.

Сегмент кода (code segment) — представлен секцией .text. Он определяет область в памяти, в которой хранятся коды инструкций. Это также фиксированная область.

Стек (stack) — это сегмент, который содержит значения данных, передаваемые в функции и процедуры в программе.

Регистры

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

Чтобы ускорить свою работу, процессор подключает определённые внутренние места хранения памяти, которые называются регистрами. Регистры хранят элементы данных для обработки без необходимости получать доступ к памяти. Ограниченное количество регистров встроено в чип процессора.

Регистры процессора

В архитектуре IA-32 есть десять 32-битных и шесть 16-битных процессорных регистра. Регистры делятся на три категории:

Общие регистры (General Registers)

Регистры управления (Control Registers)

Сегментные регистры (Segment Registers)

В свою очередь, общие регистры делятся на следующие:

Регистры данных (Data Registers)

Регистры-указатели (Pointer Registers)

Индексные регистры (Index Registers)

Регистры данных

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

Как полные 32-битные регистры данных: EAX, EBX, ECX, EDX.

Нижние половины 32-битных регистров могут использоваться как четыре 16-битных регистра данных: AX, BX, CX и DX.

Нижняя и верхняя половины вышеупомянутых четырёх 16-битных регистров могут использоваться как восемь 8-битных регистров данных: AH, AL, BH, BL, CH, CL, DH и DL.

Некоторые из этих регистров данных имеют специфическое применение в арифметических операциях.

AX (primary accumulator) используется для ввода/вывода и в большинстве арифметических операций. Например, в операции умножения один операнд сохраняется в регистре EAX или AX или AL в соответствии с размером операнда.

BX (base register) используется при индексированной адресации.

CX (count register) хранит количество циклов в повторяющихся операциях (также, как и регистры ECX и CX).

DX (data register) используется в операциях ввода/вывода, а также с регистрами AX и DX для выполнения операций умножения и деления, связанных с большими значениями.

Регистры-указатели

Регистрами-указателями являются 32-битные регистры EIP, ESP и EBP и соответствующие им 16-битные регистры IP, SP и BP. Есть три категории регистров-указателей:

Указатель на инструкцию или команду (Instruction Pointer или IP) — 16-битный регистр IP хранит смещение адреса следующей команды, которая должна быть выполнена. IP в сочетании с регистром CS (как CS:IP) предоставляет полный адрес текущей инструкции в сегменте кода.


Указатель на стек (Stack Pointer или SP) — 16-битный регистр SP обеспечивает значение смещения в программном стеке. SP в сочетании с регистром SS (SS:SP) относится к текущей позиции данных или адреса в программном стеке.

Базовый указатель (Base Pointer или BP) — 16-битный регистр BP используется в основном при передаче параметров в подпрограммы. Адрес в регистре SS объединяется со смещением в BP, чтобы получить местоположение параметра. BP также можно комбинировать с DI и SI в качестве базового регистра для специальной адресации.

Индексные регистры

32-битные индексные регистры ESI и EDI и их 16-битные версии: SI и DI, которые используются в индексированной адресации, и, иногда, в операциях сложения/вычитания. Есть два типа индексных указателей:

Исходный индекс (Source Index или SI) — используется в качестве исходного индекса в строковых операциях.

Индекс назначения (Destination Index или DI) — используется в качестве индекса назначения в строковых операциях.

Регистры управления

Регистром управления является объединённый 32-битный регистр инструкций и 32-битный регистр флагов (регистр процессора, отражающий его текущее состояние). Многие инструкции включают в себя операции сравнения и математические вычисления, которые способны изменить состояние флагов, а некоторые другие условные инструкции проверяют значения флагов состояния, чтобы перенести поток управления в другое место.

Распространённые битовые флаги:

Флаг переполнения (Overflow Flag или OF) — указывает на переполнение старшего бита данных (крайнего левого бита) после signed арифметической операции.

Флаг направления (Direction Flag или DF) — определяет направление влево или вправо для перемещения или сравнения строковых данных. Если DF = 0 , то строковая операция принимает направление слева направо, а когда DF = 1 , то строковая операция принимает направление справа налево.

Флаг прерывания (Interrupt Flag или IF) — определяет, будут ли игнорироваться или обрабатываться внешние прерывания (например, ввод с клавиатуры и т.д.). Он отключает внешнее прерывание, когда значение равно 0, и разрешает прерывание, когда установлено значение 1.

Флаг ловушка (Trap Flag или TF) — позволяет настроить работу процессора в одношаговом режиме.

Флаг знака (Sign Flag или SF) — показывает знак результата арифметической операции. Этот флаг устанавливается в соответствии со знаком элемента данных после выполнения арифметической операции. Знак определяется по старшему левому биту. Положительный результат сбрасывает значение SF до 0, а отрицательный результат устанавливает его равным 1.

Илон Маск рекомендует:  Атрибут list в HTML

Нулевой флаг (Zero Flag или ZF) — указывает результат арифметической операции или операции сравнения. Ненулевой результат сбрасывает нулевой флаг до 0, а нулевой результат устанавливает его равным 1.

Вспомогательный флаг переноса (Auxiliary Carry Flag или AF) — после выполнения арифметической операции содержит перенос с бита 3 на бит 4. Используется для специализированной арифметики. AF устанавливается, когда 1-байтовая арифметическая операция вызывает перенос из бита 3 в бит 4.

Флаг равенства (Parity Flag или PF) — указывает общее количество 1-бит в результате, полученном после выполнения арифметической операции. Чётное число 1-бит сбрасывает PF до 0, а нечётное число 1-бит устанавливает PF равным 1.

Флаг переноса (Carry Flag или CF) — после выполнения арифметической операции содержит перенос 0 или 1 из старшего бита (крайнего слева). Кроме того, хранит содержимое последнего бита операции сдвига или поворота.

В таблице ниже указано положение битовых флагов в 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 Segment или CS) — содержит все команды и инструкции, которые должны быть выполнены. 16-битный регистр сегмента кода или регистр CS хранит начальный адрес сегмента кода.

Сегмент данных (Data Segment или DS) — содержит данные, константы и рабочие области. 16-битный регистр сегмента данных или регистр DS хранит начальный адрес сегмента данных.

Сегмент стека (Stack Segment или SS) — содержит данные и возвращаемые адреса процедур или подпрограмм. Он представлен в виде структуры данных «Стек». Регистр сегмента стека или регистр SS хранит начальный адрес стека.

Кроме регистров CS, DS и SS существуют и другие регистры дополнительных сегментов (Extra Segment или ES), FS и GS, которые предоставляют дополнительные сегменты для хранения данных.

При написании программ на ассемблере, программе необходим доступ к ячейкам памяти. Все области памяти в сегменте относятся к начальному адресу сегмента. Сегмент начинается с адреса, равномерно делимого на десятичное 16 или на шестнадцатеричное 10. Таким образом, крайняя правая шестнадцатеричная цифра во всех таких адресах памяти равна 0, что обычно не сохраняется в сегментных регистрах.

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

Пример на практике

Посмотрите на следующую простую программу, чтобы понять, как используются регистры в программировании на ассемблере. Эта программа выводит 9 звездочек с простым сообщением:

Ускоряем свою Arduino

Месяца 3 назад, как и многие горе-электроники, купил себе на мой тогдашний взгляд самую навороченную микропроцессорную плату из семейства Arduino, а именно Seeeduino Mega, на базе процессора Atmega1280. Побаловавшись всласть вращающимся сервоприводом и моргающим светодиодом, встал вопрос: «зачем же я её купил?».

Я работаю одним из ведущих конструкторов на одном крупном военном Зеленоградском заводе, и в данный момент веду проект по разработке метрологического средства измерения. В данной задаче существует бесконечное множество проблем, которые требуют индивидуального решения. Одной из таких задач является управление шаговым двигателем без шумов и с шагом не 1.8 градуса, как сказано в документации шагового двигателя, а до 0.0001 градуса. Казалось бы, задача сложна и нерешабельна, но, повозившись немного со схемами управления, пришёл к выводу, что всё реально и возможно. Требуется только генерация двух сигналов специфичной формы и со сдвигом фаз и частотой изменения напряжения до 1 МГц. (Подробное исследование шагового мотора и раскрытие всех тайн управления напишу в следующей статье) Сразу же в голове стали появляться проблески надежды, что я не зря потратил 1500 рублей на свою красненькую Seeeduino, и я, набравшись энтузиазма, начал разбираться.

Первоначальный ужас:

Подключив микропроцессорную плату к осцилографу, и написав цикл digitalWrite(HIGH), и ниже digitalWrite(LOW), на осцилографе обнаружил довольно унылый меандр с частотой 50Гц. Это кошмар. Это крах, подумал я, на фоне требуемых 1Мгц.
Далее, через осцилограф, я изучил еще несколько скоростей выполнения:
AnalogRead() — скорость выполнения 110 мкс.
AnalogWrite() — 2000 мкс
SerialPrintLn() — при скорости 9600 около 250мкс, а при максимальной скорости около 3мкс.
DigitalWrite() — 1800мкс
DigitalRead() — 1900мкс

На этом я, всплакнув, чуть не выкинул свою Seeeduino. Но не тут-то было!

Глаза боятся, руки делают!

Не буду рассказывать свои душевные муки и описывать три долгих дня изучения, лучше сразу скажу всё как есть!
Подняв всю возможную документацию на Arduino и на процессор Atmega1280, исследовав опыт зарубежных коллег, хочу предложить несколько советов, как заменять чтение/запись:

Улучшаем AnalogRead()

#define FASTADC 1

// defines for setting and clearing register bits
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &=

_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

void setup() <
int start ;
int i ;


#if FASTADC
// set prescale to 16
sbi(ADCSRA,ADPS2) ;
cbi(ADCSRA,ADPS1) ;
cbi(ADCSRA,ADPS0) ;
#endif

Serial.begin(9600) ;
Serial.print(«ADCTEST: «) ;
start = millis() ;
for (i = 0 ; i

Результат: скорость 18,2 мкс против бывших 110 мкс.
Кстати, максимальная скорость АЦП Атмеги как раз 16мкс. Как вариант — использовать другую микросхему, заточенную именно под АЦП, которая позволит уменьшить скорость до 0,2мкс (читать ниже, почему)

Улучшаем digitalWrite()

Каждая Arduino/Seeeduino/Feduino/Orduino/прочаяduino имеет порты. Каждый порт — 8 бит, которые сначала надо настроить на запись. Например, на моей Seeeduino PORTA — c 22 по 30 ножку. Теперь всё просто. Управляем с 22 по 30 ножки с помощью функций
PORTA=B00001010 (битовая, ножки 23 и 25 — HIGH)
или
PORTA=10 (десятичная, всё так же)
Результат = 0,2мкс против 1800мкс, которые достигаются обычным digitalWrite()

Улучшаем digitalRead()

Практически то же самое, что и в улучшении с digitalWrite(), но теперь настраиваем ножки на INPUT, и используем, например:
if (PINA==B00000010) <. >(если на ножке 23 присутствует HIGH, а на 22 и 24-30 присутствует LOW)
Результат выполнения этого if() — 0.2мкс против 1900мкс, которые достигаются обычным digitalRead()

Улучшаем ШИМ модулятор, или analogWrite()

for (int k=0;k
Вот и получили ШИМ с частотой 19кГц против 50Гц.

Сегментные регистры по умолчанию

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

Что это за договоренность?

Считается, что регистр CS всегда указывает на начало области памяти, в которой размещены команды программы (эта область называется сегментом команд или сегментом кодов), и потому при ссылках на ячейки этой области регистр CS можно не указывать явно, он подразумевается по умолчанию. (Отметим попутно, что абсолютный адрес очередной команды, подлежащей выполнению, всегда задается парой CS: IP: в счетчике команд IP всегда находится смещение этой команды относительно адреса из регистра CS.) Аналогично предполагается, что регистр DS указывает на сегмент данных (область памяти с константами, переменными и другими величинами программы), и потому во всех ссылках на этот сегмент регистр DS можно явно не указывать, т.к. он подразумевается по умолчанию. Регистр SS, считается, указывает на стек — область памяти, доступ к которой осуществляется по принципу «последним записан — первым считан» (см. 1.7), и потому все ссылки на стек, в которых явно не указан сегментный регистр, по умолчанию сегментируются по регистру SS. Регистр ES считается свободным, он не привязан ни к какому сегменту памяти и его можно использовать по своему усмотрению; чаще всего он применяется для доступа к данным, которые не поместились или сознательно не были размещены в сегменте данных.

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

Здесь, правда, возникает такой вопрос: как по смещению определить, на какой сегмент памяти оно указывает? Точный ответ приведен ниже (см. 1.4.3), а в общих чертах он такой: ссылки на сегмент команд могут быть только в командах перехода, а ссылки практически во всех других командах (кроме строковых и стековых) — это ссылки на сегмент данных. Например, в команде пересылки MOV AX, X имя X воспринимается как ссылка на данное, а потому автоматически восстанавливается до адресной пары DS: X. В команде же безусловного перехода по адресу, находящемуся в регистре BX, JMP BX абсолютный адрес перехода определяется парой CS: [BX].

Итак, если в ссылке на какую-то ячейку памяти не указан явно сегментный регистр, то этот регистр берется по умолчанию. Явно же сегментные регистры надо указывать, только если по каким-то причинам регистр по умолчанию не подходит. Если, например, в команде пересылки нам надо сослаться на стек (скажем, надо записать в регистр AH байт стека, помеченный именем X), тогда нас уже не будет устраивать договоренность о том, что по умолчанию операнд команды MOV сегментируется по регистру DS, и потому мы обязаны явно указать иной регистр — в нашем случае регистр SS, т.к. именно он указывает на стек: MOV AH, SS: X Однако такие случаи встречаются редко и потому в командах, как правило, указываются только смещения.

Отметим, что в MASM сегментный регистр записывается в самой команде непосредственно перед смещением (именем переменной, меткой и т.п.), однако на уровне машинного языка ситуация несколько иная. Имеется 4 специальные однобайтовые команды, называемые префиксами замены сегмента (обозначаемые как CS:, DS:, SS: и ES:). Они ставятся перед командой, операнд-адрес которой должен быть просегментирован по регистру, отличному от регистра, подразумеваемому по умолчанию. Например, приведенная выше символическая команда пересылки — это на самом деле две машинные команды: SS: MOV AH, X

Директивы сегмантации в ассемблере

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

Каждая программа содержит 3 типа сегментов:

  • Сегмент кодов – содержит машинные команды для выполнения. Обычно первая выполняемая команда находится в начале этого сегмента, и операционная система передает управление по адресу данного сегмента для выполнения программы. Регистр сегмента кодов (CS) адресует данный сегмент.
  • Сегмент данных – содержит данные, константы и рабочие области, необходимые программе. Регистр сегмента данных (DS) адресует данный сегмент.
  • Сегмент стека — содержит адреса возврата как для программы (для возврата в операционную систему), так и для вызовов подпрограмм (для возврата в главную программу), а также используется для передачи параметров в процедуры. Регистр сегмента стека (SS) адресует данный сегмент. Адрес текущей вершины стека задается регистрами SS:ESP.

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

Упрощенные директивы сегментации

Для задания сегментов в тексте программы можно пользоваться упрощенными директивами:

  • .CODE — для указания начала сегмента кода;
  • .DATA — для указания начала сегмента данных;
  • .STACK — для указания начала сегмента стека.

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

Стандартные директивы сегментации

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

Директива ENDS определяет конец сегмента.

Атрибут выравнивания сегмента (тип выравнивания) align сообщает компоновщику о том, что нужно обеспечить размещение начала сегмента на заданной границе. Это важно, поскольку при правильном выравнивании доступ к данным в процессорах, совместимых с базовым i8086, выполняется быстрее. Допустимые значения этого атрибута следующие:

  • BYTE — выравнивание не выполняется. Сегмент может начинаться с любого адреса памяти;
  • WORD — сегмент начинается по адресу, кратному двум, то есть последний (младший) значащий бит физического адреса равен 0 (выравнивание на границу слова);
  • DWORD — сегмент начинается по адресу, кратному четырем, то есть два последних (младших) значащих бита равны 0 (выравнивание по границе двойного слова);
  • PARA — сегмент начинается по адресу, кратному 16, то есть последняя шестнадцатеричная цифра адреса должна быть 0h (выравнивание по границе параграфа);
  • PAGE — сегмент начинается по адресу, кратному 256, то есть две последние шестнадцатеричные цифры должны быть 00h (выравнивание по границе страницы размером 256 байт);
  • MEMPAGE — сегмент начинается по адресу, кратному 4 Кбайт, то есть три последние шестнадцатеричные цифры должны быть 000h (адрес следующей страницы памяти размером 4 Кбайт);

По умолчанию тип выравнивания имеет значение PARA .
Атрибут комбинирования сегментов (комбинаторный тип) combine сообщает компоновщику, как нужно комбинировать сегменты различных модулей, имеющие одно и то же имя. По умолчанию атрибут комбинирования принимает значение PRIVATE . Значениями атрибута комбинирования сегмента могут быть:

  • PRIVATE — сегмент не будет объединяться с другими сегментами с тем же именем вне данного модуля;
  • PUBLIC — заставляет компоновщик соединить все сегменты с одинаковым именем. Новый объединенный сегмент будет целым и непрерывным. Все адреса (смещения) объектов, а это могут быть, в зависимости от типа сегмента, команды или данные, будут вычисляться относительно начала этого нового сегмента;
  • COMMON — располагает все сегменты с одним и тем же именем по одному адресу. Все сегменты с данным именем будут перекрываться и совместно использовать память. Размер полученного в результате сегмента будет равен размеру самого большого сегмента;
  • AT xxxx — располагает сегмент по абсолютному адресу параграфа (параграф — объем памяти, кратный 16, поэтому последняя шестнадцатеричная цифра адреса параграфа равна 0). Абсолютный адрес параграфа задается выражением хххx . Компоновщик располагает сегмент по заданному адресу памяти (это можно использовать, например, для доступа к видеопамяти или области ПЗУ), учитывая атрибут комбинирования. Физически это означает, что сегмент при загрузке в память будет расположен, начиная с этого абсолютного адреса параграфа, но для доступа к нему в соответствующий сегментный регистр должно быть загружено заданное в атрибуте значение. Все метки и адреса в определенном таким образом сегменте отсчитываются относительно заданного абсолютного адреса;
  • STACK — определение сегмента стека. Заставляет компоновщик соединить все одноименные сегменты и вычислять адреса в этих сегментах относительно регистра SS . Комбинированный тип STACK (стек) аналогичен комбинированному типу PUBLIC , за исключением того, что регистр SS является стандартным сегментным регистром для сегментов стека. Регистр SP устанавливается на конец объединенного сегмента стека. Если не указано ни одного сегмента стека, компоновщик выдаст предупреждение, что стековый сегмент не найден. Если сегмент стека создан, а комбинированный тип STACK не используется, программист должен явно загрузить в регистр SS адрес сегмента (подобно тому, как это делается для регистра DS ).

Атрибут размера сегмента dim . Для процессоров i80386 и выше сегменты могут быть 16- или 32-разрядными. Это влияет прежде всего на размер сегмента и порядок формирования физического адреса внутри него. Атрибут может принимать следующие значения:

  • USE16 — это означает, что сегмент допускает 16-разрядную адресацию. При формировании физического адреса может использоваться только 16-разрядное смещение. Соответственно, такой сегмент может содержать до 64 Кбайт кода или данных;
  • USE32 — сегмент будет 32-разрядным. При формировании физического адреса может использоваться 32-разрядное смещение. Поэтому такой сегмент может содержать до 4 Гбайт кода или данных. В модели памяти FLAT используется по умолчанию именно это значение атрибута размера сегмента

Атрибут класса сегмента (тип класса) ‘class’ — это заключенная в кавычки строка, помогающая компоновщику определить соответствующий порядок следования сегментов при сборке программы из сегментов нескольких модулей. Компоновщик объединяет вместе в памяти все сегменты с одним и тем же именем класса (имя класса, в общем случае, может быть любым, но лучше, если оно будет отражать функциональное назначение сегмента). Типичным примером использования имени класса является объединение в группу всех сегментов кода программы (обычно для этого используется класс ‘code’ ). С помощью механизма типизации класса можно группировать также сегменты инициализированных и неинициализированных данных.

Все сегменты сами по себе равноправны, так как директивы SEGMENT и ENDS не содержат информации о функциональном назначении сегментов. Для того чтобы использовать их как сегменты кода, данных или стека, необходимо предварительно сообщить транслятору об этом, для чего используют специальную директиву ASSUME . Эта директива сообщает транслятору о том, какой сегмент к какому сегментному регистру привязан. В свою очередь, это позволит транслятору корректно связывать символические имена, определенные в сегментах. Привязка сегментов к сегментным регистрам осуществляется с помощью операндов этой директивы, в которых ИмяСегмента должно быть именем сегмента, определенным в исходном тексте программы директивой SEGMENT или ключевым словом nothing . Если в качестве операнда используется только ключевое слово nothing , то предшествующие назначения сегментных регистров аннулируются, причем сразу для всех шести сегментных регистров. Но ключевое слово nothing можно использовать вместо аргумента ИмяСегмента, в этом случае будет выборочно разрываться связь между сегментом с именем ИмяСегмента и соответствующим сегментным регистром.
Директива SEGMENT может применяться с любой моделью памяти. При использовании директивы SEGMENT с моделью flat требуется указать транслятору, что все сегментные регистры устанавливаются в соответствии с моделью памяти flat . Это можно сделать при помощи директивы ASSUME :

Регистры FS и GS программами не используются, поэтому для них указывается атрибут ERROR .

Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL