Шрифт:
Рис. 10.4 показывает, как работает стек. Стек располагается в обычном ОЗУ, а указатель стека (регистр SP) ЦП обеспечивает возможность доступа к той ячейке памяти, которая является «вершиной» в данный момент времени. Для МП 8086 стек состоит из 16-разрядных слов и по мере занесения в него данных «растет» вниз в ОЗУ. Содержимое регистра SP автоматически декрементируется на 2 перед каждой операцией PUSH и инкрементируется на 2 после каждой операции POP. Таким образом, например, 16-разрядное содержимое регистра АХ копируется в вершину стека командой PUSH АХ; SP указывает на последний занесенный байт. Команда POP выполняется в обратном порядке, как показано на рис. 10.4.
Рис. 10.4. Операции со стеком.
Мы увидим, что при реализации вызовов подпрограмм и прерываний стек играет ведущую роль. Команда JMP заставляет ЦП отклониться от обычной процедуры последовательного выполнения команд, переходя к выполнению той команды, на которую совершается переход. Команда условного перехода (возможно 8 различных вариантов, обозначаемых обычно Jcc) проверяет регистр флагов[4], который располагается в ЦП (биты разрядов этого регистра устанавливаются в соответствии с результатом выполнения самой последней арифметической операции), а затем либо выполняет переход (если условие истинно), либо выполняет следующую за командой условного перехода команду (если условие не истинно). Программа 10.1 показывает пример условного перехода.
Она копирует 100 слов из массива, начинающегося с адреса 1000Н, в новый массив, начинающийся на 1 Кбайт (400Н), выше. Отметим явную загрузку указателя (в регистр ВХ, используемый для адресации) и счетчика цикла (в CL). Собственно массив слов должен быть пропущен через регистр (мы выбрали АХ), поскольку МП 8086 не поддерживает команды типа память-память (см. примечание к набору команд). В конце 100-го цикла CL = 0 и команда «перейти, если не нуль» (JNZ) более не выполняется. Этот пример будет работать[5], однако на практике вам, возможно, следует использовать более быстрые команды МП 8086-пересылки строк. Хорошим тоном в практическом программировании считается использование символьных имен для обозначения массивов и их размеров вместо соответствующих констант, таких как 400Н и 1000Н.
Оператор CALL является вызовом подпрограммы; он подобен каманде перехода, за исключением того, что адрес возврата (адрес команды, следующей за командой CALL) заносится в стек. В конце подпрограммы вы выполняете оператор RET, который извлекает из стека его содержимое так, чтобы программа могла найти «обратную дорогу» (рис. 10.5).
Рис. 10.5. Работа команды CALL.
Три оператора STI, CLI и IRET имеют отношения к прерываниям, их работу мы проиллюстрируем вместе с примерами соответствующих электрических схем и ниже в этой главе. Наконец, команды ввода-вывода IN и OUT пересылают слово или байт между регистром А и соответствующим образом адресованным портом; подробнее об этом чуть позже.
10.04. Программный пример
Примеры, приведенные выше, наводят на мысль о тяге языка ассемблера к многословию; требуется множество маленьких шажков для того, чтобы сделать в общем-то простую вещь. Вот пример другого рода: допустим, вам необходимо инкрементировать число N, если оно равно другому числу — . Таким будет типичный крошечный фрагмент большой программы, и на языках высокого уровня такое действие будет выполняться единственной командой:
IF(n = = n) + + n; (Си)
IF(N. EQ. M) N = N + 1; (Фортран)
IF n = m then n: = n + 1; (Паскаль) и т. п.
На ассемблере МП 8086 эти действия будут выглядеть, как показано в программе 10.2.
Программа-ассемблер превратит этот набор мнемонических выражений в машинные коды, как правило, транслируя каждую строку исходного ассемблерного текста в несколько байтов машинных команд, и полученные коды машинных команд прежде чем быть исполненными, будут загружены в последовательно расположенные ячейки памяти.
Отметим, что ассемблеру надо указать на необходимость выделения некоторого объема памяти под переменные. Это делается с помощью ассемблерного псевдооператора DW (Define Word-определить слово) (этот оператор является псевдооператором, так как ему не соответствует никакой исполняемый код). Для того чтобы помечать команды, могут быть использованы уникальные символьные метки (например, NEXT). Команды обычно помечаются лишь в тех случаях, когда на них осуществляется переход (JNZ NEXT). Присваивая переменным понятные вам самому имена и вводя комментарии (отделенные точкой с запятой), вы облегчаете себе процесс программирования; эти рекомендации означают также, что у вас будет шанс несколькими неделями позже понять, что вы написали. Программирование на языке ассемблера может по-прежнему оставаться неприятным делом, однако часто на этом языке бывает необходимо написать короткую процедуру управления вводом-выводом, вызываемую из программы, написанной на языке высокого уровня. Программы на языке ассемблера работают быстрее, чем скомпилированные с языка высокого уровня, так что их часто используют там, где показатель скорости работы является решающим (например, во многократно выполняемых внутренних циклах численных вычислений большого объема).
Разработка языка программирования Си, обладающего большими возможностями, минимизировала количество тех случаев, когда вы должны использовать ассемблерные программы, тем самым расширив сферу применимости Си. В любом случае вам трудно будет понять, как отдельные узлы компьютера работают совместно, без уяснения существа ассемблерных команд ввод-вывода. Соответствие между мнемоникой языка ассемблера и исполняемыми машинными командами будет изучено ниже в разд. 11.03, где будет проиллюстрировано примерами программирования МП 68000.