Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Textnew2

.pdf
Скачиваний:
13
Добавлен:
06.02.2018
Размер:
1.48 Mб
Скачать

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

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

3.2. Средства организации циклов

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

MOV параметр, число_повторений_цикла метка_тела_цикла:

команды тела цикла SUB параметр, 1

JG метка_тела_цикла

или по схеме

MOV параметр, 0 метка_тела_цикла:

команды тела цикла ADD параметр, 1

CMP параметр, число_повторений_цикла JNE метка_тела_цикла

В связи с очень частым использованием элементарных действий увеличения на единицу и уменьшения на единицу, для этих операций введены отдельные команды. Они имеют мнемонику INC и DEC (от английских слов increment - увеличить и decrement - уменьшить). Обе эти команды имеют единственный операнд, задающий регистр или место в памяти. При использовании таких команд последние схемы перепишутся в виде

MOV параметр, число_повторений_цикла

41

метка_тела_цикла: команды тела цикла DEC параметр

JG метка_тела_цикла

или по схеме

MOV параметр, 0 метка_тела_цикла:

команды тела цикла INC параметр

CMP параметр, число_повторений_цикла JNE метка_тела_цикла

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

Простейшая из этих команд в мнемонике ассемблера изображаются с помощью служебного слова LOOP и имеет общий вид

LOOP метка_тела_цикла

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

Поэтому использование команды LOOP в общем случае имеет вид MOV параметр, число_повторений_цикла

метка_тела_цикла: команды тела цикла

LOOP метка_тела_цикла

Для сознательного применения этой команды нужно четко представлять, что в ходе выполнения команды LOOP вначале значение регистра для параметра цикла (обычно регистра ecx) уменьшается на единицу, а только затем осуществляется проверка полученного значения на нуль. Следствием такого хода внутренних действий является несколько неожиданное поведение цикла при нулевом начальном параметре. Цикл при этом не будет повторяться "нуль раз", как мог бы подумать поверхностный учащийся - поскольку команда LOOP организует цикл с проверкой после тела цикла, то получается цикл с постусловием, который должен выполняться не менее одного раза. При начальном значении параметра цикла, равного нулю, цикл будет выполняться максимально возможное число раз! (Это число равно 232 » 4 млр. ) Казалось бы нормально думающий программист и не задаст с помощью команды MOV нулевое число повторений цикла. Но, в действительности, начальное значение для параметра цикла может определяться не константой, а событиями в

42

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

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

JECXZ метка_перехода

и

JCXZ метка_перехода

где в качестве метки перехода метки_перехода предполагается использовать метку сразу за концом цикла, использующего команду LOOP.

Заметим, что команда LOOP предполагает ее использование в двух основных модификациях: с регистром ecx и с регистром cx. Эти различия на ассемблере NASM могут задаваться дополнительным операндом в одном из вариантов

LOOP метка_тела_цикла, ecx LOOP метка_тела_цикла, cx

явно подчеркивающим использование того или иного регистра. На ассемблерах MASM и TASM выбор варианта использования регистра ecx задается расширенным мнемокодом в виде LOOPD, а применение обычного обозначение LOOP влечет использование регистра cx в качестве места хранения параметра цикла.

На использование команды LOOP накладывается то же ограничение, что и на простейшие команды Jcc, а именно с помощью их нельзя перейти достаточно далеко от места самой команды (не далее чем на 127 байтов вперед или -128 назад).

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

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

Для простейшего применения встроенного стека достаточно знать две команды для манипуляций со стеком. Они называются PUSH и POP, обе из них имеют единственный операнд. В зависимости от используемого варианта (32-битного или 16-битного), эти команды вставляют в стек или выдают из него 32-битные двоичные данные или 16-битные. Манипуляции для 8-битных данных со стеком в

43

рассматриваемой архитектуре не предусмотрены (просто нет таких команд в процессорах семейства Intel и совместимых с ним).

В качестве операнда команды PUSH может быть задан регистр (но только не 8- битный), а также места в памяти, где помещаются запоминаемое слово. Допускается также использование числовой константы в качестве такого операнда. В последних двух случаях для компилятора с ассемблера возникает неопределенность - какой вариант команды PUSH использовать: для 32-битных или для 16-битных данных. Здесь также вынуждено используются модификаторы размера операндов, рассмотренные в разделе 2.6.1.

