Файл: 2005 Рудольф Марек. Ассемблер на примерах. Базовый курс. .pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 26.10.2023
Просмотров: 146
Скачиваний: 12
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
Глава 5. Управляющие конструкции Регистры помещаются в стек в следующем порядке (сверху вниз
(Е)АХ, (Е)СХ, (E)DX, (E)BX, (E)SP, (E)BP, (E)SI, (E)DI Рассмотрим небольшой пример pusha поместить в стек все регистры общего назначения некоторые действия, модифицирующие значения регистров рора восстанавливаем все регистры Команды PUSHF/POPF и PUSHFD/POPFD: толкаем регистр признаков Рассмотренные четыре команды не заботились о помещении в стек регистра признаков. В 16-битных процессорах и регистр признаков был 16-битным, поэтому для помещения в стек флагов и восстановления из него использовались команды PUSHF и POPF. Для новых процессоров, где регистр признаков
32-битный, нужно использовать 32-битные версии этих команд — PUSHFD и POPFD. Ни одна из рассмотренных до сих пор операций не изменяет старшие 16 битов регистра флагов, поэтому для практических целей будет достаточно команд
PUSHF и POPF. cmp ax,bx сравниваем АХ и ВХ pushf помещаем результат сравнения в стек
. . . выполняем операции, изменяющие флаги add d i , 4 например, сложение popf ; восстанавливаем флаги jz equal если АХ = ВХ, переходим на "equal" Команды CALL и RET: организуем подпрограмму Ни одна серьезная программа не обходится без подпрограмм. Основное назначение подпрограмм — сокращение кода основной программы одни и те же инструкции ненужно писать несколько раз — их можно объединить в подпрограммы и вызывать по мере необходимости. Для вызова подпрограммы используется команда CALL, а для возврата из подпрограммы в основную программу — RET. Формат обеих команд таков
CALL тип_вызова операнды
RET Команде CALL нужно передать всего один операнд — адрес начала подпрограммы. Это может быть непосредственное значение, содержимое регистра, памяти или метка. В отличие от JMP, при выполнении команды CALL первым
71
(Е)АХ, (Е)СХ, (E)DX, (E)BX, (E)SP, (E)BP, (E)SI, (E)DI Рассмотрим небольшой пример pusha поместить в стек все регистры общего назначения некоторые действия, модифицирующие значения регистров рора восстанавливаем все регистры Команды PUSHF/POPF и PUSHFD/POPFD: толкаем регистр признаков Рассмотренные четыре команды не заботились о помещении в стек регистра признаков. В 16-битных процессорах и регистр признаков был 16-битным, поэтому для помещения в стек флагов и восстановления из него использовались команды PUSHF и POPF. Для новых процессоров, где регистр признаков
32-битный, нужно использовать 32-битные версии этих команд — PUSHFD и POPFD. Ни одна из рассмотренных до сих пор операций не изменяет старшие 16 битов регистра флагов, поэтому для практических целей будет достаточно команд
PUSHF и POPF. cmp ax,bx сравниваем АХ и ВХ pushf помещаем результат сравнения в стек
. . . выполняем операции, изменяющие флаги add d i , 4 например, сложение popf ; восстанавливаем флаги jz equal если АХ = ВХ, переходим на "equal" Команды CALL и RET: организуем подпрограмму Ни одна серьезная программа не обходится без подпрограмм. Основное назначение подпрограмм — сокращение кода основной программы одни и те же инструкции ненужно писать несколько раз — их можно объединить в подпрограммы и вызывать по мере необходимости. Для вызова подпрограммы используется команда CALL, а для возврата из подпрограммы в основную программу — RET. Формат обеих команд таков
CALL тип_вызова операнды
RET Команде CALL нужно передать всего один операнд — адрес начала подпрограммы. Это может быть непосредственное значение, содержимое регистра, памяти или метка. В отличие от JMP, при выполнении команды CALL первым
71
Ассемблер на примерах. Базовый курс делом сохраняется в стеке значение регистра IP (EIP). Передача управления на указанный адрес называется вызовом подпрограммы. Как и команде JMP, команде CALL можно указать размер шага. По умолчанию используется near. Когда происходит вызов типа f a r , сегментный регистр CS также сохраняется в стеке вместе с IP (EIP). Возврат из подпрограммы выполняется с помощью команды RET, которая выталкивает из стека его вершину в IP (EIP). После этого процессор продолжит выполнение инструкций, находящихся в основной программе после команды CALL. Если подпрограмма вызывалась по команде CALL far, то для возврата из нее нужно восстановить не только IP (EIP), но и CS: следует использовать команду RETF, а не RET. Существует еще более сложный способ возврата из подпрограммы команда
RETF или RET может принимать непосредственный операнд, указывающий, сколько порций данных нужно вытолкнуть из стека вслед за IP (EIP) и CS. Этот вариант мы рассмотрим в 13 главе, когда будем говорить об объединении водной программе фрагментов кода на ассемблере и на языках высокого уровня, а сейчас перейдем к практике. Напишем подпрограмму, которая складывает значения ЕАХ и ЕВХ, аре зультат помещает в ЕСХ, не обращая внимания на переполнение. Значения
ЕАХ и ЕВХ после возвращения из подпрограммы должны сохраниться неизменными. Назовем нашу подпрограмму a d d _ i t . Прежде всего мы должны получить аргументы из основной программы. В высокоуровневых языках для передачи аргументов используется стек, номы упростим себе задачу и используем регистры. Операция ADD изменяет значение своего операнда, поэтому первым делом сохраним его в стеке a d d _ i t : push eax сохраняем значение ЕАХ в стек add eax,ebx ;EAX = ЕАХ + ЕВХ mov ecx,eax копируем значение из ЕАХ в ЕСХ pop eax восстанавливаем оригинальное значение ЕАХ ret возвращаемся Теперь вызовем нашу подпрограмму add_it с аргументами 4 и 8: mov eax,4 mov ebx,8 call add it
;ЕАХ = 4
;ЕВХ = 8 вызываем add_it Результат будет в регистре ЕСХ Что если мы забудем восстановить оригинальное значение ЕАХ (забудем написать команду pop eax)? Команда RET попыталась бы передать управле-
RETF или RET может принимать непосредственный операнд, указывающий, сколько порций данных нужно вытолкнуть из стека вслед за IP (EIP) и CS. Этот вариант мы рассмотрим в 13 главе, когда будем говорить об объединении водной программе фрагментов кода на ассемблере и на языках высокого уровня, а сейчас перейдем к практике. Напишем подпрограмму, которая складывает значения ЕАХ и ЕВХ, аре зультат помещает в ЕСХ, не обращая внимания на переполнение. Значения
ЕАХ и ЕВХ после возвращения из подпрограммы должны сохраниться неизменными. Назовем нашу подпрограмму a d d _ i t . Прежде всего мы должны получить аргументы из основной программы. В высокоуровневых языках для передачи аргументов используется стек, номы упростим себе задачу и используем регистры. Операция ADD изменяет значение своего операнда, поэтому первым делом сохраним его в стеке a d d _ i t : push eax сохраняем значение ЕАХ в стек add eax,ebx ;EAX = ЕАХ + ЕВХ mov ecx,eax копируем значение из ЕАХ в ЕСХ pop eax восстанавливаем оригинальное значение ЕАХ ret возвращаемся Теперь вызовем нашу подпрограмму add_it с аргументами 4 и 8: mov eax,4 mov ebx,8 call add it
;ЕАХ = 4
;ЕВХ = 8 вызываем add_it Результат будет в регистре ЕСХ Что если мы забудем восстановить оригинальное значение ЕАХ (забудем написать команду pop eax)? Команда RET попыталась бы передать управле-
1 2 3 4 5 6 7 8 9 ... 20
72
Глава 5. Управляющие конструкции ние адресу, заданному оригинальным значением ЕАХ, что могло бы вызвать сбой нашей программы, а то и всей операционной системы. Тоже самое произойдет, если мы забудем написать команду RET: процессор продолжит выполнение с того места, где заканчивается наша подпрограмма, и рано или поздно совершит недопустимое действие. Мы можем упростить нашу подпрограмму add_it, полностью отказавшись от инструкций POP и PUSH: a d d _ i t : mov e c x , e a x копируем значение ЕАХ (первый параметр) в ЕСХ add e c x , e b x добавляем к нему ЕВХ (второй параметр результат будет сохранен в регистре ЕСХ r e t ; возвращаемся Команды INT и IRET: вызываем прерывание Вернемся к теме прерываний. Прерыванием называется такое событие, когда процессор приостанавливает нормальное выполнение программы и начинает выполнять другую программу, предназначенную для обработки прерывания. Закончив обработку прерывания, он возвращается к выполнению приостановленной программы. Во второй главе было сказано, что все прерывания делятся на две группы программные и аппаратные Программные прерывания порождаются по команде INT. Программные прерывания можно рассматривать как прерывания по требованию, например, когда вы вызываете подпрограмму операционной системы для вывода строки символов. В случае с программным прерыванием вы сами определяете, какое прерывание будет вызвано в тот или иной момент. Команде INT нужно передать всего один 8-битный операнд, который задает номер нужного прерывания.
INT
op Аппаратные прерывания вызываются аппаратными средствами компьютера, подключенными к общей шине (ISA или PCI). Устройство, запрашивающее прерывание, генерирует так называемый запрос на прерывание (IRQ, interrupt requests). Всего существует 16 аппаратных запросов на прерывание, поскольку только 16 проводников в шине ISA выделено для этой цели. Запрос на прерывание направляется контроллеру прерываний, который, в свою очередь, запрашивает микропроцессор. Вместе с запросом он передает процессору номер прерывания. После запуска компьютера и загрузки операционной системы
DOS, IRQ 0 (системный таймер) соответствует прерыванию 8 (часы. Когда процессор получает номер прерывания, он помещает в стек контекст выполняемой в данный момент программы, подобно тому, как человек кладет в книгу закладку, чтобы не забыть, с какого места продолжить чтение книги. Роль закладки играют значения CS, (ЕР и регистр флагов.
73
INT
op Аппаратные прерывания вызываются аппаратными средствами компьютера, подключенными к общей шине (ISA или PCI). Устройство, запрашивающее прерывание, генерирует так называемый запрос на прерывание (IRQ, interrupt requests). Всего существует 16 аппаратных запросов на прерывание, поскольку только 16 проводников в шине ISA выделено для этой цели. Запрос на прерывание направляется контроллеру прерываний, который, в свою очередь, запрашивает микропроцессор. Вместе с запросом он передает процессору номер прерывания. После запуска компьютера и загрузки операционной системы
DOS, IRQ 0 (системный таймер) соответствует прерыванию 8 (часы. Когда процессор получает номер прерывания, он помещает в стек контекст выполняемой в данный момент программы, подобно тому, как человек кладет в книгу закладку, чтобы не забыть, с какого места продолжить чтение книги. Роль закладки играют значения CS, (ЕР и регистр флагов.
73
Ассемблер на примерах. Базовый курс Теперь процессор будет выполнять другую программу — обработчик прерывания. Адрес этой программы называется вектором прерывания. Векторы прерывания хранятся в таблице векторов прерываний, находящейся в памяти. Таблицу прерываний можно представить себе как массив адресов подпрограмм, в котором индекс массива соответствует номеру прерывания. После того, как процессор определит адрес обработчика прерывания, он запишет его в пару CS и (ЕР. Следующая выполненная команда будет первой командой обработчика прерывания. В десятой главе, посвященной программированию в DOS, мы опишем функции го (0x21) прерывания, которое генерируется следующей инструкцией i n t 0 x 2 1 ; вызов Возврат из обработчика прерывания осуществляется с помощью команды
IRET, которая восстанавливает исходные значения (E)IP, CS и флагов из стека Формат команды
IRET Давайте разберемся, как вызывается прерывание на примере го прерывания (рис. 5.9). Мы считаем, что процессор работает в реальном режиме с
16-битной адресацией. Рис. 5.9. Загрузка в CS и IP новых значений Прежде всего процессор находит номер прерывания. Программные прерывания вызываются инструкцией INT, которая содержит номер прерывания в своем операнде. Следующий шаг — сохранение контекста программы в стеке. Продемонстрируем этот процесс с помощью команд ассемблера pushf push cs сохраняем значения регистра признаков в стеке сохраняем регистр CS в стеке
74
IRET, которая восстанавливает исходные значения (E)IP, CS и флагов из стека Формат команды
IRET Давайте разберемся, как вызывается прерывание на примере го прерывания (рис. 5.9). Мы считаем, что процессор работает в реальном режиме с
16-битной адресацией. Рис. 5.9. Загрузка в CS и IP новых значений Прежде всего процессор находит номер прерывания. Программные прерывания вызываются инструкцией INT, которая содержит номер прерывания в своем операнде. Следующий шаг — сохранение контекста программы в стеке. Продемонстрируем этот процесс с помощью команд ассемблера pushf push cs сохраняем значения регистра признаков в стеке сохраняем регистр CS в стеке
74
Глава 5. Управляющие конструкции push ip эта команда недопустима. Мы написали ее с демонстрационной целью. Реализовать ее можно так c a l l here here: Последний шаг — это безусловный переход на адрес, прочитанный из таблицы прерываний по номеру запрошенного прерывания JMP far. Здесь мы слегка забегаем вперед пока просто примите, что таблица векторов прерываний находится в самом начале адресуемой памяти, то есть по адресу 0x0000:0x0000. Каждый из векторов прерывания занимает четыре байта. Первые два байта определяют новое значение IP (то есть смещение, а оставшиеся два — новое значение сегментного регистра CS. Вектор прерывания номер 0x21 хранится по адресу 0х0000:(0х21*4), поэтому его вызов можно записать так jmp far [0x21*4] ; эта инструкция переходит на указанный адрес, предполагаем, что DS=0, поэтому мы можем адресовать память с адресах) Команду INT можно реализовать самостоятельно с помощью команд PUSHF и CALL far: pushf сохраняем флаги в стеке c a l l far [0x21*4] ; теперь сохраняем CS и IP и выполняем "jump" Программные прерывания часто используются как шлюз для вызова функций операционной системы. Подробнее о них мы поговорим в главах, посвященных отдельным операционным системам.
75
75
ева 6 Прочие команды Изменение регистра признаков напрямую Команда XCHG — меняем местами операнды Команда LEA — не только вычисление адреса Команды для работы со строками Команды ввода/вывода (I/O) Сдвиги ротация Псевдокоманды Советы по использованию команд Ассемблер на примерах. Базовый курс
В этой главе мы рассмотрим наиболее часто используемые команды системы команд процессорах. С помощью этих команд вы сможете написать достаточно сложные программы для решения самых разнообразных задач.
6.1. Изменение регистра признаков напрямую Несколько команд позволяют непосредственно модифицировать флаги регистра признаков. Мы рассмотрим четыре команды, которые изменяют флаги
IF и ID, то есть флаг прерывания и флаг направления. Команды СИ и STI Команды CLI (Clear Interrupt) и STI (Set Interrupt) сбрасывают и устанавливают флаг прерывания IF. Иными словами, при их помощи вы можете запретить или разрешить аппаратные прерывания. Если этот флаг установлен
(1), аппаратные прерывания разрешены. Команда CLI сбрасывает флаг (0) — запрещает аппаратные прерывания. Потом вы должны снова разрешить их, выполнив команду STI: c l i отключить прерывания — использовать только в DOS!
. . . теперь мы никем не будем потревожены и можем выполнять какие-то действия, например, изменять таблицу прерываний s t i включаем прерывания снова Команды STD и CLD Команды STD и CLD модифицируют значение флага DF. Этим флагом пользуется группа команд для обработки строк, поэтому подробнее о нем мы поговорим в следующей главе. Команда CLD сбрасывает этот флаг (что означает отсчет вверх, a STD устанавливает его (отсчет в обратном порядке.
77
6.1. Изменение регистра признаков напрямую Несколько команд позволяют непосредственно модифицировать флаги регистра признаков. Мы рассмотрим четыре команды, которые изменяют флаги
IF и ID, то есть флаг прерывания и флаг направления. Команды СИ и STI Команды CLI (Clear Interrupt) и STI (Set Interrupt) сбрасывают и устанавливают флаг прерывания IF. Иными словами, при их помощи вы можете запретить или разрешить аппаратные прерывания. Если этот флаг установлен
(1), аппаратные прерывания разрешены. Команда CLI сбрасывает флаг (0) — запрещает аппаратные прерывания. Потом вы должны снова разрешить их, выполнив команду STI: c l i отключить прерывания — использовать только в DOS!
. . . теперь мы никем не будем потревожены и можем выполнять какие-то действия, например, изменять таблицу прерываний s t i включаем прерывания снова Команды STD и CLD Команды STD и CLD модифицируют значение флага DF. Этим флагом пользуется группа команд для обработки строк, поэтому подробнее о нем мы поговорим в следующей главе. Команда CLD сбрасывает этот флаг (что означает отсчет вверх, a STD устанавливает его (отсчет в обратном порядке.
77
Ассемблер на примерах. Базовый курс Формат команд простой
STD
CLD
6.2. Команда XCHG — меняем местами операнды Очень часто нам нужно поменять местами значения двух регистров. Конечно, можно это сделать, используя третий, временный, регистр, но намного проще использовать инструкцию XCHG (exchange — обмен, которая как раз для этого и предназначена.
XCHG o l , o 2 Подобно инструкции MOV, она имеет два операнда — ol и о. Каждый из них может быть 8-, 16- и разрядными только один из них может находиться в памяти — требования такие же, как у команды MOV. xchg еах,еах меняем местами значения ЕАХ и ЕАХ. Так реализован NOP — пустой оператор xchg ebx,ecx меняем местами значения ЕВХ и ЕСХ xchg al,ah меняем местами значения AL и АН xchg dl,ah меняем местами значения DL и АН xchg byte [variable],cl меняем местами один байт памяти и CL
6.3. Команда LEA — не только вычисление адреса Имя этой команды — это сокращение от «Load Effective Address», то есть загрузить эффективный адрес. Она вычисляет эффективный адрес второго операнда и сохраняет его в первом операнде (который может быть только регистром. Синтаксис этой команды требует, чтобы второй операнд был заключен в квадратные скобки, но фактически она не адресует память.
LEA o l , [ o 2 ]
LEA полезна в тех случаях, когда мы не собираемся обращаться к памяти, а адрес нужен нам для другой цели lea edi, [ebx*4+ecx] загружает в регистр EDI адрес, вычисленный как EDI = ЕВХ*4+ЕСХ Значение, вычисленное командой LEA, необязательно рассматривать как адрес ее можно использовать для целочисленных арифметических вычислений. Несколько примеров экзотического использования LEA будет приведено в главе, посвященной оптимизации.
78
STD
CLD
6.2. Команда XCHG — меняем местами операнды Очень часто нам нужно поменять местами значения двух регистров. Конечно, можно это сделать, используя третий, временный, регистр, но намного проще использовать инструкцию XCHG (exchange — обмен, которая как раз для этого и предназначена.
XCHG o l , o 2 Подобно инструкции MOV, она имеет два операнда — ol и о. Каждый из них может быть 8-, 16- и разрядными только один из них может находиться в памяти — требования такие же, как у команды MOV. xchg еах,еах меняем местами значения ЕАХ и ЕАХ. Так реализован NOP — пустой оператор xchg ebx,ecx меняем местами значения ЕВХ и ЕСХ xchg al,ah меняем местами значения AL и АН xchg dl,ah меняем местами значения DL и АН xchg byte [variable],cl меняем местами один байт памяти и CL
6.3. Команда LEA — не только вычисление адреса Имя этой команды — это сокращение от «Load Effective Address», то есть загрузить эффективный адрес. Она вычисляет эффективный адрес второго операнда и сохраняет его в первом операнде (который может быть только регистром. Синтаксис этой команды требует, чтобы второй операнд был заключен в квадратные скобки, но фактически она не адресует память.
LEA o l , [ o 2 ]
LEA полезна в тех случаях, когда мы не собираемся обращаться к памяти, а адрес нужен нам для другой цели lea edi, [ebx*4+ecx] загружает в регистр EDI адрес, вычисленный как EDI = ЕВХ*4+ЕСХ Значение, вычисленное командой LEA, необязательно рассматривать как адрес ее можно использовать для целочисленных арифметических вычислений. Несколько примеров экзотического использования LEA будет приведено в главе, посвященной оптимизации.
78
Глава 6. Прочие команды
6.4. Команды для работы со строками Строкой в языке ассемблера называется последовательность символов (байтов, заканчивающаяся нулевым байтом (точно также, как в языке С.
0x43 0x6F 0x6D 0x70 0x75 0x74 0x65 0x72 0x00
C
o m
p u
t e
r / 0 Рис. 6.1. Строка заканчивается нулевым байтом Система команд х86-совместимых процессоров содержит несколько команд, облегчающих манипуляции со строками. Каждую из них можно заменить несколькими элементарными командами, но, как ив случае с командой LOOP, эти команды сделают вашу программу короче и понятнее. В зависимости от размера операнда команды для обработки строк делятся натри группы. Первая группа предназначена для работы с 8-битными операндами, то есть с отдельными символами. Имена этих команд заканчиваются на В (byte). Имена команд второй группы, предназначенных для работы с
16-битными операндами, заканчиваются символом «W» (word). Третья группа команд работает с 32-битными операндами, и имена их заканчиваются на «D»
(double word). Все команды, обсуждаемые в этом параграфе, работают с фиксированными операндами и не имеют аргументов. Порядок отсчета байтов в строке вовремя работы этих команд зависит от флага направления (DF). Команды STOSx — запись строки в память Под единым обозначением STOSx (STOre String) скрываются три команды
•STOSB
•STOSW
• STOSD Команда STOSB копирует содержимое регистра AL в ячейку памяти, адрес которой находится в паре регистров ES:(E)DI, и уменьшает или увеличивает в зависимости от флага DF) на единицу значение регистра (E)DI, чтобы приготовиться к копированию AL в следующую ячейку. Если DF=0, то (E)DI будет увеличен на 1, в противном случае — уменьшен на 1. Какой регистр будет использоваться — DI или EDI — зависит от режима процессора. Вторая инструкция, STOSW, работает аналогично, но данные берутся из регистра АХ, a (E)DI уменьшается/увеличивается на 2. STOSD копирует содержимое ЕАХ, a E(DI) уменьшает/увеличивает на 4.
79
6.4. Команды для работы со строками Строкой в языке ассемблера называется последовательность символов (байтов, заканчивающаяся нулевым байтом (точно также, как в языке С.
0x43 0x6F 0x6D 0x70 0x75 0x74 0x65 0x72 0x00
C
o m
p u
t e
r / 0 Рис. 6.1. Строка заканчивается нулевым байтом Система команд х86-совместимых процессоров содержит несколько команд, облегчающих манипуляции со строками. Каждую из них можно заменить несколькими элементарными командами, но, как ив случае с командой LOOP, эти команды сделают вашу программу короче и понятнее. В зависимости от размера операнда команды для обработки строк делятся натри группы. Первая группа предназначена для работы с 8-битными операндами, то есть с отдельными символами. Имена этих команд заканчиваются на В (byte). Имена команд второй группы, предназначенных для работы с
16-битными операндами, заканчиваются символом «W» (word). Третья группа команд работает с 32-битными операндами, и имена их заканчиваются на «D»
(double word). Все команды, обсуждаемые в этом параграфе, работают с фиксированными операндами и не имеют аргументов. Порядок отсчета байтов в строке вовремя работы этих команд зависит от флага направления (DF). Команды STOSx — запись строки в память Под единым обозначением STOSx (STOre String) скрываются три команды
•STOSB
•STOSW
• STOSD Команда STOSB копирует содержимое регистра AL в ячейку памяти, адрес которой находится в паре регистров ES:(E)DI, и уменьшает или увеличивает в зависимости от флага DF) на единицу значение регистра (E)DI, чтобы приготовиться к копированию AL в следующую ячейку. Если DF=0, то (E)DI будет увеличен на 1, в противном случае — уменьшен на 1. Какой регистр будет использоваться — DI или EDI — зависит от режима процессора. Вторая инструкция, STOSW, работает аналогично, но данные берутся из регистра АХ, a (E)DI уменьшается/увеличивается на 2. STOSD копирует содержимое ЕАХ, a E(DI) уменьшает/увеличивает на 4.
79
Ассемблер на примерах. Базовый курс e l d сбрасываем DF, направление будет вверх stosw сохраняем АХ вили (зависит от режима процессора) и увеличиваем (E)DI на 2 Команды LODSx — чтение строки из памяти Под единым обозначением LODSx (LOaD String) скрываются три команды
• LODSB
•LODSW
•LODSD Действие этих команд противоположно командам предыдущей группы они копируют порцию данных из памяти в регистр AL, АХ и ЕАХ. Адрес нужной ячейки памяти берется из пары DS:(E)SI. Если флаг DF равен нулю, торе гистр SI будет увеличен на 1/2/4 (В, W, D), в противном случае — уменьшен на 1/2/4. Команды CMPSx — сравнение строк Под единым обозначением CMPSx (CoMPare String) скрываются три команды
• CMPSB
• CMPSW
• CMPSD Команда CMPSB сравнивает байт, находящийся по адресу ES:(E)DI, с байтом по адресу DS:(E)SI и изменяет значения регистров SI ив зависимости от флага DF. Команды CMPSB и CMPSD сравнивают не байты, а слова и двойные слова соответственно, увеличивая или уменьшая регистры SI и DI на размер порции данных (2 или 4). Команды SCASx — поиск символа в строке Под единым обозначением SCASx (SCAn String) скрываются три команды
• SCASB
•SCASW
• SCASD Команды SCASB/W/D сравнивают значения регистра AL/AX/EAX со значением в памяти по адресу [ES:(E)DI]. Регистр (E)DI изменяется в зависимости от флага DF. Команды REP и REPZ — повторение следующей команды Команда REP (Repeat) облегчает обработку строк произвольной длины. Это так называемая префиксная команда ее нельзя использовать отдельно,
80
• LODSB
•LODSW
•LODSD Действие этих команд противоположно командам предыдущей группы они копируют порцию данных из памяти в регистр AL, АХ и ЕАХ. Адрес нужной ячейки памяти берется из пары DS:(E)SI. Если флаг DF равен нулю, торе гистр SI будет увеличен на 1/2/4 (В, W, D), в противном случае — уменьшен на 1/2/4. Команды CMPSx — сравнение строк Под единым обозначением CMPSx (CoMPare String) скрываются три команды
• CMPSB
• CMPSW
• CMPSD Команда CMPSB сравнивает байт, находящийся по адресу ES:(E)DI, с байтом по адресу DS:(E)SI и изменяет значения регистров SI ив зависимости от флага DF. Команды CMPSB и CMPSD сравнивают не байты, а слова и двойные слова соответственно, увеличивая или уменьшая регистры SI и DI на размер порции данных (2 или 4). Команды SCASx — поиск символа в строке Под единым обозначением SCASx (SCAn String) скрываются три команды
• SCASB
•SCASW
• SCASD Команды SCASB/W/D сравнивают значения регистра AL/AX/EAX со значением в памяти по адресу [ES:(E)DI]. Регистр (E)DI изменяется в зависимости от флага DF. Команды REP и REPZ — повторение следующей команды Команда REP (Repeat) облегчает обработку строк произвольной длины. Это так называемая префиксная команда ее нельзя использовать отдельно,
80
Глава 6. Прочие команды а только в паре с какой-нибудь другой командой. Она работает подобно команде LOOP: повторяет следующую за ней команду до тех пор, пока значение в регистре (Е)СХ не станет равно нулю. Регистр (Е)СХ уменьшается на единицу при каждой итерации. Чаще всего команда REP применяется в паре с MOVS или STOS: r e p movsb Или r e p s t o s b скопировать первые (Е)СХ байтов изв. Это ассемблерный аналог
С-функции memcpy() скопировать (Е)СХ раз значение в E S : ( E ) D I . Это ассемблерный аналог С-функции m e m s e t ( ) Команда REPZ (синоним REPE), подобно команде LOOPZ, позволяет уточнить условие. Следующая итерация выполняется тогда, когда не только
(Е)СХ неравен нулю, но и флаг ZF не установлен. Второе условие может быть инвертировано командой REPNZ (синоним REPNE). Эти команды часто используются вместе с SCAS или CMPS: r e p z s c a s b Или r e p z cmpsb
; повторяем повторяем Теперь вы уже знаете достаточно, чтобы реализовать на языке ассемблера функцию подсчета символов в строке, которая в языке С называется strlenQ. Рис. 6.2. Блок-схема функции strlen()
С-функции memcpy() скопировать (Е)СХ раз значение в E S : ( E ) D I . Это ассемблерный аналог С-функции m e m s e t ( ) Команда REPZ (синоним REPE), подобно команде LOOPZ, позволяет уточнить условие. Следующая итерация выполняется тогда, когда не только
(Е)СХ неравен нулю, но и флаг ZF не установлен. Второе условие может быть инвертировано командой REPNZ (синоним REPNE). Эти команды часто используются вместе с SCAS или CMPS: r e p z s c a s b Или r e p z cmpsb
; повторяем повторяем Теперь вы уже знаете достаточно, чтобы реализовать на языке ассемблера функцию подсчета символов в строке, которая в языке С называется strlenQ. Рис. 6.2. Блок-схема функции strlen()
Ассемблер на примерах. Базовый курс Функции нужен только один аргумент — адрес начала строки, который нужно сохранить в ES:(E)DI. Результат функции (количество символов строки + нулевой символ) будет записан в ЕСХ. Указатель ES:(E)DI будет указывать на байт, следующий за последним (нулевым) символом строки. Код функции приведен в листинге 6.1. Листинг 6.1. Код функции strlen()
strlen: push eax xor ecx,ecx xor eax,eax dec ecx eld repne scasb neg ecx pop eax ret помещаем регистр EAX в стек сбрасываем ЕСХ (ЕСХ=0), тоже самое можно сделать так mov О
EAX = 0
ЕСХ = ЕСХ - 1. Получаем OxFFFFFFFF - максимальная Длина строки
DF = 0, будем двигаться вперед, а не назад ищем нулевой байт отрицание ЕСХ (дополнительный код) даст количество выполненных итераций восстанавливаем исходный ЕАХ выходим из подпрограммы Вы можете легко адаптировать программу для работы только с 16-битными регистрами просто удалите Ев имени каждого регистра. Мы уже написали функцию для подсчета символов в строке, но до сих пор не знаем, как разместить строку в памяти. Пока будем считать, что в ES:(E)DI уже загружен правильный адрес начала строки. Мы вызываем нашу подпрограмму с помощью CALL, а после ее выполнения в регистре ЕСХ получим количество символов в строке. c a l l s t r l e n вызываем s t r l e n
82 Рис. 6.3. Состояние регистров ES:(E)DI перед вызовом strlen и после ее выполнения
strlen: push eax xor ecx,ecx xor eax,eax dec ecx eld repne scasb neg ecx pop eax ret помещаем регистр EAX в стек сбрасываем ЕСХ (ЕСХ=0), тоже самое можно сделать так mov О
EAX = 0
ЕСХ = ЕСХ - 1. Получаем OxFFFFFFFF - максимальная Длина строки
DF = 0, будем двигаться вперед, а не назад ищем нулевой байт отрицание ЕСХ (дополнительный код) даст количество выполненных итераций восстанавливаем исходный ЕАХ выходим из подпрограммы Вы можете легко адаптировать программу для работы только с 16-битными регистрами просто удалите Ев имени каждого регистра. Мы уже написали функцию для подсчета символов в строке, но до сих пор не знаем, как разместить строку в памяти. Пока будем считать, что в ES:(E)DI уже загружен правильный адрес начала строки. Мы вызываем нашу подпрограмму с помощью CALL, а после ее выполнения в регистре ЕСХ получим количество символов в строке. c a l l s t r l e n вызываем s t r l e n
82 Рис. 6.3. Состояние регистров ES:(E)DI перед вызовом strlen и после ее выполнения
Глава 6. Прочие команды Еще одна очень нужная функция — это функция сравнения двух строк, которая в языке С называется strcmp(). Рис. 6.4. Блок-схема функции strcmp Подпрограмма сравнивает две строки адрес первой сохранен в ES:(E)DI, a адрес второй — в DS:(E)SI. Если строки одинаковы, то после завершения подпрограммы ЕСХ будет содержать 0, а если нетто в ЕСХ будет количество первых одинаковых символов. Код нашей strcmp () приведен в листинге 6.2. Листинг 6.2. Код функции strcmp() strcmp: push edx push edi call strlen mov edx,ecx mov edi,esi
;push ds
;push ds
;pop es call strlen
;pop ds cmp ecx,edx jae .length_ok помещаем EDX в стек помещаем EDI в стек вычисляем длину первой строки сохраняем длину первой строки в EDX
;EDI = ESI сохраняем DS в стек еще раза теперь загружаем его в ES (ES = DS) теперь вычисляем длину второй строки
; восстанавливаем исходный DS какая строка длиннее переходим, если длина первой строки (ЕСХ) больше или равна длине второй
83
;push ds
;push ds
;pop es call strlen
;pop ds cmp ecx,edx jae .length_ok помещаем EDX в стек помещаем EDI в стек вычисляем длину первой строки сохраняем длину первой строки в EDX
;EDI = ESI сохраняем DS в стек еще раза теперь загружаем его в ES (ES = DS) теперь вычисляем длину второй строки
; восстанавливаем исходный DS какая строка длиннее переходим, если длина первой строки (ЕСХ) больше или равна длине второй
83
Ассемблер на примерах. Базовый курс mov ecx,edx
.length_ok: pop edi eld repe empsb pop edx ret будем использовать более длинную строку восстанавливаем исходный EDI
;DF = О повторяем сравнение, пока не встретим два разных символа или пока ЕСХ не станет равен О
; восстанавливаем исходное значение EDX конец Наша функция stremp будет работать только тогда, когда значения сегментных регистров DS и ES равны. Корректнее было бы раскомментировать заком
ментированные команды — наша функция смогла бы работать со строками, расположенными в разных сегментах. Однако в большинстве программ сегментные регистры равны, поэтому нам ненужно учитывать их разницу. На рис. 6.5 вы можете видеть содержимое регистров после сравнения неодинаковых строк.
ECX=0xA Рис. 6.5. Содержимое регистров дои после вызова stremp
6.5. Команды ввода/вывода (I/O) Периферийные устройства используют так называемые шлюзы ввода/вывода — обычно их называют портами ввода/вывода. С помощью портов ваша программа (или сам процессор) может общаться стем или иным периферийным устройством. Для общения с периферийным или другим устройством на аппаратном уровне используются команды IN и OUT. Команды IN и OUT — обмен данными с периферией Команда IN позволяет получить от устройства, a OUT — передать устройству порцию данных в размере байта, слова или двойного слова.
IN a l , dx OUT dx, a l
IN ax, dx OUT dx, ax
IN eax, dx OUT dx, eax
IN a l , imm8 OUT imm8, al
IN ax, imm8 OUT imm8, ax
84
.length_ok: pop edi eld repe empsb pop edx ret будем использовать более длинную строку восстанавливаем исходный EDI
;DF = О повторяем сравнение, пока не встретим два разных символа или пока ЕСХ не станет равен О
; восстанавливаем исходное значение EDX конец Наша функция stremp будет работать только тогда, когда значения сегментных регистров DS и ES равны. Корректнее было бы раскомментировать заком
ментированные команды — наша функция смогла бы работать со строками, расположенными в разных сегментах. Однако в большинстве программ сегментные регистры равны, поэтому нам ненужно учитывать их разницу. На рис. 6.5 вы можете видеть содержимое регистров после сравнения неодинаковых строк.
ECX=0xA Рис. 6.5. Содержимое регистров дои после вызова stremp
6.5. Команды ввода/вывода (I/O) Периферийные устройства используют так называемые шлюзы ввода/вывода — обычно их называют портами ввода/вывода. С помощью портов ваша программа (или сам процессор) может общаться стем или иным периферийным устройством. Для общения с периферийным или другим устройством на аппаратном уровне используются команды IN и OUT. Команды IN и OUT — обмен данными с периферией Команда IN позволяет получить от устройства, a OUT — передать устройству порцию данных в размере байта, слова или двойного слова.
IN a l , dx OUT dx, a l
IN ax, dx OUT dx, ax
IN eax, dx OUT dx, eax
IN a l , imm8 OUT imm8, al
IN ax, imm8 OUT imm8, ax
84
Глава 6. Прочие команды Команда IN читает данные из порта ввода/вывода, номер которого содержится в регистре DX, и помещает результат в регистр AL/AX/EAX. Другие регистры, кроме AL/AX/EAX и DX, использовать нельзя. Команда OUT отправляет данные в порт. Типы ее операндов такие же, как у
IN, но обратите внимание операнды указываются в обратном порядке. Диапазоны портов ввода/вывода, которые связаны с различными устройствами, перечислены в табл. 6.1. Диапазоны портов ввода/вывода Таблица 6.1
0000-001f :dma1 0020-003f: p i d
0040-005f: timer
0060-006f: keyboard
0070-007f: rtc
0080-008f: dma page reg
OOaO-OObf: pic2
OOcO-OOdf: dma2
OOfO-OOff: fpu
0170-0177 :ide1 01f0-01f7:ide0 0213-0213 :isapnp read
0220-022f: soundblaster
0290-0297 :w83781d
0376-0376 : idel
03c0-03df:vga+
03f2-03f5: floppy
03f6-03f6: ideO
03f7-03f7: floppy DIR
03f8-03ff: lirc_serial
0a79-0a79: isapnp write
0cf8-0cff: PCI conf 1 4000-403f: Intel Corp. 82371AB/EB/MB PIIX4 ACPI И Intel Corp. 82371AB/EB/MB PIIX4 ACPI e000-e01f: Intel Corp. 82371AB/EB/MB PIIX4 USB fOOO-fOOf: Intel Corp. 82371AB/EB/MB PIIX4 IDE Первый контроллер DMA (Direct Memory Access) Первый аппаратный контроллер прерываний Системный таймер Клавиатура Часы реального времени (RTC, real time clock)
DMA page register Второй аппаратный контроллер прерываний Второй контроллер Математический сопроцессор Второй контроллер (Secondary) Первый контроллер (Primary) Интерфейс PnP (plug-and-play) шины ISA Звуковая плата Аппаратный мониторинг температуры и напряжения Второй контроллер (продолжение) Видеоадаптер Дисковод для гибких дисков Первый контроллер (продолжение) Дисковод для гибких дисков (продолжение) Последовательный порт Интерфейс PnP (plug-and-play) шины ISA продолжение) Первый конфигурационный регистр шины PCI
Чипсет ACPI
Чипсет ACPI
Чипсет USB
Чипсет контроллера дисков
IN, но обратите внимание операнды указываются в обратном порядке. Диапазоны портов ввода/вывода, которые связаны с различными устройствами, перечислены в табл. 6.1. Диапазоны портов ввода/вывода Таблица 6.1
0000-001f :dma1 0020-003f: p i d
0040-005f: timer
0060-006f: keyboard
0070-007f: rtc
0080-008f: dma page reg
OOaO-OObf: pic2
OOcO-OOdf: dma2
OOfO-OOff: fpu
0170-0177 :ide1 01f0-01f7:ide0 0213-0213 :isapnp read
0220-022f: soundblaster
0290-0297 :w83781d
0376-0376 : idel
03c0-03df:vga+
03f2-03f5: floppy
03f6-03f6: ideO
03f7-03f7: floppy DIR
03f8-03ff: lirc_serial
0a79-0a79: isapnp write
0cf8-0cff: PCI conf 1 4000-403f: Intel Corp. 82371AB/EB/MB PIIX4 ACPI И Intel Corp. 82371AB/EB/MB PIIX4 ACPI e000-e01f: Intel Corp. 82371AB/EB/MB PIIX4 USB fOOO-fOOf: Intel Corp. 82371AB/EB/MB PIIX4 IDE Первый контроллер DMA (Direct Memory Access) Первый аппаратный контроллер прерываний Системный таймер Клавиатура Часы реального времени (RTC, real time clock)
DMA page register Второй аппаратный контроллер прерываний Второй контроллер Математический сопроцессор Второй контроллер (Secondary) Первый контроллер (Primary) Интерфейс PnP (plug-and-play) шины ISA Звуковая плата Аппаратный мониторинг температуры и напряжения Второй контроллер (продолжение) Видеоадаптер Дисковод для гибких дисков Первый контроллер (продолжение) Дисковод для гибких дисков (продолжение) Последовательный порт Интерфейс PnP (plug-and-play) шины ISA продолжение) Первый конфигурационный регистр шины PCI
Чипсет ACPI
Чипсет ACPI
Чипсет USB
Чипсет контроллера дисков
1 2 3 4 5 6 7 8 9 10 ... 20
85
Ассемблер на примерах. Базовый курс Рассмотрение функций отдельных регистров периферийных устройств выходит далеко за рамки этой книги. Вот просто пример in a l , 0 x 6 0 чтение значения из порта с номером 0x60
; ( это скан-код последней нажатой клавиши) Организация задержки. Команда NOP Название этой команды звучит как «No Operation», то есть это ничего не делающая, пустая инструкция. пор ничего не делать Для чего же нужна такая команда Когда программа работает с портами вво
да/вывода, между операциями чтения и записи нужно делать паузы. Причина ясна периферийные устройства работают медленнее процессора, и им нужно время, чтобы обработать запрос. Для организации задержки и используют
NOP. Выполнение этой команды занимает один цикл процессора — маловато, поэтому чаще задержку организуют при помощи безусловного перехода наследующую команду jmp short delayl delayl: Поскольку команда короткого перехода занимает 2 байта, тот же переход наследующую команду можно записать так jmp short $+2 перейти на 2 байта вперед Знак доллара означает текущий адрес. Новые процессоры снабжены схемой предсказания переходов, то есть еще до самого перехода процессор будет знать, что ему нужно сделать. Предсказанный переход будет выполнен за меньшее число циклов процессора, чем непредсказанный, поэтому на новых процессорах задержку с помощью команды JMP организовать нельзя. Лучшим способом подождать периферийное устройство будет запись произвольного значения в безопасный порт — это диагностический порт с номером 0x80: out 0x80,al ; это просто задержка
6.6. Сдвиги ротация Операции поразрядного сдвига перемещают отдельные биты в байте, слове или двойном слове. Сдвиг может быть влево или вправо. При сдвиге вправо на одну позицию крайний правый (младший) бит теряется, а крайний левый замещается нулем. При сдвиге влево теряется крайний левый (старший) бита крайний правый замещается нулем. При сдвиге на несколько позиций выталкивается указанное количество битов, а освободившиеся биты замещаются нулями.
86
; ( это скан-код последней нажатой клавиши) Организация задержки. Команда NOP Название этой команды звучит как «No Operation», то есть это ничего не делающая, пустая инструкция. пор ничего не делать Для чего же нужна такая команда Когда программа работает с портами вво
да/вывода, между операциями чтения и записи нужно делать паузы. Причина ясна периферийные устройства работают медленнее процессора, и им нужно время, чтобы обработать запрос. Для организации задержки и используют
NOP. Выполнение этой команды занимает один цикл процессора — маловато, поэтому чаще задержку организуют при помощи безусловного перехода наследующую команду jmp short delayl delayl: Поскольку команда короткого перехода занимает 2 байта, тот же переход наследующую команду можно записать так jmp short $+2 перейти на 2 байта вперед Знак доллара означает текущий адрес. Новые процессоры снабжены схемой предсказания переходов, то есть еще до самого перехода процессор будет знать, что ему нужно сделать. Предсказанный переход будет выполнен за меньшее число циклов процессора, чем непредсказанный, поэтому на новых процессорах задержку с помощью команды JMP организовать нельзя. Лучшим способом подождать периферийное устройство будет запись произвольного значения в безопасный порт — это диагностический порт с номером 0x80: out 0x80,al ; это просто задержка
6.6. Сдвиги ротация Операции поразрядного сдвига перемещают отдельные биты в байте, слове или двойном слове. Сдвиг может быть влево или вправо. При сдвиге вправо на одну позицию крайний правый (младший) бит теряется, а крайний левый замещается нулем. При сдвиге влево теряется крайний левый (старший) бита крайний правый замещается нулем. При сдвиге на несколько позиций выталкивается указанное количество битов, а освободившиеся биты замещаются нулями.
86
Глава 6. Прочие команды Ротация (также называемая циклическим сдвигом) не выталкивает биты, а возвращает их на место освободившихся. В результате ротации вправо крайний правый бит встанет на место крайнего левого, а остальные биты будут сдвинуты на одну позицию вправо. Где это может использоваться, вы поймете из описания самих команд. Команды SHR и SHL — сдвиг беззнаковых чисел Команды SHR и SHL поразрядно сдвигают беззнаковые целые числа вправо и влево соответственно Это самый быстрый способ умножить или разделить целое число на степень двойки. Представим число 5 в двоичной системе — 0101b. После умножения на 2 мы получим 10, в двоичной системе это число 01010b. Сравнивая два двоичных числа, вы, наверное, заметили, как можно быстро получить из числа 5 число
10: с помощью сдвига на один бит влево, добавив один нуль справа. Точно также можно умножить число на любую степень двух. Например, вместо умножения на 16 (2 в степени 4) можно сдвинуть исходное число набита влево.
0 0 0 0 0
1 0 1 0
I I I I I I I I Рис. 6.6. Умножение 5 на 2 методом поразрядного сдвига влево Деление на степень двойки выполняется по такому же принципу, только вместо поразрядного сдвига влево нужно использовать сдвиг вправо. Команде SHL нужно передать два операнда
SHL o l , о Первый должен быть регистром или адресом памяти, который нужно сдвинуть. Второй операнд определяет число позиций, на которое нужно сдвинуть. Чаще всего это непосредственное значение. Можно использовать в качестве второго операнда и регистр, но только CL — это касается всех операций сдвига и ротации. Сдвиг возможен не более чем на 32 позиции, поэтому принимается в расчет не весь второй операнда остаток от его деления на 32. Старший вытолкнутый бит сохраняется в флаге переноса CF, а младший бит заменяется нулем. Кроме флага CF используется флаг знака (SF) и флаг t
87
10: с помощью сдвига на один бит влево, добавив один нуль справа. Точно также можно умножить число на любую степень двух. Например, вместо умножения на 16 (2 в степени 4) можно сдвинуть исходное число набита влево.
0 0 0 0 0
1 0 1 0
I I I I I I I I Рис. 6.6. Умножение 5 на 2 методом поразрядного сдвига влево Деление на степень двойки выполняется по такому же принципу, только вместо поразрядного сдвига влево нужно использовать сдвиг вправо. Команде SHL нужно передать два операнда
SHL o l , о Первый должен быть регистром или адресом памяти, который нужно сдвинуть. Второй операнд определяет число позиций, на которое нужно сдвинуть. Чаще всего это непосредственное значение. Можно использовать в качестве второго операнда и регистр, но только CL — это касается всех операций сдвига и ротации. Сдвиг возможен не более чем на 32 позиции, поэтому принимается в расчет не весь второй операнда остаток от его деления на 32. Старший вытолкнутый бит сохраняется в флаге переноса CF, а младший бит заменяется нулем. Кроме флага CF используется флаг знака (SF) и флаг t
87
Ассемблер на примерах. Базовый курс переполнения (OF). За этими флагами нужно следить, выполняя действия над числами со знаком, чтобы избежать превращения положительного числа вот рицательное и наоборот (в этом случае флаги SF и OF устанавливаются в 1). Тоже самое, что и SHL, только биты сдвигаются вправо
SHR o l , о Младший бит перемещается в CF, а старший заменяется нулем. Принцип работы SHR показан на рис. 6.7. А теперь рассмотрим несколько примеров, которые помогут вам лучше усвоить прочитанное. Пример используя SHR, разделим АХ на 16, не обращая внимания на переполнение shr ах сдвигаем АХ набита вправо Пример подсчитаем количество двоичных единиц в АХ и сохраним результат
B B L . АХ — разрядный регистр, поэтому мы должны сдвинуть его влево или вправо 16 раз. После каждого сдвига мы анализируем флаг переноса CF, в который попал вытолкнутый бит, что очень легко сделать с помощью инструкции JC. Если CF равен единице, то увеличиваем BL. mov Ы , 0 инициализируем счетчик единиц BL=0 mov ex, 16 ; СХ = 16 r e p e a t : s h r ax, 1 сдвигаем набит вправо, младший бит попадает в CF j n c not_one если это 0, пропускаем следующую команду i n c Ы иначе — увеличиваем BL на 1 n o t _ o n e : loop r e p e a t повторить 16 раз После выполнения этого фрагмента кода регистр BL будет содержать количество единиц регистра АХ, асам регистр АХ будет содержать 0.
88 Рис. 6.7. Как работает SHR
SHR o l , о Младший бит перемещается в CF, а старший заменяется нулем. Принцип работы SHR показан на рис. 6.7. А теперь рассмотрим несколько примеров, которые помогут вам лучше усвоить прочитанное. Пример используя SHR, разделим АХ на 16, не обращая внимания на переполнение shr ах сдвигаем АХ набита вправо Пример подсчитаем количество двоичных единиц в АХ и сохраним результат
B B L . АХ — разрядный регистр, поэтому мы должны сдвинуть его влево или вправо 16 раз. После каждого сдвига мы анализируем флаг переноса CF, в который попал вытолкнутый бит, что очень легко сделать с помощью инструкции JC. Если CF равен единице, то увеличиваем BL. mov Ы , 0 инициализируем счетчик единиц BL=0 mov ex, 16 ; СХ = 16 r e p e a t : s h r ax, 1 сдвигаем набит вправо, младший бит попадает в CF j n c not_one если это 0, пропускаем следующую команду i n c Ы иначе — увеличиваем BL на 1 n o t _ o n e : loop r e p e a t повторить 16 раз После выполнения этого фрагмента кода регистр BL будет содержать количество единиц регистра АХ, асам регистр АХ будет содержать 0.
88 Рис. 6.7. Как работает SHR
Глава 6. Прочие команды Команды SAL и SAR — сдвиг чисел со знаком Команды SAL и SAR используются для поразрядного сдвига целых чисел со знаком (арифметического сдвига. Команда SAL — это сдвиг влево, а команда
SAR -- вправо. Формат команд таково, о Команда SAR сдвигает все биты, кроме старшего, означающего знак числа — этот бит сохраняется. Младший бит, как обычно, вытесняется в CF. Операнды обеих инструкций такие же, как у SHL и SHR. Рис 6.8. Как работает SAR Команды RCR и RCL — ротация через флаг переноса Эти команды выполняют циклический поразрядный сдвиг (ротацию RCR действует точно также, как SHR, но вместо нуля в старший бит первого операнда заносится предыдущее содержимое CF. Исходный младший бит вытесняется в CF. Команда RCL работает подобно RCR, только в обратном направлении. Формат команд таково, о Рис 6.9. Как работает RCR Рис. 6.10. Как работает RCL
89
SAR -- вправо. Формат команд таково, о Команда SAR сдвигает все биты, кроме старшего, означающего знак числа — этот бит сохраняется. Младший бит, как обычно, вытесняется в CF. Операнды обеих инструкций такие же, как у SHL и SHR. Рис 6.8. Как работает SAR Команды RCR и RCL — ротация через флаг переноса Эти команды выполняют циклический поразрядный сдвиг (ротацию RCR действует точно также, как SHR, но вместо нуля в старший бит первого операнда заносится предыдущее содержимое CF. Исходный младший бит вытесняется в CF. Команда RCL работает подобно RCR, только в обратном направлении. Формат команд таково, о Рис 6.9. Как работает RCR Рис. 6.10. Как работает RCL
89
Ассемблер на примерах. Базовый курс Команды ROR и ROL — ротация с выносом во флаг переноса Эти команды выполняют другой вариант циклического сдвига ROR сначала копирует младший бит первого операнда в его старший бита потом заносит его в CF; ROL работает в обратном направлении.
ROR o l , о
ROL o l , о Операнды этих команд аналогичны операндам команд RCR и RCL. Рис. 6.11. Как работает ROR
6.7. Псевдокоманды Некоторые из команд, рассмотренных нами, могут работать с операндом, расположенным в памяти. Классический пример — команда MOV AX, [number], загружающая в регистр АХ значение из области памяти, адрес которой представлен символическим обозначением «number». Номы до сих пор не знаем, как связать символическое обозначение и реальный адрес в памяти. Как раз для этого и служат псевдокоманды. Предложения языка ассемблера делятся на команды и псевдокоманды (директивы. Команды ассемблера — это символические имена машинных команд, обработка их компилятором приводит к генерации машинного кода Псевдокоманды же управляют работой самого компилятора На одной и той же аппаратной архитектуре могут работать различные ассемблеры их команды обязательно будут одинаковыми, но псевдокоманды могут быть разными. Псевдокоманды DB, DW и DD — определение констант Чаще всего используется псевдокоманда DB (define byte), позволяющая определить числовые константы и строки. Рассмотрим несколько примеров d b 0 x 5 5 d b 0 x 5 5 , 0 x 5 6 , 0 x 5 7 db 'а db 'Hello',13,10,'$' один байт в шестнадцатеричном виде три последовательных байта 0x5 5,
;0х5б, 0x57 можно записать символ в одинарных кавычках. Получится последовательность 0x61, 0x55 можно записать целую строку. Получится 0x48, 0x65, ОхбС, ОхбС,
;0x6F, OxD, OxA, 0x24 90
ROR o l , о
ROL o l , о Операнды этих команд аналогичны операндам команд RCR и RCL. Рис. 6.11. Как работает ROR
6.7. Псевдокоманды Некоторые из команд, рассмотренных нами, могут работать с операндом, расположенным в памяти. Классический пример — команда MOV AX, [number], загружающая в регистр АХ значение из области памяти, адрес которой представлен символическим обозначением «number». Номы до сих пор не знаем, как связать символическое обозначение и реальный адрес в памяти. Как раз для этого и служат псевдокоманды. Предложения языка ассемблера делятся на команды и псевдокоманды (директивы. Команды ассемблера — это символические имена машинных команд, обработка их компилятором приводит к генерации машинного кода Псевдокоманды же управляют работой самого компилятора На одной и той же аппаратной архитектуре могут работать различные ассемблеры их команды обязательно будут одинаковыми, но псевдокоманды могут быть разными. Псевдокоманды DB, DW и DD — определение констант Чаще всего используется псевдокоманда DB (define byte), позволяющая определить числовые константы и строки. Рассмотрим несколько примеров d b 0 x 5 5 d b 0 x 5 5 , 0 x 5 6 , 0 x 5 7 db 'а db 'Hello',13,10,'$' один байт в шестнадцатеричном виде три последовательных байта 0x5 5,
;0х5б, 0x57 можно записать символ в одинарных кавычках. Получится последовательность 0x61, 0x55 можно записать целую строку. Получится 0x48, 0x65, ОхбС, ОхбС,
;0x6F, OxD, OxA, 0x24 90
Глава 6. Прочие команды Для определения порции данных размера, кратного слову, служит директивах второй байт заполняется нулями Директива DD (define double word) задает значение порции данных размера, кратного двойному слову
dd 0x12345678 х 0x56 0x34 0x12 dd 1.234567e20 так определяются числа с плавающей точкой А вот так определяется переменная, тот самый «number»:
number dd 0x1 переменная number инициализирована значением 1 Переменная «number» теперь представляет адрес в памяти, по которому записано значение 0x00000001 длиной в двойное слово. Псевдокоманды RESB,
R E S W H
RESD — объявление переменных Вторая группа псевдокоманд позволяет определить неинициализированные данные. Если в первом случае мы заносим данные в память, то сейчас мы просто укажем, сколько байтов нужно зарезервировать для будущих данных. Память для этих неинициализированных переменных распределяется динамически, асами данные не содержатся в исполнимом файле. Грубо говоря, предыдущую группу инструкций удобно использовать для определения константа эту группу — для переменных. Для резервирования памяти служат три директивы RESB (резервирует байт,
RESW (резервирует слово) и RESD (резервирует двойное слово. Аргументом этих псевдокоманд является количество резервируемых позиций
resb 1 резервирует 1 байт resb 2 резервирует 2 байта resw 2 резервирует 4 байта (2 слова) resd 1 резервирует 4 байта number resd l резервирует 4 байта для переменной
;"number" buffer resb 64 резервирует 64 байта для переменной buffer Некоторые ассемблеры поддерживают дополнительные директивы выделения памяти, номы будем использовать ассемблер NASM, который их не поддерживает. Поэтому мы ограничимся директивами RESx. Псевдокоманда TIMES — повторение следующей псевдокоманды Директива TIMES — это псевдокоманда префиксного типа, то есть она используется только в паре с другой командой. Она повторяет последующую
91
dd 0x12345678 х 0x56 0x34 0x12 dd 1.234567e20 так определяются числа с плавающей точкой А вот так определяется переменная, тот самый «number»:
number dd 0x1 переменная number инициализирована значением 1 Переменная «number» теперь представляет адрес в памяти, по которому записано значение 0x00000001 длиной в двойное слово. Псевдокоманды RESB,
R E S W H
RESD — объявление переменных Вторая группа псевдокоманд позволяет определить неинициализированные данные. Если в первом случае мы заносим данные в память, то сейчас мы просто укажем, сколько байтов нужно зарезервировать для будущих данных. Память для этих неинициализированных переменных распределяется динамически, асами данные не содержатся в исполнимом файле. Грубо говоря, предыдущую группу инструкций удобно использовать для определения константа эту группу — для переменных. Для резервирования памяти служат три директивы RESB (резервирует байт,
RESW (резервирует слово) и RESD (резервирует двойное слово. Аргументом этих псевдокоманд является количество резервируемых позиций
resb 1 резервирует 1 байт resb 2 резервирует 2 байта resw 2 резервирует 4 байта (2 слова) resd 1 резервирует 4 байта number resd l резервирует 4 байта для переменной
;"number" buffer resb 64 резервирует 64 байта для переменной buffer Некоторые ассемблеры поддерживают дополнительные директивы выделения памяти, номы будем использовать ассемблер NASM, который их не поддерживает. Поэтому мы ограничимся директивами RESx. Псевдокоманда TIMES — повторение следующей псевдокоманды Директива TIMES — это псевдокоманда префиксного типа, то есть она используется только в паре с другой командой. Она повторяет последующую
91
Ассемблер на примерах. Базовый курс псевдокоманду указанное количество раз, подобно директиве DUP из ассемблера Borland TASM. Применяется эта директива в тех случаях, когда нужно забить некоторую область памяти повторяющимся образцом. Следующий код определяет строку, состоящую из 64 повторяющихся «Hello»:
many_hello: times 64 db 'Hello' Первый аргумент, указывающий количество повторений, может быть ивы bbражением. Например, задачу разместить строку в области памяти размером в
32 байта и заполнить пробелами оставшееся место легко решить с помощью директивы TIMES:
buffer db "Hello" определяем строку times 32-($-buffer) db ' ' определяем нужное количество пробелов Выражение 32-($-buffer) возвратит значение 27, потому что $-buffer равно текущей позиции минус позиция начала строки, то есть 5. Вместе с TIMES можно использовать не только псевдокоманды, но и команды процессора
times 5 inc eax ;5 раз выполнить INC EAX В результате будет сгенерирован код
inc eax inc eax inc eax inc eax
inc eax Псевдокоманда INCBIN — подключение двоичного файла Эта директива будет полезна более опытным разработчикам. Она упаковывает графические или звуковые данные вместе с исполняемым файлом
incbin "sound.wav" упаковываем весь файл incbin "sound.wav",512 пропускаем первые 512 байтов incbin "sound.wav",512,80 пропускаем первые 512 байтов и последние 80 Псевдокоманда EQU — вычисление константных выражений Эта директива определяет константу, известную вовремя компиляции. В качестве значения константы можно указывать также константное выражение. Директиве EQU должно предшествовать символическое имя
four EQU 4 ; тривиальный пример. Позже я покажу и нетривиальные
92
many_hello: times 64 db 'Hello' Первый аргумент, указывающий количество повторений, может быть ивы bbражением. Например, задачу разместить строку в области памяти размером в
32 байта и заполнить пробелами оставшееся место легко решить с помощью директивы TIMES:
buffer db "Hello" определяем строку times 32-($-buffer) db ' ' определяем нужное количество пробелов Выражение 32-($-buffer) возвратит значение 27, потому что $-buffer равно текущей позиции минус позиция начала строки, то есть 5. Вместе с TIMES можно использовать не только псевдокоманды, но и команды процессора
times 5 inc eax ;5 раз выполнить INC EAX В результате будет сгенерирован код
inc eax inc eax inc eax inc eax
inc eax Псевдокоманда INCBIN — подключение двоичного файла Эта директива будет полезна более опытным разработчикам. Она упаковывает графические или звуковые данные вместе с исполняемым файлом
incbin "sound.wav" упаковываем весь файл incbin "sound.wav",512 пропускаем первые 512 байтов incbin "sound.wav",512,80 пропускаем первые 512 байтов и последние 80 Псевдокоманда EQU — вычисление константных выражений Эта директива определяет константу, известную вовремя компиляции. В качестве значения константы можно указывать также константное выражение. Директиве EQU должно предшествовать символическое имя
four EQU 4 ; тривиальный пример. Позже я покажу и нетривиальные
92
Глава 6. Прочие команды Оператор SEG — смена сегмента При создании больших программ для реального режима процессора нам нужно использовать для кода и данных несколько сегментов, чтобы обойти проблему 16-битной адресации. Пока будем считать, что сегмент — это часть адреса переменной. С помощью оператора SEG в сегментный регистр может быть загружен адрес сегмента, где физически расположена переменная mov a x , s e g c o u n t e r поместить в АХ адрес сегмента, где размещена переменная c o u n t e r mov e s , a x поместить этот адрес в сегментный регистр. это можно сделать только косвенно mov b x , c o u n t e r ; загрузить в ВХ адрес (смещение) переменной c o u n t e r . Теперь пара ES:BX содержит полный адрес переменной c o u n t e r mov c x , e s : [ b x ] копировать значение переменной в регистр СХ В наших учебных программах на сегментные регистры можно не обращать внимания, потому что они содержат одно и тоже значение. Поэтому оператор
SEG нам пока не понадобится.
6.8. Советы по использованию команд Теперь, когда вы уже познакомились с основными командами, самое время поговорить об оптимизации кода программы. Сегодня, когда компьютеры очень быстры, а память стоит очень дешево, почти никто не считается ни со скоростью работы программы, ни с объемом занимаемой памяти. Но язык ассемблера часто применяется в особых ситуациях — в узких местах, где быстродействие или малый размер программы особенно важны. Удачным подбором команд можно существенно сократить размер программы или увеличить ее быстродействие — правда, при этом пострадает удобочитаемость. Ускорить работу программы иногда можно также путем правильного выделения памяти. Директива ALIGN — выравнивание данных в памяти Мы знаем, что процессоры работают с регистрами на порядок быстрее, чем с операндами, расположенными в памяти. Поэтому желательно выполнять все вычисления в регистрах, сохраняя в памяти только результат. При обработке больших массивов частые обращения к памяти неизбежны. Скорость доступа к памяти можно увеличить, если размещать данные по адресам, кратным степени двойки. Дело в том, что между памятью и про
SEG нам пока не понадобится.
6.8. Советы по использованию команд Теперь, когда вы уже познакомились с основными командами, самое время поговорить об оптимизации кода программы. Сегодня, когда компьютеры очень быстры, а память стоит очень дешево, почти никто не считается ни со скоростью работы программы, ни с объемом занимаемой памяти. Но язык ассемблера часто применяется в особых ситуациях — в узких местах, где быстродействие или малый размер программы особенно важны. Удачным подбором команд можно существенно сократить размер программы или увеличить ее быстродействие — правда, при этом пострадает удобочитаемость. Ускорить работу программы иногда можно также путем правильного выделения памяти. Директива ALIGN — выравнивание данных в памяти Мы знаем, что процессоры работают с регистрами на порядок быстрее, чем с операндами, расположенными в памяти. Поэтому желательно выполнять все вычисления в регистрах, сохраняя в памяти только результат. При обработке больших массивов частые обращения к памяти неизбежны. Скорость доступа к памяти можно увеличить, если размещать данные по адресам, кратным степени двойки. Дело в том, что между памятью и про
Ассемблер на примерах. Базовый курс цессором имеется система буферов памяти, недоступная программисту. Эти буферы содержат блоки чаще всего используемых данных из основной памяти или несохраненные данные. Выравнивание данных по некоторым адресам освобождает буферы, поэтому данные обрабатываются быстрее. К сожалению, каждый тип процессора предпочитает свой тип выравнивания. Мы можем сообщить компилятору требуемый тип директивой ALIGN. Ее аргумент — то число, по адресам, кратным которому, требуется размещать данные a l i g n 4 размещает данные по адресам, кратным 4 a l i g n 16 размещает данные по адресам, кратным 16 Загрузка значения в регистр Много обращений к памяти можно исключить, если отказаться от непосредственных операндов, что положительно скажется на быстродействии программы. Например, как мы привыкли инициализировать счетчик Командой
MOV, которая копирует в регистр значение 0? Намного рациональнее для этого использовать логическую функцию XOR (если ее аргументы одинаковы, результат равен 0): хог еах,еах в машинном коде 0хЗЗ,0хС0 Эта команда будет выполнена быстрее и займет меньше памяти, чем mov eax,0 в машинном коде 0хВ8,0,0,0,0 Но нужно помнить, что XOR изменяет регистр признаков, поэтому ее нужно использовать только в тех случаях, когда его изменение не имеет значения. Другой часто используемой парой инструкций являются хог еах,еах ;ЕАХ = 0 inc eax увеличиваем на 1 Этот фрагмент кода загружает в ЕАХ значение 1. Если использовать DEC вместо INC, в ЕАХ будет значение - 1 . Оптимизируем арифметику Если вам нужно добавить к значению константу, то имейте ввиду, что несколько команд INC будут выполнены быстрее, чем одна ADD. Таким образом, вместо команды add eax,4 добавляем 4 к ЕАХ следует использовать четыре команды inc eax ; добавляем 1 к ЕАХ inc eax inc eax inc eax
94
MOV, которая копирует в регистр значение 0? Намного рациональнее для этого использовать логическую функцию XOR (если ее аргументы одинаковы, результат равен 0): хог еах,еах в машинном коде 0хЗЗ,0хС0 Эта команда будет выполнена быстрее и займет меньше памяти, чем mov eax,0 в машинном коде 0хВ8,0,0,0,0 Но нужно помнить, что XOR изменяет регистр признаков, поэтому ее нужно использовать только в тех случаях, когда его изменение не имеет значения. Другой часто используемой парой инструкций являются хог еах,еах ;ЕАХ = 0 inc eax увеличиваем на 1 Этот фрагмент кода загружает в ЕАХ значение 1. Если использовать DEC вместо INC, в ЕАХ будет значение - 1 . Оптимизируем арифметику Если вам нужно добавить к значению константу, то имейте ввиду, что несколько команд INC будут выполнены быстрее, чем одна ADD. Таким образом, вместо команды add eax,4 добавляем 4 к ЕАХ следует использовать четыре команды inc eax ; добавляем 1 к ЕАХ inc eax inc eax inc eax
94
Глава 6. Прочие команды Помните, что ни INC, ни DEC не устанавливают флаг переноса, поэтому не используйте их в разрядной арифметике, где используется перенос. Зато при вычислении значения адреса вы можете смело использовать INC и DEC, поскольку арифметические операции над адресами никогда не требуют переноса. Вы уже знаете, что для быстрого умножения и деления можно использовать поразрядные сдвиги влево и право. Умножение можно выполнять также с помощью команды LEA, которая вычисляет эффективный адрес второго операнда. Вот несколько примеров lea ebx,[ecx+edx*4+0x500] загружаем в ЕВХ результат выражения ЕСХ + EDX*4 + 0x500 lea ebx, [eax+eax*4-l] ; ЕВХ = ЕАХ*5 - 1 lea ebx, [еах+еах*8] ; ЕВХ = ЕАХ*9 lea ecx,[eax+ebx] вычисляем ЕСХ = ЕАХ + ЕВХ Операции сравнения Очень часто нам нужно проверить какой-то регистр на наличие в нем 0. Совсем необязательно использовать для этого СМР, можно использовать OR или TEST. Таким образом, вместо команд сmр еах,0 ;ЕАХ равен 0 ? jz is_zero да Переходим к is_zero можно написать следующее or еах,еах
jz is_zero этот OR устанавливает тот же флаг (ZF), если результат равен Ода Переходим к is_zero Если оба операнда команды OR имеют одно и тоже значение, первый операнд сохраняется. Кроме того, команда изменяет регистр признаков если результат операции OR нулевой, то нулевой признак (ZF) будет установлен в 1. Любая арифметическая команда устанавливает флаг нуля, поэтому совсем необязательно использовать СМР для проверки на 0. Можно сразу использовать jz: dec еах jz now_zero переход, если ЕАХ после декремента равен 0 Иногда нужно узнать, содержит ли регистр отрицательное число. Для этого можно использовать команду TEST, которая вычисляет логическое И, ноне сохраняет результата только устанавливает флаги регистра признаков. Флаг знака SF будет установлен в 1, если результат отрицательный, то есть если старший бит результата равен 1. Значит, мы можем выполнить TEST с двумя одинаковыми операндами если число отрицательное, то флаг SF будет установлен (логическое И для двух отрицательных чисел даст 1 (1 AND 1 = 1) в старшем бите, то есть SF=1):
95
jz is_zero этот OR устанавливает тот же флаг (ZF), если результат равен Ода Переходим к is_zero Если оба операнда команды OR имеют одно и тоже значение, первый операнд сохраняется. Кроме того, команда изменяет регистр признаков если результат операции OR нулевой, то нулевой признак (ZF) будет установлен в 1. Любая арифметическая команда устанавливает флаг нуля, поэтому совсем необязательно использовать СМР для проверки на 0. Можно сразу использовать jz: dec еах jz now_zero переход, если ЕАХ после декремента равен 0 Иногда нужно узнать, содержит ли регистр отрицательное число. Для этого можно использовать команду TEST, которая вычисляет логическое И, ноне сохраняет результата только устанавливает флаги регистра признаков. Флаг знака SF будет установлен в 1, если результат отрицательный, то есть если старший бит результата равен 1. Значит, мы можем выполнить TEST с двумя одинаковыми операндами если число отрицательное, то флаг SF будет установлен (логическое И для двух отрицательных чисел даст 1 (1 AND 1 = 1) в старшем бите, то есть SF=1):
95
Ассемблер на примерах. Базовый курс t e s t eax,eax вызываем TEST для двух одинаковых операндов js is_negative переходим, если SF=1 Разное Обычно вместо нескольких простых команд проще использовать одну сложную (например, LOOP или команды для манипуляций со строками. Очень часто такой подходи правильнее сложные команды оптимизированы и могут выполнить туже самую функцию быстрее, чем множество простых команд. Размер исполняемого файла может бьпь уменьшен, если оптимизировать переходы. По умолчанию используется средний шаг — near, но если все цели находятся в пределах 128 байтов, используйте короткий тип перехода (short). Это позволит сэкономить один-три байта на каждом переходе.
1 ... 4 5 6 7 8 9 10 11 ... 20
96
Глава 7 Полезные фрагменты кода Простые примеры Преобразование числа в строку Преобразование строки в число Ассемблер на примерах. Базовый курс
В этой главе мы представим несколько подпрограмм, которые можно использовать в большинстве ваших ассемблерных программ. Мы будем рассматривать только системно-независимый код, одинаково пригодный для любой операционной системы. Программы, использующие особенности операционных систем, будут рассмотрены в следующих главах.
7.1. Простые примеры Начнем с нескольких простых примеров, которые помогут уменьшить пропасть между языком высокого уровня и ассемблером. Сложение двух переменных Задача сложить два разрядных числа, содержащихся в переменных num- b e r l и number2, а результат сохранить в переменной r e s u l t . Сначала нужно выбрать регистры, куда будут загружены нужные значения. Затем нужно сложить эти регистры, а результат записать в переменную r e
s u l t : mov eax,[number1] mov ebx,[number2] add eax,ebx mov [result],eax квадратные скобки означают доступ к памяти
;ЕВХ = number2
;ЕАХ = ЕАХ + ЕВХ сохраняем результат в переменной r e s u l t number1 dd 8 number2 dd 2 определяем переменную numberl и инициализируем 8 переменную number2 инициализируем значением 2 определяем переменную result r e s u l t dd 0 Мы можем переписать программу, используя на подхвате регистр ЕАХ: mov eax,[numberl] ;EAX = "numberl" add eax,[number2] ;EAX = EAX + number2 mov [result],eax сохраняем результат в переменной result
98
7.1. Простые примеры Начнем с нескольких простых примеров, которые помогут уменьшить пропасть между языком высокого уровня и ассемблером. Сложение двух переменных Задача сложить два разрядных числа, содержащихся в переменных num- b e r l и number2, а результат сохранить в переменной r e s u l t . Сначала нужно выбрать регистры, куда будут загружены нужные значения. Затем нужно сложить эти регистры, а результат записать в переменную r e
s u l t : mov eax,[number1] mov ebx,[number2] add eax,ebx mov [result],eax квадратные скобки означают доступ к памяти
;ЕВХ = number2
;ЕАХ = ЕАХ + ЕВХ сохраняем результат в переменной r e s u l t number1 dd 8 number2 dd 2 определяем переменную numberl и инициализируем 8 переменную number2 инициализируем значением 2 определяем переменную result r e s u l t dd 0 Мы можем переписать программу, используя на подхвате регистр ЕАХ: mov eax,[numberl] ;EAX = "numberl" add eax,[number2] ;EAX = EAX + number2 mov [result],eax сохраняем результат в переменной result
98
Глава 7. Полезные фрагменты кода Сложение двух элементов массива Задача сложить два 32-битных числа. Регистр EDI содержит указатель на первое слагаемое, второе слагаемое расположено непосредственно за первым. Результат записать в EDX. Фактически мы имеем массив из двух 32-битных чисел, адрес которого задан указателем в регистре EDI. Каждый элемент массива занимает 4 байта, следовательно, второй элемент расположен по адресу, на 4 байта большему. mov e d x , [ e d i ] add e d x , [ e d i + 4 ; загружаем в EDX первый элемент массива складываем его со вторым, результат — в EDX Дополним программу загрузкой регистра EDI: mov e d i , n u m b e r s загружаем в EDI адрес массива numbers
;какие-то команды mov e d x , [ e d i ] загружаем в EDX первый элемент массива add e d x , [ e d i + 4 ] прибавляем второй элемент numbers dd l dd 2 массив numbers инициализируется значениями 1 и 2, поэтому результат в EDX будет равен 3 у второго элемента массива нет особого имени В следующем пункте мы расскажем, как работать с массивами в цикле. Суммируем элементы массива Задача вычислить сумму массива разрядных чисел, если в ESI загружен адрес первого элемента массива. Массив заканчивается нулевым байтом. Сумма разрядных чисел может оказаться числом большей разрядности, поэтому на всякий случай отведем для хранения результата 32-битный регистр. Элементы массива будут суммироваться до тех пор, пока не будет встречен нулевой байт. mov esi,array mov ebx,0 mov eax,ebx again: mov al,[esi] inc esi add ebx,eax cmp a1,0 jnz again array db 1,2,3,4 загружаем в ESI начало массива
;ЕВХ = О
;ЕАХ = О загружаем в AL элемент массива перемещаем указатель на след. элемент
;ЕВХ = ЕВХ + ЕАХ
;AL равен нулю переходим к again, если AL не О
5,6,7,8,0 инициализируем массив. Сумма (ЕВХ) должна быть равна 3 6 99
;какие-то команды mov e d x , [ e d i ] загружаем в EDX первый элемент массива add e d x , [ e d i + 4 ] прибавляем второй элемент numbers dd l dd 2 массив numbers инициализируется значениями 1 и 2, поэтому результат в EDX будет равен 3 у второго элемента массива нет особого имени В следующем пункте мы расскажем, как работать с массивами в цикле. Суммируем элементы массива Задача вычислить сумму массива разрядных чисел, если в ESI загружен адрес первого элемента массива. Массив заканчивается нулевым байтом. Сумма разрядных чисел может оказаться числом большей разрядности, поэтому на всякий случай отведем для хранения результата 32-битный регистр. Элементы массива будут суммироваться до тех пор, пока не будет встречен нулевой байт. mov esi,array mov ebx,0 mov eax,ebx again: mov al,[esi] inc esi add ebx,eax cmp a1,0 jnz again array db 1,2,3,4 загружаем в ESI начало массива
;ЕВХ = О
;ЕАХ = О загружаем в AL элемент массива перемещаем указатель на след. элемент
;ЕВХ = ЕВХ + ЕАХ
;AL равен нулю переходим к again, если AL не О
5,6,7,8,0 инициализируем массив. Сумма (ЕВХ) должна быть равна 3 6 99
Ассемблер на примерах. Базовый курс Рис. 7.1. Блок-схема алгоритма суммирования массива Чет и нечет Задача определить, четное или нечетное значение содержит регистр АХ. Четное число отличается от нечетного тем, что его младший бит равен нулю. Используя SHR, мы можем сдвинуть этот битва затем проверить этот бит, выполнив условный переход. p u s h ax сохраняем исходное значение АХ в стеке ах перемещаем младший бит АХ в CF pop ax восстанавливаем оригинальное значение jc odd если CF = 1, переходим к odd e v e n : ; действия, если число в в АХ - четное odd: ; действия, если число в в АХ — нечетное Как обычно, мы можем переписать этот фрагмент кода проще t e s t a l , l младший бит маски содержит 1, вызываем TEST jz even ;ZF (флаг нуля) установлен, если результат равен 0, о есть младший бит = 0, то есть число четное odd: e v e n : ; действия, если АХ — четное Обратите внимание, что мы тестировали только AL, а не весь регистр АХ. Старшие биты АХ для проверки четности совершенно ненужны Глава 7. Полезные фрагменты кода Перестановка битов в числе Задача реверсируем порядок битов числа, сохраненного в AL, то есть переставим младший бит на место старшего, второй справа — на место второго слева и т.д. Полученный результат сохраним в АН. Например, наше число равно 0x15, то есть 00010101b. После реверсирования мы получим его зеркальное отображение 10101000b, то есть 0хА8. Как обычно, поставленную задачу можно решить несколькими способами. Можно пройтись по всем битам AL так, как показано в параграфе о битовых массивах, проверяя значение отдельных битов и устанавливая в соответствии с ним значения отдельных битов АН, но это не лучшее решение. Оптимальным в нашем случае будет использование инструкций сдвига и ротации. Например, используя SHR (как в предыдущем примере, мы можем вытолкнуть один битв (флаг переноса) и, используя RCL, переместить этот битв младший бит операнда. Повторив это действие 8 раз, мы решим поставленную задачу. mov с х , 8 наш счетчик СХ = 8 t h e l o o p : s h r a l , l сдвигаем AL набит вправо, младший битв сдвигаем АН набит влево, заменяем младший бит на CF l o o p t h e l o o p повторяем 8 раз Проверка делимости числа нацело Задача определить, заканчивается ли десятичная запись числа цифрой нуль. Простого сравнения битов здесь недостаточно, мы должны разделить число на
10 (ОхА). Операция целочисленного деления помещает в регистр AL частное, а в регистр АН — остаток. Нам останется только сравнить остаток с нулем если число делится нацело, то передадим управление наметку. Для определенности считаем, что наше число находится в регистре АХ mov Ы,0хА ;BL = 10 - делитель d i v bl делим АХ на BL cmp a h , 0 остаток = 0? jz y e s если да, перейти к YES по если нет, продолжить yes :
101
10 (ОхА). Операция целочисленного деления помещает в регистр AL частное, а в регистр АН — остаток. Нам останется только сравнить остаток с нулем если число делится нацело, то передадим управление наметку. Для определенности считаем, что наше число находится в регистре АХ mov Ы,0хА ;BL = 10 - делитель d i v bl делим АХ на BL cmp a h , 0 остаток = 0? jz y e s если да, перейти к YES по если нет, продолжить yes :
101
Ассемблер на примерах. Базовый курс
7.2. Преобразование числа в строку Чтобы вывести число на экран, нужно преобразовать его в строку. В высоко
уровневых языках программирования это действие давно стало тривиальным для преобразования числа в строку достаточно вызвать всего одну функцию. А вот на языке ассемблера эту подпрограмму еще нужно написать, чем мы и займемся в этом параграфе. Как бы мы решали эту задачу на С Делили бы в цикле на 10 и записывали остатки, преобразуя их в символы цифр добавлением кода цифры 0 (см. таблицу кодов ASCII, рис. 1.2). Цикл повторялся бы до тех пор, пока частное не стало бы нулем. Вот соответствующий код
#include void main(void) { unsigned int number; char remainder; number=12345 678; while (number 1= 0)
{ remainder = (number % 10)+'0';
/* remainder = number mod 10 + char('0') */ number /=10; /* number = number div 10*/ printf("%c",remainder);
}
} Рис. 7.2. Блок-схема алгоритма преобразования числа в строку
102
7.2. Преобразование числа в строку Чтобы вывести число на экран, нужно преобразовать его в строку. В высоко
уровневых языках программирования это действие давно стало тривиальным для преобразования числа в строку достаточно вызвать всего одну функцию. А вот на языке ассемблера эту подпрограмму еще нужно написать, чем мы и займемся в этом параграфе. Как бы мы решали эту задачу на С Делили бы в цикле на 10 и записывали остатки, преобразуя их в символы цифр добавлением кода цифры 0 (см. таблицу кодов ASCII, рис. 1.2). Цикл повторялся бы до тех пор, пока частное не стало бы нулем. Вот соответствующий код
#include
{ remainder = (number % 10)+'0';
/* remainder = number mod 10 + char('0') */ number /=10; /* number = number div 10*/ printf("%c",remainder);
}
} Рис. 7.2. Блок-схема алгоритма преобразования числа в строку
102
Глава 7. Полезные фрагменты кода Правда, программа выведет немного не то, что мы ожидали наше число равно
12345678, нона экране мы увидим 87654321, — это потому что при делении мы получаем сначала младшие цифры, а потом старшие. Как теперь перевернуть полученную последовательность символов При программировании на языке ассемблера нам доступен стек, куда можно сохранять наши остатки, а затем, вытолкнув их из стека, получить правильную последовательность. Мы оформим функцию преобразования числа в строку в виде подпрограммы, которую вы можете использовать в любой своей программе на языке ассемблера. Первая проблема, которую предстоит решить — передача параметров. Мы знаем, что в высокоуровневых языках программирования параметры передаются через стек. Однако мы будем использовать более быстрый способ передачи параметров — через регистры процессора. В регистр ЕАХ мы занесем число, а после выполнения подпрограммы регистр
EDI будет содержать адрес памяти (или указатель, по которому сохранен первый байт нашей строки. Сама строка будет заканчиваться нулевым байтом как в С. Наша подпрограмма convert состоит из двух циклов. Первый цикл соответствует циклу while из предыдущего листинга — внутри него мы будем помещать остатки в стек, попутно преобразуя их в символы цифр, пока частное не сравняется с нулем. Второй цикл извлекает цифры из стека и записывает в указанную строку. Наша подпрограмма convert может выглядеть так, как показано в листинге 7.1. Листинг 7.1. Программа преобразования числа в строку предварительный вариант)
convert: mov ecx,0 mov О
.dividei mov edx,0 div ebx add edx, ' 0' push edx inc ecx cmp eax,0 jnz .divide
.reverse: pop eax
;ЕСХ = О
;ЕВХ = 010
;EDX = 0 делим ЕАХ на ЕВХ, частное в ЕАХ, остаток в EDX добавляем код цифры 0 к остатку сохраняем в стеке увеличиваем счетчик цифр в стеке закончили (частное равно 0?) если нет, переходим к .divide иначе число уже преобразовано, цифры сохранены в стеке, ЕСХ содержит их количество выталкиваем цифру из стека
103
12345678, нона экране мы увидим 87654321, — это потому что при делении мы получаем сначала младшие цифры, а потом старшие. Как теперь перевернуть полученную последовательность символов При программировании на языке ассемблера нам доступен стек, куда можно сохранять наши остатки, а затем, вытолкнув их из стека, получить правильную последовательность. Мы оформим функцию преобразования числа в строку в виде подпрограммы, которую вы можете использовать в любой своей программе на языке ассемблера. Первая проблема, которую предстоит решить — передача параметров. Мы знаем, что в высокоуровневых языках программирования параметры передаются через стек. Однако мы будем использовать более быстрый способ передачи параметров — через регистры процессора. В регистр ЕАХ мы занесем число, а после выполнения подпрограммы регистр
EDI будет содержать адрес памяти (или указатель, по которому сохранен первый байт нашей строки. Сама строка будет заканчиваться нулевым байтом как в С. Наша подпрограмма convert состоит из двух циклов. Первый цикл соответствует циклу while из предыдущего листинга — внутри него мы будем помещать остатки в стек, попутно преобразуя их в символы цифр, пока частное не сравняется с нулем. Второй цикл извлекает цифры из стека и записывает в указанную строку. Наша подпрограмма convert может выглядеть так, как показано в листинге 7.1. Листинг 7.1. Программа преобразования числа в строку предварительный вариант)
convert: mov ecx,0 mov О
.dividei mov edx,0 div ebx add edx, ' 0' push edx inc ecx cmp eax,0 jnz .divide
.reverse: pop eax
;ЕСХ = О
;ЕВХ = 010
;EDX = 0 делим ЕАХ на ЕВХ, частное в ЕАХ, остаток в EDX добавляем код цифры 0 к остатку сохраняем в стеке увеличиваем счетчик цифр в стеке закончили (частное равно 0?) если нет, переходим к .divide иначе число уже преобразовано, цифры сохранены в стеке, ЕСХ содержит их количество выталкиваем цифру из стека
103
Ассемблер на примерах. Базовый курс mov [edi] add e d i , 1 dec ecx al cmp ecx,0 jnz . reverse ret помещаем ее в строку перемещаем указатель наследующий символ уменьшаем счетчик цифр, оставшихся в стеке цифры кончились Нет Обрабатываем следующую Да Возвращаемся Эту программу я намеренно написал с недостатками. Попробуем ее оптимизировать, руководствуясь предыдущей главой. Начнем с замены команды MOV ecx, 0 на более быструю — XOR ecx, ecx. Далее, вместо загрузки в ЕВХ 10 мы можем сбросить этот регистр (записать в него 0), а потом загрузить 10 в BL: так мы избавимся от использования длинного непосредственного операнда. Проверка ЕАХ на 0 может быть заменена инструкцией OR eax,eax (или
TEST eax,eax). А две команды записи байта в строку можно заменить одной вместо mov [ e d i ] , a l add e d i , 1 напишем stosb Эта команда делает тоже самое, но занимает всего один байт. Последний цикл нашей функции можно переписать с использованием команды
LOOP. В итоге функция будет выглядеть так, как показано в листинге 7.2. Листинг 7,2. Программа преобразования числа в строку промежуточный вариант)
convert: хог есх,есх xor ebx,ebx mov Ы , 1 0
.divide: xor edx,edx div ebx add dl,'0' push edx inc ecx or eax,eax jnz .divide
;ЕСХ = О
;ЕВХ
= О теперь ЕВХ = 010
;EDX = 0 делим ЕАХ на ЕВХ, частное в ЕАХ, остаток в EDX добавляем код цифры 0 к остатку сохраняем в стеке увеличиваем счетчик цифр в стеке закончили (частное равно 0?) если не 0, переходим к .divide. Иначе число уже преобразовано, цифры сохранены в стеке,
;ЕСХ содержит их количество
104
TEST eax,eax). А две команды записи байта в строку можно заменить одной вместо mov [ e d i ] , a l add e d i , 1 напишем stosb Эта команда делает тоже самое, но занимает всего один байт. Последний цикл нашей функции можно переписать с использованием команды
LOOP. В итоге функция будет выглядеть так, как показано в листинге 7.2. Листинг 7,2. Программа преобразования числа в строку промежуточный вариант)
convert: хог есх,есх xor ebx,ebx mov Ы , 1 0
.divide: xor edx,edx div ebx add dl,'0' push edx inc ecx or eax,eax jnz .divide
;ЕСХ = О
;ЕВХ
= О теперь ЕВХ = 010
;EDX = 0 делим ЕАХ на ЕВХ, частное в ЕАХ, остаток в EDX добавляем код цифры 0 к остатку сохраняем в стеке увеличиваем счетчик цифр в стеке закончили (частное равно 0?) если не 0, переходим к .divide. Иначе число уже преобразовано, цифры сохранены в стеке,
;ЕСХ содержит их количество
104
Глава 7. Полезные фрагменты кода
. r e v e r s e : pop eax ; выталкиваем цифру из стека s t o s b ; записываем AL по адресу, содержащемуся в
;EDI, увеличиваем EDI на 1 loop . r e v e r s e ;ЕСХ=ЕСХ-1, переходим, если ЕСХ неравно О r e t Да Возвращаемся Мы слегка улучшили программу, но она все еще работает неправильно. Необходимый нулевой байт не записывается вконец строки, поэтому нужно добавить дополнительную инструкцию
MOV b y t e [ e d i ] , 0 Ее нужно вставить между LOOP и RET. Обратите внимание на слово byte — оно очень важно, поскольку сообщает компилятору о том, какого размера нуль нужно записать по адресу EDI. Самостоятельно он определить этого не может. Строку завершает нулевой байт, поэтому нуль размером в байт мы и запишем. Наша программа уничтожает исходное содержимое некоторых регистров
(ЕАХ, ЕВХ, ЕСХ, EDX и EDI). Чтобы их сохранить, перед изменением этих регистров нужно поместить их значения в стека затем извлечь их оттуда в правильном порядке. Вызывать нашу подпрограмму нужно так mov eax,0x12345678 ; загрузить в ЕАХ число, подлежащее преобразованию mov e d i , b u f f ; загрузить в EDI адрес буфера для записи строки c a l l c o n v e r t вызов подпрограммы Число, которое мы хотим преобразовать, перед вызовом подпрограммы загружаем в регистр ЕАХ. Второй параметр — это указатель на адрес в памяти, куда будет записана наша строка. Указатель должен быть загружен вили (в зависимости от режима процессора. Команда CALL вызывает нашу подпрограмму. В результате ее выполнения созданная строка будет записана по указанному адресу. Наша функция convert преобразует число только в десятичное представление. Сейчас мы изменим ее так, чтобы получать число в шестнадцатеричной записи. Мы получаем десятичные цифры, прибавляя к остаткам код цифры нуль. Это возможно, потому что символы цифр в таблице расположены последовательно (рис. 1.2). Но если наши остатки получаются отделения на
16, тонам понадобятся цифры А — F, расположенные в таблице тоже последовательно, ноне вслед за цифрой 9. Решение простое если остаток
105
. r e v e r s e : pop eax ; выталкиваем цифру из стека s t o s b ; записываем AL по адресу, содержащемуся в
;EDI, увеличиваем EDI на 1 loop . r e v e r s e ;ЕСХ=ЕСХ-1, переходим, если ЕСХ неравно О r e t Да Возвращаемся Мы слегка улучшили программу, но она все еще работает неправильно. Необходимый нулевой байт не записывается вконец строки, поэтому нужно добавить дополнительную инструкцию
MOV b y t e [ e d i ] , 0 Ее нужно вставить между LOOP и RET. Обратите внимание на слово byte — оно очень важно, поскольку сообщает компилятору о том, какого размера нуль нужно записать по адресу EDI. Самостоятельно он определить этого не может. Строку завершает нулевой байт, поэтому нуль размером в байт мы и запишем. Наша программа уничтожает исходное содержимое некоторых регистров
(ЕАХ, ЕВХ, ЕСХ, EDX и EDI). Чтобы их сохранить, перед изменением этих регистров нужно поместить их значения в стека затем извлечь их оттуда в правильном порядке. Вызывать нашу подпрограмму нужно так mov eax,0x12345678 ; загрузить в ЕАХ число, подлежащее преобразованию mov e d i , b u f f ; загрузить в EDI адрес буфера для записи строки c a l l c o n v e r t вызов подпрограммы Число, которое мы хотим преобразовать, перед вызовом подпрограммы загружаем в регистр ЕАХ. Второй параметр — это указатель на адрес в памяти, куда будет записана наша строка. Указатель должен быть загружен вили (в зависимости от режима процессора. Команда CALL вызывает нашу подпрограмму. В результате ее выполнения созданная строка будет записана по указанному адресу. Наша функция convert преобразует число только в десятичное представление. Сейчас мы изменим ее так, чтобы получать число в шестнадцатеричной записи. Мы получаем десятичные цифры, прибавляя к остаткам код цифры нуль. Это возможно, потому что символы цифр в таблице расположены последовательно (рис. 1.2). Но если наши остатки получаются отделения на
16, тонам понадобятся цифры А — F, расположенные в таблице тоже последовательно, ноне вслед за цифрой 9. Решение простое если остаток
105
Ассемблер на примерах. Базовый курс больше 9, то мы будем прибавлять к нему другую константу. Вместо добавления к остатку кода нуля будем вызывать еще одну подпрограмму,
H e x D i g i t :
0-15,
H e x D i g i t : cmp d l , 1 0 j b . l e s s add d l , ' A ' r e t
. l e s s : o r d l , ' 0 ' r e t в DL ожидается число нужно сопоставить ему шестнадцатеричную цифру сравниваем DL с 10 переходим, если меньше
-10 ;10 превращается в ' А ' , 11 в ' В ' и т . д . конец подпрограммы можно прибавлять итак конец подпрограммы Модифицируя подпрограмму convert, не забудьте заменить делитель 10 на
0x10, то есть 16. А теперь обобщим нашу подпрограмму так, чтобы она преобразовывала число в строку в любой системе счисления. После того, как мы написали универсальную подпрограмму превращения остатка в N-ичную цифру, достаточно будет просто передавать основание системы счисления как еще один параметр. Все, что нужно сделать, — это удалить строки кода, загружающие делитель в регистр ЕВХ. Кроме того, для сохранения значений регистров общего назначения мы добавим команды PUSHAD и POPAD. Окончательную версию подпрограммы, приведенную в листинге 7.3, мы будем использовать в других примерах Листинг 7.3. Программа преобразования числа в строку окончательный вариант)
NumToASCII еах = 32-битное число ebx = основание системы счисления edi = указатель на строку-результат Возвращает заполненный буфер
NumToASCII: pushad xor esi,esi convert_loop: сохраняем все регистры общего назначения в стеке
ESI = 0: это счетчик цифр в стеке
106
H e x D i g i t :
0-15,
H e x D i g i t : cmp d l , 1 0 j b . l e s s add d l , ' A ' r e t
. l e s s : o r d l , ' 0 ' r e t в DL ожидается число нужно сопоставить ему шестнадцатеричную цифру сравниваем DL с 10 переходим, если меньше
-10 ;10 превращается в ' А ' , 11 в ' В ' и т . д . конец подпрограммы можно прибавлять итак конец подпрограммы Модифицируя подпрограмму convert, не забудьте заменить делитель 10 на
0x10, то есть 16. А теперь обобщим нашу подпрограмму так, чтобы она преобразовывала число в строку в любой системе счисления. После того, как мы написали универсальную подпрограмму превращения остатка в N-ичную цифру, достаточно будет просто передавать основание системы счисления как еще один параметр. Все, что нужно сделать, — это удалить строки кода, загружающие делитель в регистр ЕВХ. Кроме того, для сохранения значений регистров общего назначения мы добавим команды PUSHAD и POPAD. Окончательную версию подпрограммы, приведенную в листинге 7.3, мы будем использовать в других примерах Листинг 7.3. Программа преобразования числа в строку окончательный вариант)
NumToASCII еах = 32-битное число ebx = основание системы счисления edi = указатель на строку-результат Возвращает заполненный буфер
NumToASCII: pushad xor esi,esi convert_loop: сохраняем все регистры общего назначения в стеке
ESI = 0: это счетчик цифр в стеке
106
Глава 7. Полезные фрагменты кода xor edx,edx div ebx call HexDigit push edx inc esi test eax,eax jnz convert_loop eld write_loop: pop eax stosb dec esi test esi,esi jnz write_loop mov byte [edi],0 popad ret
;EDX = Оделим ЕАХ на ЕВХ , частное в ЕАХ, остаток в EDX преобразуем в ASCII сохраняем EDX в стеке увеличиваем счетчик цифр в стеке все (ЕАХ = 0) если не 0, продолжаем сбрасываем флаг направления DF: запись вперед выталкиваем цифру из стека записываем их в буфер по адресу ES:(E)DI уменьшаем счетчик оставшихся цифр все (ESI = 0) если не 0, переходим к следующей цифре заканчиваем строку нулевым байтом восстанавливаем исходные значения регистров все ! ! !
7.3. Преобразование строки в число Часто бывает нужно прочитать число, то есть извлечь его из строки. Высо
коуровневые языки программирования предоставляют ряд библиотечных функций для преобразования строки в число (readln, scanf), а мы напишем простенькую функцию такого назначения самостоятельно. Для преобразования одного символа в число мы напишем подпрограмму convert_char, которая преобразует цифры '0'-'9' в числа 0-9, а символы 'A'-'F' и 'а'-Т в числа 10—15 (ОхА-OxF). Единственным входным, он же выходной, параметром будет регистр AL, в который перед вызовом подпрограммы нужно будет загрузить один символ. В результате работы подпрограммы в этом же регистре окажется числовое значение. Давайте рассмотрим эту подпрограмму. convert_char: вычитаем из символа код нуля если разность меньше 10, то была десятичная цифра — больше ничего ненужно делать команда JB — переход, если меньше иначе — была буква add al,'0' ;AL = исходная буква and al,0x5f приводим ее к верхнему регистру sub al,'0' emp al, 10 jb done
107
;EDX = Оделим ЕАХ на ЕВХ , частное в ЕАХ, остаток в EDX преобразуем в ASCII сохраняем EDX в стеке увеличиваем счетчик цифр в стеке все (ЕАХ = 0) если не 0, продолжаем сбрасываем флаг направления DF: запись вперед выталкиваем цифру из стека записываем их в буфер по адресу ES:(E)DI уменьшаем счетчик оставшихся цифр все (ESI = 0) если не 0, переходим к следующей цифре заканчиваем строку нулевым байтом восстанавливаем исходные значения регистров все ! ! !
7.3. Преобразование строки в число Часто бывает нужно прочитать число, то есть извлечь его из строки. Высо
коуровневые языки программирования предоставляют ряд библиотечных функций для преобразования строки в число (readln, scanf), а мы напишем простенькую функцию такого назначения самостоятельно. Для преобразования одного символа в число мы напишем подпрограмму convert_char, которая преобразует цифры '0'-'9' в числа 0-9, а символы 'A'-'F' и 'а'-Т в числа 10—15 (ОхА-OxF). Единственным входным, он же выходной, параметром будет регистр AL, в который перед вызовом подпрограммы нужно будет загрузить один символ. В результате работы подпрограммы в этом же регистре окажется числовое значение. Давайте рассмотрим эту подпрограмму. convert_char: вычитаем из символа код нуля если разность меньше 10, то была десятичная цифра — больше ничего ненужно делать команда JB — переход, если меньше иначе — была буква add al,'0' ;AL = исходная буква and al,0x5f приводим ее к верхнему регистру sub al,'0' emp al, 10 jb done
107
Ассемблер на примерах. Базовый курс sub a l , ' A ' - 1 0 получаем диапазон отвернуть нужно значение 0-15. Если буква больше F, то очищаем 4 старших бита AL d o n e : r e t возвращаемся Наша программа пока не предусматривает никакой проверки корректности входных данных мы просто предполагаем, что байт на входе представляет десятичную цифру либо латинскую букву в верхнем или нижнем регистре. Понятно, что против некорректного вызова она беззащитна. Сначала мы вычитаем из исходного символа код цифры нуль. Если результат попадает в диапазон 0-9, то нужное число уже получено переходим к метке done; если нетто считаем символ буквой. Благодаря хорошо продуманной таблице ASCII (рис. 1.2) код строчной буквы отличается от кода соответствующей ей заглавной только единицей в пятом бите (считая с нулевого, поэтому для перевода буквы в верхний регистр достаточно сбросить этот бит (маска 0x5F). Следующий шаг — вычитанием сдвинуть значение в другой диапазон, получив из 'А' — ОхА, из 'В' — ОхВ и т.д. И, наконец, последняя операция AND ограничивает диапазон возвращаемых значений младшими четырьмя битами — числами от 0x00 до OxOF. Теперь подумаем, как реализовать функцию для преобразования чисел со знаком. Довольно непростая задача, номы ее упростим. Договоримся, что запись отрицательных чисел, и только их, будет начинаться с минуса, и только одного. То есть если вначале строки стоит символ минус, то, значит, перед нами отрицательное число. Преобразование отрицательного числа выполняется точно также, как и положительного, только в самом конце подпрограммы мы выполним операцию
NEG (отрицание. Займемся разработкой самого алгоритма преобразования. Впервой главе мы обсуждали представление любого числа в виде а = a n
*z n
+ a n-1
*z n-1
+ ... + a
1
*z
1
+ a
0
*z° (n — это количество цифр) Например, десятичное число 1234 может быть записано так
1234 = 1*10 3
+ 2*10 2
+ 3*10' + 4*10° Аналогично, шестнадцатеричное значение 0x524D может быть записано как
(524D)
16
= 5*16 3
+ 2*16 2
+ 4*16' + 13*16° = 21 069 На первый взгляд кажется, что сама формула подсказывает алгоритм преобразуем каждую цифру в число по программе convert_char, умножаем на соответствующую степень основания, складываем и получаем в итоге нужное число. Но со второго взгляда становится видно, что степени основания мы не
108
NEG (отрицание. Займемся разработкой самого алгоритма преобразования. Впервой главе мы обсуждали представление любого числа в виде а = a n
*z n
+ a n-1
*z n-1
+ ... + a
1
*z
1
+ a
0
*z° (n — это количество цифр) Например, десятичное число 1234 может быть записано так
1234 = 1*10 3
+ 2*10 2
+ 3*10' + 4*10° Аналогично, шестнадцатеричное значение 0x524D может быть записано как
(524D)
16
= 5*16 3
+ 2*16 2
+ 4*16' + 13*16° = 21 069 На первый взгляд кажется, что сама формула подсказывает алгоритм преобразуем каждую цифру в число по программе convert_char, умножаем на соответствующую степень основания, складываем и получаем в итоге нужное число. Но со второго взгляда становится видно, что степени основания мы не
108
Глава 7. Полезные фрагменты кода узнаем, пока не прочтем все цифры и по их количеству неопределим порядок числа. Неуклюжим решением было бы сначала подсчитать цифры, а потом перейти к очевидному алгоритму, номы поступим изящнее. Это же число 1234 может быть записано так
1234 = ((((1)*10 + 2)*10 + 3)*10) + 4 Это означает, что мы можем умножить первую слева цифру на основание, прибавить вторую цифру, снова умножить на основание и т.д. Благодаря этому постепенному умножению нам ненужно заранее знать порядок преобразуемого числа. Рис. 7.3. Блок-схема алгоритма преобразования строки в число Окончательная версия подпрограммы преобразования строки в число приведена в листинге 7.4.
109
1234 = ((((1)*10 + 2)*10 + 3)*10) + 4 Это означает, что мы можем умножить первую слева цифру на основание, прибавить вторую цифру, снова умножить на основание и т.д. Благодаря этому постепенному умножению нам ненужно заранее знать порядок преобразуемого числа. Рис. 7.3. Блок-схема алгоритма преобразования строки в число Окончательная версия подпрограммы преобразования строки в число приведена в листинге 7.4.
109
Ассемблер на примерах. Базовый курс Листинг 7.4. Программа преобразований строки в число
ASCIIToNum
; esi = указатель на строку, заканчивающуюся символом с
; кодом 0x0
; есх = основание системы счисления
; Возвращает
; еах = число
ASCIIToNum: push esi xor eax,еах xor ebx,ebx cmp byte [esi] jnz .next inc esi
.next: lodsb or al,al j z .done call convert_char imul ebx,ecx add ebx,eax jmp short .next
.done: xchg ebx,eax pop esi cmp byte [esi],'-' jz .negate ret
.negate: neg eax ret сохраняем указатель в стеке
;ЕАХ = 0
;ЕВХ = 0: накопитель для числа число отрицательное если нет, не пропускаем следующий символ пропускаем символ '- ' читаем цифру вконец строки преобразовать в число и сохранить его в AL умножить ЕВХ на ЕСХ, сохранить в ЕВХ сложить и повторить поместить накопленное число в ЕАХ восстановить исходное значение ESI результат должен быть отрицательным да, выполним отрицание нет, положительным — все готово выполняем отрицание все
110
ASCIIToNum
; esi = указатель на строку, заканчивающуюся символом с
; кодом 0x0
; есх = основание системы счисления
; Возвращает
; еах = число
ASCIIToNum: push esi xor eax,еах xor ebx,ebx cmp byte [esi] jnz .next inc esi
.next: lodsb or al,al j z .done call convert_char imul ebx,ecx add ebx,eax jmp short .next
.done: xchg ebx,eax pop esi cmp byte [esi],'-' jz .negate ret
.negate: neg eax ret сохраняем указатель в стеке
;ЕАХ = 0
;ЕВХ = 0: накопитель для числа число отрицательное если нет, не пропускаем следующий символ пропускаем символ '- ' читаем цифру вконец строки преобразовать в число и сохранить его в AL умножить ЕВХ на ЕСХ, сохранить в ЕВХ сложить и повторить поместить накопленное число в ЕАХ восстановить исходное значение ESI результат должен быть отрицательным да, выполним отрицание нет, положительным — все готово выполняем отрицание все
110
Операционная система Эволюция операционных систем Распределение процессорного времени. Процессы Управление памятью Файловые системы Загрузка системы Ассемблер на примерах. Базовый курс
Операционная система предоставляет интерфейс, то есть средства взаимодействия, между прикладными программами и аппаратным обеспечением оборудованием) компьютера. Она управляет системными ресурсами и распределяет эти ресурсы между отдельными процессами.
1 ... 5 6 7 8 9 10 11 12 ... 20
8.1. Эволюция операционных систем История операционных систем началась в х годах. Первые компьютеры требовали постоянного внимания оператора он вручную загружал программы, написанные на перфокартах, и нажимал всевозможные кнопки на пульте управления, управляя вычислительным процессом. Простои такого компьютера обходились очень дорого, поэтому первые операционные системы были разработаны для автоматического запуска следующей задачи после окончания текущей. Часть операционной системы — так называемый монитор — управлял последовательным выполнением задачи позволял запускать их в автоматизированном пакетном режиме. Следующим этапом в развитии операционных систем стало создание специального компонента операционной системы — ее ядра (е годы. Причина появления ядра была довольно простой. Периферийное оборудование компьютеров становилось все более разнообразными задача управления отдельными устройствами все более усложнялась. Ядро операционной системы предоставило стандартный интерфейс для управления периферийными устройствами. При загрузке операционной системы ядро загружается в память, чтобы впоследствии программы могли обращаться к периферийным устройствам через него. Постепенно в ядро добавлялись новые функции. С исторической точки зрения особенно интересна служба учета, позволившая владельцу компьютера выставлять счет пользователю в соответствии с затраченным на его задание машинным временем. Разработчики операционных систем старались более эффективно использовать ресурсы компьютера. Из очевидного соображения о том, что нерационально выполнять на компьютере только одно задание, если другое, использующее другие периферийные устройства, могло бы выполняться одновременно с ним, в 1964 году родилась концепция мультипрограммирования.
112
Глава 8. Операционная система Мультипрограммирование поставило операционные системы перед новым вызовом как распределить ресурсы компьютера, то есть процессор, память, периферийные устройства, между отдельными программами В этой главе мы рассмотрим ответы на этот вопрос.
8.2. Распределение процессорного времени. Процессы Одной из важнейших задач, решаемых операционной системой, является распределение процессорного времени между отдельными программами. Процессы Попросту говоря, процесс — это загруженная в оперативную память программа, которой выделено процессорное время. Каждый процесс, подобно человеку, от кого-то родился, нов отличие от человека у процесса родитель один — это запустивший его процесс. Родительский процесс еще называют предком. Всех потомков или детей процесса (то есть те процессы, которые он породил называют дочерними процессами. Родословное дерево называется иерархией процессов. РИС 8.1. Иерархия процессов Загрузившись, ядро операционной системы запускает первый процесс. В операционных системах UNIX (Linux) он называется init. Этот процесс станет прародителем всех остальных процессов, протекающих в системе. В операционной системе DOS прародителем является командный интерпретатор
COMMAND.COM. Дочерний процесс может унаследовать некоторые свойства или данные от своего родительского процесса. Процесс можно убить (kill), то есть завершить. Если убит родительский процесс, то его дочерние процессы либо
«усыновляются» другим процессом (обычно init), либо тоже завершаются.
113
8.2. Распределение процессорного времени. Процессы Одной из важнейших задач, решаемых операционной системой, является распределение процессорного времени между отдельными программами. Процессы Попросту говоря, процесс — это загруженная в оперативную память программа, которой выделено процессорное время. Каждый процесс, подобно человеку, от кого-то родился, нов отличие от человека у процесса родитель один — это запустивший его процесс. Родительский процесс еще называют предком. Всех потомков или детей процесса (то есть те процессы, которые он породил называют дочерними процессами. Родословное дерево называется иерархией процессов. РИС 8.1. Иерархия процессов Загрузившись, ядро операционной системы запускает первый процесс. В операционных системах UNIX (Linux) он называется init. Этот процесс станет прародителем всех остальных процессов, протекающих в системе. В операционной системе DOS прародителем является командный интерпретатор
COMMAND.COM. Дочерний процесс может унаследовать некоторые свойства или данные от своего родительского процесса. Процесс можно убить (kill), то есть завершить. Если убит родительский процесс, то его дочерние процессы либо
«усыновляются» другим процессом (обычно init), либо тоже завершаются.
113
Ассемблер на примерах. Базовый курс Планирование процессов На самом деле процессы выполняются не параллельно, как если бы они были запущены на независимых компьютерах. Если у компьютера только один процессор, то процессы будут выполняться в так называемом псевдопараллельном режиме. Это означает, что каждый процесс разбивается на множество этапов. И этапы разных процессов по очереди получают кванты процессорного времени — промежутки времени, в течение которого они монопольно занимают процессор. Список всех запущенных процессов, называемый очередью заданий, ведет ядро операционной системы. Специальная часть ядра — планировщик заданий — выбирает задание из очереди, руководствуясь каким-то критерием, и пробуждает его, то есть выделяет ему квант времени и передает процесс диспетчеру процессов Диспетчер процессов — это другая часть ядра, которая следит за процессом вовремя его выполнения. Диспетчер должен восстановить контекст процесса. Восстановление контекста процесса напоминает обработку прерывания, когда все регистры должны быть сохранены, а потом, после окончания обработки, восстановлены, чтобы прерванная программа смогла продолжить работу. После восстановления контекста ядро передает управление самому процессу. Затем, по прошествии кванта времени, выполняемый процесс прерывается и управление передается обратно ядру. Планировщик заданий выбирает следующий процесс, диспетчер процессов его запускает, и цикл повторяется. Состояния процессов Ядро операционной системы внимательно наблюдает за каждым процессом. Вся необходимая информация о процессе хранится в структуре, которая называется блоком управления процессом (РСВ, process control block). В операционной системе UNIX процесс может находиться водном из пяти различных состояний
• Рождение — пассивное состояние, когда самого процесса еще нет, но уже готова структура для появления процесса.
• Готовность — пассивное состояние процесс готов к выполнению, но процессорного времени ему пока не выделено.
• Выполнение — активное состояние, вовремя которого процесс обладает всеми необходимыми ему ресурсами. В этом состоянии процесс непосредственно выполняется процессором.
• Ожидание — пассивное состояние, вовремя которого процесс заблокирован, потому что не может быть выполнен он ожидает какого-то события, например, ввода данных или освобождения нужного ему устройства.
• Смерть процесса — самого процесса уже нет, но может случиться, что его место, то есть структура, осталось в списке процессов (процессы-зомби).
114
• Рождение — пассивное состояние, когда самого процесса еще нет, но уже готова структура для появления процесса.
• Готовность — пассивное состояние процесс готов к выполнению, но процессорного времени ему пока не выделено.
• Выполнение — активное состояние, вовремя которого процесс обладает всеми необходимыми ему ресурсами. В этом состоянии процесс непосредственно выполняется процессором.
• Ожидание — пассивное состояние, вовремя которого процесс заблокирован, потому что не может быть выполнен он ожидает какого-то события, например, ввода данных или освобождения нужного ему устройства.
• Смерть процесса — самого процесса уже нет, но может случиться, что его место, то есть структура, осталось в списке процессов (процессы-зомби).
114
Глава 8. Операционная система Рис 8.2. Жизненный цикл процесса Процессы в DOS состояний не имеют, поскольку DOS — это однозадачная операционная система процессор предоставлен в монопольное распоряжение только одного процесса. Ядро операционной системы хранит следующую информацию о процессе
• Размещение в памяти.
• Ресурсы процесса (открытые файлы, устройства и т.п.).
• Контекст процесса.
• Состояние процесса.
• Имя процесса.
• Идентификационный номер процесса (PID, Process ID).
• Идентификационный номер родительского процесса.
• Права доступа.
• Текущий рабочий каталог.
• Приоритет. Стратегия планирования процессов Пока мы не сказали ничего о том, как именно ядро (точнее, планировщик заданий) решает, какой процесс нужно запустить следующим. Простейшая стратегия планирования процессов называется кольцевой (Round
Robin). Все процессы ставятся в очередь. Планировщик выбирает первый процесс и передает его диспетчеру. Через определенное время ядро прерывает первый процесс, перемещает его вконец очереди и выбирает следующий процесс. Другая стратегия распределения процессорного времени основана на приоритетах Каждому процессу в очереди назначен некоторый приоритет, ив соответствии с ним планировщик распределяет процессорное время — статически или динамически. Статическое назначение приоритетов означает,
115
• Размещение в памяти.
• Ресурсы процесса (открытые файлы, устройства и т.п.).
• Контекст процесса.
• Состояние процесса.
• Имя процесса.
• Идентификационный номер процесса (PID, Process ID).
• Идентификационный номер родительского процесса.
• Права доступа.
• Текущий рабочий каталог.
• Приоритет. Стратегия планирования процессов Пока мы не сказали ничего о том, как именно ядро (точнее, планировщик заданий) решает, какой процесс нужно запустить следующим. Простейшая стратегия планирования процессов называется кольцевой (Round
Robin). Все процессы ставятся в очередь. Планировщик выбирает первый процесс и передает его диспетчеру. Через определенное время ядро прерывает первый процесс, перемещает его вконец очереди и выбирает следующий процесс. Другая стратегия распределения процессорного времени основана на приоритетах Каждому процессу в очереди назначен некоторый приоритет, ив соответствии с ним планировщик распределяет процессорное время — статически или динамически. Статическое назначение приоритетов означает,
115
Ассемблер на примерах. Базовый курс Рис. 8.3. Кольцевая стратегия Round Robin что сначала будут выполняться процессы с высоким приоритетом, причем приоритет процесса не может быть изменен на протяжении всего времени выполнения процесса. При динамическом же назначении приоритет может быть изменен вовремя выполнения.
8.3. Управление памятью Операционная система отслеживает свободную оперативную память, реализует стратегию выделения оперативной памяти и освобождения ее для последующего использования. Оперативная память делится между ядром и всеми выполняемыми процессами. Простое распределение памяти Стратегия простого распределения памяти была самой первой. Она состоит в том, что доступная память делится на блоки фиксированного размера, одни из которых доступны только ядру, а другие — остальным процессам. Для разделения памяти на блоки используются различные аппаратные и программные методы. Рис. 8.4. Фиксированное распределение памяти между процессами
116
8.3. Управление памятью Операционная система отслеживает свободную оперативную память, реализует стратегию выделения оперативной памяти и освобождения ее для последующего использования. Оперативная память делится между ядром и всеми выполняемыми процессами. Простое распределение памяти Стратегия простого распределения памяти была самой первой. Она состоит в том, что доступная память делится на блоки фиксированного размера, одни из которых доступны только ядру, а другие — остальным процессам. Для разделения памяти на блоки используются различные аппаратные и программные методы. Рис. 8.4. Фиксированное распределение памяти между процессами
116
Глава 8. Операционная система Каждый блок памяти может быть занят только одним процессом и будет занят, даже если процесс еще не начал выполняться. Ядро обеспечивает защиту блоков памяти, занятых различными процессами. Позже эта схема была усовершенствована введением блоков переменной длины. При запуске процесс сообщает, сколько памяти ему нужно для размещения. Это была уже технология переменного распределения памяти. Обе технологии распределения памяти сейчас считаются устаревшими и практически не используются, потому что они не могут удовлетворить запросы современных задач. Случается, например, что размер процесса превышает весь имеющийся объем оперативной памяти. При традиционной стратегии распределения памяти операционная система просто не сможет запустить такой процесс. Вот поэтому была разработана новая стратегия распределения памяти — стратегия свопинга, которая позволяет использовать пространство жесткого диска для хранения не помещающейся в оперативную память информации.
Свопинг (swapping) — организация подкачки Пока процессы и размеры обрабатываемых данных были небольшими, всех вполне устраивало фиксированное распределение памяти. С появлением многозадачных операционных систем запущенные процессы не всегда умещались в оперативную память. Поэтому было решено выгружать неиспользуемые в данный момент данные (и процессы) на жесткий диск. В оперативной памяти остается только структура процесса, а все остальное выгружается. Эта процедура называется свопингом или подкачкой. Когда системе нужны данные, находящиеся в файле (разделе) подкачки, система подгружает их в оперативную память, выгрузив предварительно в файл подкачки другие данные. Виртуальная память и страничный обмен Закон Мерфи гласит программа растет до тех пор, пока не заполнит всю доступную память. Решением проблемы стало притвориться, что в распоряжении операционной системы больше оперативной памяти, чем на самом деле. Этот механизм называется виртуальной памятью и чаще всего реализуется через систему страничного обмена. Каждому процессу выделено виртуальное адресное пространство, то есть диапазон адресов, ограниченный только архитектурой процессора, а не объемом физически установленной оперативной памяти. Виртуальное адресное пространство делится на страницы одинакового размера, обычно 4 Кб. Физическая оперативная память делится на так называемые фреймы, размер каждого фрейма равен размеру страницы.
117
Свопинг (swapping) — организация подкачки Пока процессы и размеры обрабатываемых данных были небольшими, всех вполне устраивало фиксированное распределение памяти. С появлением многозадачных операционных систем запущенные процессы не всегда умещались в оперативную память. Поэтому было решено выгружать неиспользуемые в данный момент данные (и процессы) на жесткий диск. В оперативной памяти остается только структура процесса, а все остальное выгружается. Эта процедура называется свопингом или подкачкой. Когда системе нужны данные, находящиеся в файле (разделе) подкачки, система подгружает их в оперативную память, выгрузив предварительно в файл подкачки другие данные. Виртуальная память и страничный обмен Закон Мерфи гласит программа растет до тех пор, пока не заполнит всю доступную память. Решением проблемы стало притвориться, что в распоряжении операционной системы больше оперативной памяти, чем на самом деле. Этот механизм называется виртуальной памятью и чаще всего реализуется через систему страничного обмена. Каждому процессу выделено виртуальное адресное пространство, то есть диапазон адресов, ограниченный только архитектурой процессора, а не объемом физически установленной оперативной памяти. Виртуальное адресное пространство делится на страницы одинакового размера, обычно 4 Кб. Физическая оперативная память делится на так называемые фреймы, размер каждого фрейма равен размеру страницы.
117
Ассемблер на примерах. Базовый курс Рис. 8.5. Преобразование виртуального адреса в физический Перед каждым обращением к памяти процессор запрашивает так называемый блок управления памятью (MMU, Memory Management Unit), чтобы он преобразовал виртуальный адрес в физический. Трансляция виртуального адреса в физический производится с использованием таблицы страниц, каждая строка которой описывает одну виртуальную страницу. Рис. 8.6. Запись таблицы страниц Наиболее важный бит — бит присутствия страницы. Если он установлен, то эта страница сохранена во фрейме, номер которого (адрес) определен в физическом адресе. Другие биты в таблице определяют права доступа или разрешения для страницы (read/write/execute) или используются для буфера. Таблица страниц индексирована номером виртуальной страницы. Рассмотрим пример трансляции (преобразования) виртуального адреса в физический (рис. 8.7).
118
118
Глава 8. Операционная система Рис. 8.7. Принцип преобразования виртуального адреса в физический По виртуальному адресу MMU вычисляется номер виртуальной страницы, на которой он находится. Разность между адресом начала страницы и требуемым адресом называется смещением. По номеру страницы ищется строка в таблице страниц. Бит присутствия в этой строке указывает, находится ли виртуальная страница в физической памяти компьютера (бит Р установлен вили в файле (разделе) подкачки на диске. Если страница присутствует в физической памяти, ток физическому адресу номеру фрейма) добавляется смещение, и нужный физический адрес готов. Если страница выгружена из оперативной памяти (бит Р установлен в 0), то процессор (или его MMU) вызывает прерывание Страница не найдена (Page
Not Found), которое должно быть обработано операционной системой. В ответ на прерывание Страница не найдена операционная система должна загрузить требуемую страницу в физическую оперативную память. Если заняты не все фреймы, то ОС загружает страницу из файла подкачки в свободный фрейм, исправляет таблицу страниц и заканчивает обработку прерывания. Если свободного фрейма нетто операционная система должна решить, какую страницу переместить из фрейма в область подкачки, чтобы освободить физи-
119
Not Found), которое должно быть обработано операционной системой. В ответ на прерывание Страница не найдена операционная система должна загрузить требуемую страницу в физическую оперативную память. Если заняты не все фреймы, то ОС загружает страницу из файла подкачки в свободный фрейм, исправляет таблицу страниц и заканчивает обработку прерывания. Если свободного фрейма нетто операционная система должна решить, какую страницу переместить из фрейма в область подкачки, чтобы освободить физи-
119
Ассемблер на примерах. Базовый курс ческую память. В освобожденный фрейм будет загружена страница, которая первоначально вызвала прерывание. После этого ОС исправит биты присутствия в записях об обеих страницах и закончит обработку прерывания. Для процесса процедура преобразования виртуальных адресов в физические абсолютно прозрачна (незаметна) — он думает, что у вашего компьютера просто много оперативной памяти. В х86-совместимых компьютерах каждая программа может иметь несколько адресных пространств по 4 ГБ. В защищенном режиме значения, загруженные в сегментные регистры, используются как индексы таблицы дескрипторов, описывающей свойства отдельных сегментов, или областей, памяти. Рассмотрение сегментов выходит далеко за пределы этой книги.
8.4. Файловые системы Файловая система — это часть операционной системы, предназначенная для управления данными, записанными на постоянных носителях. Она включает в себя механизмы записи, хранения и чтения данных. Когда компьютеры только начали развиваться, программист должен был знать точную позицию данных на носителе. Почему мы используем слово носитель, а не диск В то время дисков как таковых или еще не было, или они были слишком дороги, поэтому для хранения данных использовалась магнитная лента. С появлением дисков все еще больше усложнилось, поэтому для облегчения жизни программистами пользователями была разработана файловая система, которая стала частью ядра. Структура файловой системы Кроме программного модуля в ядре в понятие файловой системы входит структура данных, описывающая физическое местоположение файлов на диске, права доступа к ними, конечно же, их имена. Сам файл состоит из записей, которые могут быть фиксированной или переменной длины. Метод доступа к записям файла зависит от его типа. Самый простой — метод последовательного доступа, при котором для чтения нужной записи требуется сначала прочитать все предшествующие ей записи. Если записи проиндексированы, ток ним можно обращаться также по индексу. Такой тип файла называется индексно-последовательным (IBM 390, AS/400). Что же касается операционной системы, то она не устанавливает формат файла. Файл состоит из последовательности байтов, а структура самого файла должна быть понятна приложению, которое используется для его обработки. Поговорим о методах доступа к файлу в операционных системах DOS и
UNIX.
120
8.4. Файловые системы Файловая система — это часть операционной системы, предназначенная для управления данными, записанными на постоянных носителях. Она включает в себя механизмы записи, хранения и чтения данных. Когда компьютеры только начали развиваться, программист должен был знать точную позицию данных на носителе. Почему мы используем слово носитель, а не диск В то время дисков как таковых или еще не было, или они были слишком дороги, поэтому для хранения данных использовалась магнитная лента. С появлением дисков все еще больше усложнилось, поэтому для облегчения жизни программистами пользователями была разработана файловая система, которая стала частью ядра. Структура файловой системы Кроме программного модуля в ядре в понятие файловой системы входит структура данных, описывающая физическое местоположение файлов на диске, права доступа к ними, конечно же, их имена. Сам файл состоит из записей, которые могут быть фиксированной или переменной длины. Метод доступа к записям файла зависит от его типа. Самый простой — метод последовательного доступа, при котором для чтения нужной записи требуется сначала прочитать все предшествующие ей записи. Если записи проиндексированы, ток ним можно обращаться также по индексу. Такой тип файла называется индексно-последовательным (IBM 390, AS/400). Что же касается операционной системы, то она не устанавливает формат файла. Файл состоит из последовательности байтов, а структура самого файла должна быть понятна приложению, которое используется для его обработки. Поговорим о методах доступа к файлу в операционных системах DOS и
UNIX.
120
Глава 8. Операционная система Доступ к файлу Перед началом обработки любого файла его нужно открыть. Это делает операционная система по запросу программы. В запросе нужно указать имя файла и требуемый метод доступа (чтение или запись. Ядро возвратит номер файла, который называется файловым дескриптором. В дальнейшем этот номер будет использоваться для операций с файлом. Имя файла обычно содержит путь к этому файлу, то есть перечисление всех каталогов, необходимых для того, чтобы система смогла найти файл. Имена подкаталогов в составе пути разделяются специальным символом, который зависит от операционной системы. В DOS это \, а в UNIX — /. Позиция в файле определяется числом — указателем. Его значение — это количество прочитанных или записанных байтов. С помощью специального системного вызова можно переместить указатель на заданную позицию в файле и начать процесс чтения или записи с нужного места. Максимальный размер блока данных, который может быть прочитан или записан зараз, ограничен буфером записи. Завершив работу с файлом, его нужно закрыть. В мире UNIX укоренилась идея представлять устройства ввода/вывода в виде обычных файлов чтение символов с клавиатуры и вывод их на экран выполняются теми же самыми системными вызовами, что и чтение/запись их в файл. Дескриптор файла клавиатуры — 0, он называется стандартным потоком ввода (stdin). У экрана два дескриптора — стандартный поток вывода (stdout) и стандартный поток ошибок (stderr). Номер первого идентификатора — 1, второго — 2. Операционная система может не только читать и записывать файлы, но также и перенаправлять потоки ввода/вывода. Например, можно перенаправить вывод программы в файл вместо вывода на экран, не меняя исходного текста программы и не перекомпилируя ее. Перенаправление известно как в DOS, таки в UNIX, и выполняется с помощью специального системного вызова, который обычно принимает два аргумента — файловые дескрипторы соединяемых файлов. В DOS и UNIX можно перенаправить вывод программы в файл так
Is > f i l e l Фактически интерпретатор команд (или оболочка) открывает файл filel перед запуском Is, получает его дескриптор и, используя системный вызов, заменяет дескриптор стандартного вывода полученным дескриптором. Программа Is и понятия не имеет, куда записываются данные — она просто записывает их на стандартный вывод. Благодаря системному вызову вывод попадает в указанный файла не на экран.
121
Is > f i l e l Фактически интерпретатор команд (или оболочка) открывает файл filel перед запуском Is, получает его дескриптор и, используя системный вызов, заменяет дескриптор стандартного вывода полученным дескриптором. Программа Is и понятия не имеет, куда записываются данные — она просто записывает их на стандартный вывод. Благодаря системному вызову вывод попадает в указанный файла не на экран.
121
Ассемблер на примерах. Базовый курс Физическая структура диска Файловая система обращается к диску непосредственно (напрямую, ^ поэтому она должна знать его физическую структуру (геометрию. Магнитный диск состоит из нескольких пластин, обслуживаемых читающими/пишущими головками (рис. 8.8). Пластины разделены на дорожки, а дорожки — насек тора. Дорожки, расположенные друг над другом, образуют цилиндр. Исторически сложилось так, что точное место на диске определяется указанием трех координат цилиндра, головки и сектора. Рис 8.8. Физическая структура диска Дорожки — это концентрические круги, разделенные на отдельные сектора. Подобно странице памяти, сектор диска — это наименьший блок информации, размер его обычно равен 512 байтам. Головки чтения/записи плавают над поверхностью диска. Значения трех вышеупомянутых координат позволяют головкам определить нужный сектор диска для чтения или записи данных. Для цилиндров, головок и секторов были определены диапазоны приемлемых значений. Поскольку объемы дисков росли, увеличивалось и количество цилиндров, головок и секторов. Увеличивалось оно до тех пор, пока не вышло за пределы этого самого диапазона. Вот поэтому некоторые старые компьютеры не могут прочитать диски размером 60 Гб и более (а некоторые итого меньше. Вместо увеличения этих диапазонов были разработаны различные преобразования, которые позволяют вернуть комбинации сектора, головки и цилиндра назад в указанный диапазон. Было решено заменить адресацию геометрического типа логической (или линейной) адресацией, где отдельные секторы были просто пронумерованы от 0 до последнего доступного сектора. Теперь для того, чтобы обратиться к сектору, нужно просто указать его номер.
122
122
Глава 8. Операционная система Логические диски Необязательно, чтобы файловая система занимала весь диск. Обычно диск разбивают на логические диски, или разделы. Так даже безопаснее например, на одном логическом диске у вас находится операционная система, на другом — прикладные программы, на третьем — ваши данные. Если какая-то программа повредила один раздел, остальные два останутся неповрежденными. Первый сектор любого диска отведен под таблицу разделов (partition table). Каждая запись этой таблицы содержит адреса начального и конечного секторов одного разделав геометрической (три координаты) и логической последовательный номер) форме. А на каждом разделе хранится таблица файлов, позволяющая определить координаты файла на диске.
1 ... 6 7 8 9 10 11 12 13 ... 20