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

Textnew2

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

Важнейшим регистром процессора является регистр, в котором автоматически поддерживается адрес следующей для выполнения команды. Его называют счетчиком команд или подобным образом. В архитектуре Intel этот регистр назван указателем инструкций (instruction pointer) и обозначается EIP. Он имеет разрядность 32 бита. В 16-битной архитектуре используется младшая половина этого регистра, обозначаемая IP. Следует заметить, что в 16-битной архитектуре этого регистра в большинстве случаев недостаточно для указания места следующей команды, так как с помощью 1б-битного кода можно указать только смещение в 64 килобайтном сегменте, а это мало даже для компьютеров 80-х годов XX века. Поэтому для указания начала самого сегмента в архитектуре служит еще регистр CS (сокращение от code segment), который имеет 16 битов. В 32-битной архитектуре этот регистр также используется, но довольно сложным образом, и, главное, манипуляции над ним практически всегда поручаются операционной системе. Поэтому понимание роли этого специального регистра в 32-битных ОС не существенно для начинающих программистов. Мы для начального знакомства ограничимся рассмотрением использования регистра EIP.

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

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

CALL имя_подпрограммы

Заметим, что в архитектуре Intel для обозначения подпрограмм принята более частная терминология, а именно они обозначаются термином процедура. Напомним, что в терминологии языка Си все подпрограммы называются функциями, но в языке Паскаль используются оба термина: процедурами в нем называются подпрограммы, не возвращающие собственного значения, а функциями называются подпрограммы, обязательно возвращающие собственное значение. Из-за несогласованного многообразия использования этих терминов, мы и применяем более общие понятие подпрограммы. Далее через некоторое время мы перейдем на использова-

51

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

Работа аппаратно программного стека в архитектуре Intel также использует специальные регистры. Наиболее важным для начального знакомства построения этого стека является регистр ESP - расширенный регистр указателя стека (Stack Pointer). Он также 32-битный, а его младшая половина с обозначением SP используется в 16-битной архитектуре с этими же целями. Регистр ESP своим содержимым - относительным адресом в сегменте стека - указывает на верхнее запомненное поле в стеке. При выполнении команды PUSH для двойного слова регистр EIP автоматически уменьшается на 4. Стек растет от старших адресов к младшим, заполняясь со дна, которым является самый старший адрес в сегменте стека. При выполнении команды POP, которая снимает из стека 4-байтовое значение двойного слова, регистр EIP автоматически увеличивается на 4. При выполнении команд PUSH и POP для слов одинарной длины (16-битных операндов), указатель стека в EIP уменьшается или увеличивается, соответственно, на 2. (Заметим, что адрес начала сегмента стека задается с помощью специального регистра SS - Stack Segment, но нам особенности использования этого регистра сейчас не нужны.)

Наиболее важной командой в составе подпрограммы является команда, задаваемая мнемокодом RET. Ее действие соответствует действию оператора return в языке программирования Си. Детальные действия команды RET заключаются в том, что она снимает из стека верхнее значение и помещает его в регистр EIP (в 16-бит- ной архитектуре снимает слово и помещает его в IP). Как следствие, следующей автоматически будет выполняться команда, расположенная по адресу возврата, т.е. расположенная сразу за той командой CALL вызова подпрограммы, которая и обеспечила перед этим обращение к подпрограмме. В отличие от языков высокого уровня, в частности языка Си, присутствие команды RET совершенно необходимо в подпрограмме. При отсутствии этой команды в конце подпрограммы, будет автоматически выполняться двоичный код команды, записанный в памяти за последней командой, предусмотренной программистом в исходном коде. За последней командой подпрограмм в машинном коде не может быть ничего, какие-то коды оставшиеся от других программ или коды команд других подпрограмм там будут обязательно (редким, но возможным явлением может оказаться нарушение защиты памяти, но для программиста это также не то, что ему бы хотелось).

В ассемблерах MASM и TASM, подражающим языкам высокого уровня, для выделения команд подпрограммы из остальной части программного текста служат специализированные директивы, задаваемые ключевыми словами PROC и ENDP. Их используют согласно следующей схеме

имя_подпрограммы PROC команды подпрограммы ret

имя_подпрограммы ENDP

Таким образом, директива с ключевым словом PROC служит для именования подпрограммы, а вспомогательная директива с ключевым словом ENDP для

52

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

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

имя_подпрограммы:

команды подпрограммы ret

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

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

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

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

имя_подпрограммы: PUSH регистр1 PUSH регистр2

. . .

PUSH регистрN команды подпрограммы POP регистрN

. . .

53

POP регистр2

POP регистр1 ret

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