Построение вложенных циклов на изложенной основе демонстрирует следующая схема

MOV ecx, число_повторений_внешнего_цикла метка_начала_внешнего_цикла:

PUSH ecx

MOV параметр, число_повторений_внутреннего_цикла метка_начла_внутреннего_цикла:

команды тела цикла

LOOP метка_начала_внутреннего_цикла

POP ecx

LOOP метка_начала_внешнего_цикла

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

Кроме простейшего варианта команды цикла имеется два ее несколько более сложных варианта. Они используют в качестве мнемокода обозначения LOOPE и LOOPNE, которым также равносильны обозначения LOOPZ и LOOPNZ. Отличием этих вариантов от простой команды LOOP заключается в том, что переход по метке, заданной в этих командах, производится не просто по условию отличия от нуля текущего значения регистра параметра (регистра ecx или регистра cx), а только в том случае, когда текущее условие, зафиксированное в регистре EFLAGS, отвечает дополнительному условию "равно" или "не равно" (или, что равносильно, результат "равен нулю" или "не равен нулю"). Таким образом, условие перехода по метке, можно записать в виде

(ecx ? 0) И (флаг ZF установлен) для команд LOOPE и LOOPZ,

(ecx ? 0) И (флаг ZF не установлен) для команд LOOPNE и LOOPNZ Применение этих вариантов команды цикла будут рассмотрено позже при изуче-

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

3.3. Особенности команд умножения и деления

44

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

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

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

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

Было принято следующее правило: результат умножения 8-битных чисел размешается в регистре AX, результат умножения 16-битных чисел размещается в паре регистров AX и DX, причем в DX размещаются старшие разряды произведения, а в регистре AX - его младшие разряды. При умножении 32-битных чисел старшие 32 бита произведения помещается в регистр EDX, а младшие 32 бита этого произведения помещаются в регистра EAX.

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

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

45

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

Следствием перечисленных соглашений стало, в частности, неявная интерпретация следующих вариантов записи команд умножения, которое для беззнаковых операндов задается мнемоникой MUL. Команда, записанная как MUL DH, выполняет умножение содержимого регистра AL на содержимое регистра DH, размещая результат в регистре AX. Команда MUL DI выполняет умножение содержимого регистра AX на содержимое регистра DI размещая результат в паре регистров DX, AX. Команда MUL DWORD [fooo] выполняет умножение содержимого регистра EAX на значение числа, записанного в области из четырех байтах, начало которой обозначено именем fooo.

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

Принятые соглашения по командам умножения повлекли закономерные последствия для команд деления. Эти последствия связаны с тем общеизвестным фактом, что деление есть операция обратная к умножению, поэтому даже в машинной архитектуре желательно, чтобы после умножения можно было просто и естественно выполнить обратное действие без вспомогательных команд. Поэтому было принято, что исходное значение делимого для деления на 8-битное число должно размещаться в регистре AX, исходное значение делимого для 16-битного деления - в паре регистров DX, AX, а исходное значение делимого для 32-битного деления в паре регистров EDX, EAX (именно там, где они оказались при соответствующем умножении). Сама команда деления задается при этом мнемокодом DIV, имеет один операнд, который задает делимое, а результат деления опять же размещается согласно принятым соглашениям. Эти дополнительные соглашения, основанные на исходных соглашениях для деления, следующие. Частное при 8-битном делении помещается аппаратурой в регистр AL, а остаток при этом делении - в регистр AH. При 16-битном делении частное помещается в регистр AX, а остаток от деления - в регистр DX. При 32-битном делении для размещения результата используются, соответственно, регистры EAX и EDX. Для деления чисел с учетом знаков предназначена команда с мнемокодом IDIV, которая по использованию операндов полностью совпадает с рассмотренной командой DIV.

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

Применим рассмотренную информацию к решению следующей практической задачи. Операционные системы в составе своего программного интерфейса не со-

46