Когда используемых в подпрограмме регистров много, целесообразно для их запоминания использовать специальную команду PUSHA, а для их восстановления - команду POPA. Эти команды запоминают и восстанавливают, соответственно, регистры EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI. (Кроме уже известных по изложению выше, здесь используется еще специальный регистр EBP, роль и применение которого будет рассмотрены позже.)

3.5.Неарифметические операции над кодами

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

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

AND операнд1, операнд2

Основное практическое применение эта команда имеет с константным вторым операндом, который в этом случае называют маской логической операции. Команда AND с маской применяется для выборочного сброса в нуль отдельных битов двоичного кода первого операнда. (Такое действие основаны на обычных, хорошо известных свойствах логической операции И: x&0=0, x&1=x.) Например, команда

AND al, 3Fh

сбрасывает в нуль два старших бита двоичного кода в регистре AL

Для выполнения поразрядной логической операции ИЛИ служит команда с мнемокодом OR, имеющая общий вид

OR операнд1, операнд2

Основное практическое применение эта команда имеет также с константным вторым операндом - маской логической операции ИЛИ. Команда OR с маской применяется для выборочной установки (в единицу) отдельных битов двоичного кода первого операнда. (Такое действие основаны на обычных, хорошо известных свойствах логической операции И: xЪ0=x, xЪ1=1.) Например, команда

OR dx, 8001h

устанавливает в единицу старший и младший биты двоичного кода в регистре DX.

54

Менее употребительной является команда исключающего ИЛИ, задаваемая мнемокодом XOR и употребляемая обычно также с константой - маской в качестве второго операнда. В общем случае она имеет вид

XOR операнд1, операнд2

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

Последняя из логических команд выполняет операции поразрядного инвертирования двоичного кода единственного ее операнда и имеет вид

NOT операнд

Команды сдвига представлены множеством модификаций. Основными в большинстве применений являются команды с мнемокодами SHL и SHR, наименования которых восходят к английским словам Shift, Left и Right. Эти команды задают сдвиг двоичного кода в первом операнде на число разрядов, заданное во втором операнде. В настоящее время основными формами этих команд, являются задаваемые следующими схемами

SHL операнд1, число

SHR операнд1, число

Биты, вдвигаемые в этих командах, всегда нулевыми. Кроме перечисленных команд имеются еще команды арифметического сдвига SAL, SAR и циклического сдвига ROL, ROR, RCL, RCR. Из-за редкого их использования они не будут рассматриваться в данном пособии.

Далее на рис. 3.5.1 приведена программа демонстрирующая материал разделов 3.4 и 3.5. В этой программе содержаться подпрограммы вывода на экран содержимого регистров AL, AX и EAX в виде шестнадцатеричных представлений. Причем процедура call wrhex_hal отображает на экране значение только младшей половины регистра AL, выдавая его всегда в виде одной шестнадцатеричной цифры, процедура wrhex_al выдает содержимое регистра AL в виде двух шестнадцатеричных цифр, процедура wrhex_ax выдает содержимое регистра AX в виде четырех шестнадцатеричных цифр, а процедура wrhex_eax выдает содержимое регистра EAX в виде восьми шестнадцатеричных цифр.

; Использование процедур. GLOBAL _start

SEGMENT .text _start: mov al, 0d5h

call wrhex_hal call wrcrlf

mov al, 0fah call wrhex_al call wrcrlf

mov ax, 05e7bh

55

call wrhex_ax call wrcrlf

mov eax, 03fec5e7bh call wrhex_eax

call wrcrlf

mov eax,1

int 80h ; function=exit for Linux

;procedure wrcrlf wrcrlf:

pusha

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

int 80h popa ret

; end procedure wrcrlf

;procedure wrhex_hal wrhex_hal: push eax

push ebx push ecx push edx and al, 0Fh cmp al, 10 jge hexa add al, '0' jmp wrchar

hexa: add al,'A'-10 ; из AL вычесть 10 и прибавить значение 'A'

wrchar:

mov [cha],al

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

int 80h pop edx pop ecx

56

pop ebx pop eax ret

;end procedure wrhex_hal

;procedure wrhex_al wrhex_al: push eax

shr al,4

call wrhex_hal pop eax

call wrhex_hal ret

;end procedure wrhex_al

;procedure wrhex_ax wrhex_ax: push eax

shr ax,8

call wrhex_al pop eax

call wrhex_al ret

;end procedure wrhex_ax

;procedure wrhex_eax wrhex_eax: push eax

shr eax,16 call wrhex_ax pop eax

call wrhex_ax ret

;end procedure wrhex_eax

SEGMENT .data crlf db 10

cnt dd 0 cha db 0

Рис. 3.5.1. Вывод шестнадцатеричного значения из регистров AL, AX, EAX

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

Во всех приведенных процедурах используется сохранение регистров и восстановление их из стека, причем в процедуре wrcrlf для этого применена универсаль-

57

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

Основную работу для всех процедур выполняет подпрограмма wrhex_hal, предназначенная для вывода значения младшей половины регистра AL в виде одной шестнадцатеричной цифры. В ее начале содержимое регистра AL изменяется обнулением битов в старшей половине регистра. Для этих целей использована команда логического поразрядного умножения на константу 0fh. После этого выполняется сравнение промежуточного результата (значения младшей половины) с константой 10.

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

add al,'A'-10

которая прибавляет к промежуточному значению в регистре AL значение константы, сформированной из разности кода буквы 'A' и числа 10.

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

Процедура wrhex_al строится уже проще на основе использования процедуры wrhex_hal. В начале из аргумента - регистра AL - сдвигом на 4 бита в младшую половину регистра помещается его бывшая старшая половина. Затем обращением call wrhex_hal на экран выводится шестнадцатеричная цифра, отвечающая значению этой старшей половины. Потом в регистре AL восстанавливается первоначальное значение, бывшее в нем при обращении к данной подпрограмме call wrhex_hal. С этой цель ранее запомненное для сохранение значение всего регистра EAX восстанавливается командой pop eax. Далее опять вызывается подпрограмма wrhex_hal, которая выводит в виде шестнадцатеричной цифры значение младшей половины регистра AL. Тем самым отображаются обе шестнадцатеричных цифр значения всего регистра AL, причем в правильном порядке. Заметим, что какое-либо восстановление регистров после последнего вызова уже не требуется, так как после восстановления значения регистра EAX никаких изменений регистров в текущей процедуре wrhex_al не происходит. Это, в частности, демонстрирует, что общее правило восстановления регистров может в конкретных ситуациях видоизменяться.

58

Процедуры wrhex_ax и wrhex_eax построены очень похоже на процедуру wrhex_al, они отличаются только тем, что на первом этапе содержимое регистров, соответственно, AX и EAX сдвигается на 8 и 16 битов. Тем самым вначале задается для промежуточного вывода старшая половину соответствующего регистра, а затем, после восстановления значения регистра eax, используется отображение содержимого младшей половины этого регистра. Отображения же указанных половин выполняется вызовом, соответственно, процедур wrhex_al и wrhex_ax.

В основной части программы после занесения в регистры AL, AX и EAX констант 0fah, 05e7bh, 03fec5e7bh осуществляются вызовы, соответственно, процедур wrhex_al, wrhex_ax и wrhex_eax, что обеспечивает демонстрацию использования этих процедур.

Упражнения.

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

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

4.ИСПОЛЬЗОВАНИЕ НЕЭЛЕМЕНТАРНЫХ СПОСОБОВ АДРЕСАЦИИ

4.1. Косвенно-регистровая адресация и ее использование

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

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

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

59

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

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

MOV регистр, имя_области_данных

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

Косвенно-регистровый способ адресации задается на ассемблере записью операнда в виде [имя_регистра]. В качестве регистра, используемого в такой записи, можно применять любые из регистров EAX, EBX, ECX, EDX, ESI, EDI. Для более специальных целей в косвенно-регистровом способе адресации можно применять и регистры ESP, EBP, но делать это вне узкой области специализированного применения не рекомендуется. (Следует заметить, что в 16-битной архитектуре применение регистров для косвенно-регистровой адресации было существенно ограниченным, нельзя было с этой целью использовать регистры AX, CX и DX. Приходилось практически ограничиваться лишь регистрами BX, SI и DI, что было очень не удобно.)

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

MOV ecx, число_элементов_в_массиве

MOV edx, tabla povt: mov al, [edx]

анализ или обработка очередного байта, находящегося сейчас в регистре AL

INC edx LOOP povt

Первая и последняя команды в этом фрагмент вспомогательные для демонстрации возможностей рассматриваемого способа адресации (цикл может быть организован и как-то иначе). Собственно косвенно-регистровый способ адресации использован здесь во втором операнде третьей команды. Именно, в команде пересылки местом источника данных задается байт памяти, адрес которого содержит регистр edx. В первом шаге выполнения цикла в этом регистре содержится адрес начала области tabla, занесенный предыдущей командой, о которой речь уже шла раньше. Поэтому в первом шаге выполнения цикла в регистр AL пересылается значение первого байта из области tabla. Перед концом цикла использована команда инкремента регистра edx, благодаря которой перед вторым шагом значение адреса в

60

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