держат средств ввода и вывода данных числовых форматов. Их программный интерфейс позволяет вводить и выводить только отдельные символы и тексты из таких символов, а результаты расчетов образуются программами в двоичных кодах, которые совсем не похожи на внешние коды отдельных цифр. Ввод трех цифр числа 375 должен приводить к построению двоичного кода 0000000101110111 для представления этого числа в двоичной системе счисления, а сами вводимые цифры имеют двоичные коды 00110011, 00110111, 00110101. Из этого примера видно, что преобразование между внешним представлением числа последовательность кодов цифр и представлением в двоичной системе счисления оказывается далеко не тривиальным. С другой стороны, такие преобразования оказываются необходимыми из-за отсутствия для них средств в операционной системе.(Отсутствие же последних в ОС обусловлено многообразием возможных форматов для множества типов данных, которые используются различных языках программирования.) Такие средства входят в стандартные библиотеки языков программирования высокого уровня, в частности, в стандартную библиотеку языка Си. При разработке программ на ассемблере разработчик оказывается перед проблемой построения подпрограмм ввода и вывода чисел. Этой проблемой в частном варианте мы и займемся.

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

Напомним, что получение значений цифр для другой системы счисления достигается последовательно путем деления исходного числа и промежуточных частных на основание целевой системы счисления. На каждом шаге такого процесса вычисления получаемый остаток и дает очередную цифру результата. Существенной особенностью этого процесса является то, что вначале получаются младшие цифры результата, а только затем старшие. Иначе говоря, цифры результата получаются в очередности, обратной их использованию в записи числа цифрами слева направо. Например, при последовательном делении на основание 10 числа 375 и промежуточных частных, вначале образуется значение цифры 5, затем 7 и только потом старшей цифры 3. Такая особенность заставляет в приводимой далее программе запоминать промежуточные цифры результата, чтобы на завершающем этапе извлекать их из места хранения в обратной последовательности. Для такого запоминая с обратным порядком извлечения удобно использовать стек. Предлагаемая программа приведена на рис. 3.3.1.

GLOBAL _start SEGMENT .text

_start: mov eax, 0ffffffffh

mov esi,10 ; base of position digit system mov ecx, 0 ; reset digit counter

pov: mov edx, 0 ; null into left part of devident

47

div esi

; divide for next digit = rest

add dl, '0'

 

push edx

 

inc ecx

; step into counter

cmp eax, 0

 

jne pov

 

mov [cnt], ecx izv: pop edx

mov [digit],dl ; digit into digit for write

mov eax,4 ; N function=write mov ebx,1 ; N handle=1 (stdout) mov ecx, digit; address of digit mov edx, 1 ; number of byte

int 80h

dec dword [cnt] cmp dword [cnt],0 jne izv

mov eax, 1 ; N function = exit int 80h ;

SEGMENT .data digit db 0

cntdd 0

Рис. 3.3.1. Вывод десятичного значения содержимого EAX в Linux

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

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

48

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

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

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

По вычислению всех цифр результата значение регистра ECX, в котором получено число таких цифр, запоминается в именованной области cnt с помощью команды MOV [cnt], ecx. Затем из стека извлекается очередная, запомненная ранее цифра (командой POP EDX). Ее значение из младших битов регистра EDX перемещается в вспомогательное однобайтовое поле digit и выполняется системная функция write для вывода этой одной цифры из служебного поля (системная функция write не позволяет задать вывод содержимого непосредственно из регистра).

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

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

У начинающего программиста может возникнут вопрос, нельзя ли было вместо деления 64-битного кода из пары регистров EDX, EAX использовать деление исходного 32-битного числа путем деления на 16-битный делитель, помещенного,

49

например, в регистре DX ? Конечно при этом пришлось бы переместить исходное число из регистра EAX в пару регистров DX, AX. Казалось бы, так можно было несколько упростить программу. Тем ни менее, последнее решения привело бы к неправильным результатам. Дело в том, что после первого 16-битного деления промежуточное частное (значение, полученное из исходного числа отбрасыванием младшей десятичной цифры) может не поместиться в регистре AX и аппаратурой будет обнаруживаться ошибка деления.

3.4. Организация процедур

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

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

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

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

50

Соседние файлы в предмете Операционные системы