Электронный учебник - Assembler - Ответ 5356782

ГЛАВА 15
ПОДПРОГРАММЫ-ПРОЦЕДУРЫ

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

Макросы

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

Функция

— основная структурная единица процедурных и объектно-ориентированных языков. Функция представляет собой обособленную последовательность команд, вызываемую из различных частей программы. Функция может принимать один или несколько аргументов, а может быть без аргументов. Функция может возвращать результат своей работы (целочисленный результат или адрес, как правило, через регистр rax/eax/ax, вещественное число через st(0)), а может и ничего не возвращать. Ключевое свойство функции — по окончании работы функции управление возвращается на следующую инструкцию после вызова функции, характерный признак функции — многократный вызов функции из различных частей программы.

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

подпрограммой

.

При использовании подпрограмм основная программа становится более компактной. Кроме того использование подпрограмм облегчает поиск ошибок.

Где размещать подпрограмму?

Где угодно. При этом не нужно забывать, что сама по себе подпрограмма не должна выполняться, а может выполняться только тогда, когда к ней обращаются. Поэтому ее размещают таким образом, чтобы на нее случайно не попало управление, например, сразу же за командами JMP, RET. Если имеется несколько подпрограмм, то их располагают рядом. Обычно подпрограммы размещают:

в конце сегмента команд;
если по какой-то причине процедуры располагаются в середине сегмента команд, то перед процедурами ставят команду JMP, чтобы на них случайно не было передано управление;
в самом начале сегмента команд — перед той командой, с которой
должно начинаться выполнение программы;
процедуры могут разместить в отдельном модуле, который будет присоединен к основному, либо в виде OBJ-, либо в виде LIB-файла, во время линковки. В этом случае процедуры должны быть описаны как внешние;
так же как внешние, можно вызывать процедуры, расположенные в ядре операционной системы, системных dll, драйверах, dll написанных автором программы или другими
программистами.

В больших DOS-программах подпрограммы нередко размещались в отдельном сегменте команд.

Как оформлять подпрограмму?

Перед телом процедуры (ее командами) ставится директива PROC (

procedure

), за телом — команда RET (возврат из процедуры,

return

), после команды RET — директива ENDP (

end of procedure

). В обеих этих директивах указывается одно и то же имя — имя, которое мы дали подпрограмме. В директиве PROC после имени не ставится двоеточие, однако это имя считается меткой. Это имя указывается в команде перехода CALL, и тогда будет осуществлен переход на первую команду подпрограммы.

У директивы PROC есть параметр — это либо NEAR (близкий), либо FAR (дальний). Параметр может и отсутствовать, тогда считается, что он равен NEAR (в связи с этим параметр NEAR обычно не указывается). При параметре NEAR процедура называется близкой, при параметре FAR — дальней. К близкой процедура можно обращаться только из того сегмента, где она описана, а к дальней — из любого сегмента.

Вызов процедур и возврат из них

Существует два варианта CALL и RET для близких и дальних процедур.

PROC и ENDP — псевдооператоры определения процедур. PROC определяет начало процедуры, а ENDP — ее конец. Метка перед каждой из процедур — это имя, которое мы ей присвоили. Достаточно только вставить имя процедуры в текст программы, а ассемблер присвоит необходимые адреса сам.

Псевдооператоры NEAR и FAR предоставляют ассемблеру информацию о том как мы используем сегменты. Ассемблер применяет эту информацию при трансляции инструкции CALL. Существует два типа инструкций CALL и RET: близкие CALL NEAR и RETN и дальние CALL FAR и RETF. Дальний CALL вызывает процедуру, расположенную в другом сегменте. Ближний CALL вызывает процедуру из того же сегмента, в котором находится сам.

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

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

.686P
.model flat
include windows.inc
includelib user32.lib
extern _imp__MessageBoxA@16:dword
extern _imp__wsprintfA:dword
.code
start: xor eax,eax ;установить eax=0
 xor ebx,ebx ;установить ebx=0
 xor ecx,ecx ;установить ecx=0
 cdq ;установить edx=0
 mov al,1;загрузка суммируемых значений в al,bl,cl,dl
 mov bl,2
 mov cl,3
 mov dl,4
 call AddReg;eax=al+bl+cl+dl=10=0Ah
 call AddReg;eax=19=13h
 call AddReg;eax=28=1Ch
 push eax
 push offset format
 push offset buffer
 call _imp__wsprintfA;преобразуем значение для вывода на экран
 add esp,34 ;выравниваем стек после wsprintfA
 push 0
 push offset Caption ;заголовок
 push offset buffer ;сообщение
 push 0
 call _imp_MessageBox@16 ;выводим результат
 ret
;----------------------------------------
;AddReg суммирование al,bl,cl и dl
;----------------------------------------
; Вход: al,bl,cl,dl
; Выход: eax=al+bl+cl+dl
; Изменяются регистры: eax, bl, cl, dl
;----------------------------------------
AddReg proc
 add eax,ebx;eax=al+bl
 add eax,ecx;eax=al+bl+cl
 add eax,edx;eax=al+bl+cl+dl
 ret; возврат в вызывающую программу
AddReg endp
.data
format db 'eax=%08lXh',0
buffer db 100 dup (0)
Caption db 'моя первая процедура',0
end start

Компилируем и запускаем.

Главная часть программы расположена в строках 01-29 и 43-47. Подпрограмма описана в строках 31-42. Комментарии в строках 31-36 описывают имя подпрограммы, ее назначение, входные и выходные данные и используемые регистры. Пунктирная черта служит просто для выделения начала подпрограммы в объемном листинге. Директивы PROC и ENDP (строки 37 и 42) отмечают начало и конец подпрограммы. Команда RET (строка 41) указывает на выход из подпрограммы и служит для возвращения в основную программу.

В TASM после директивы PROC, а в MASM до директивы PROC следует имя подпрограммы, помечающее адрес первой команды (строка 38). Директива ENDP завершает подпрограмму. В TASM после директивы ENDP, а в MASM до директивы ENDP следует имя процедуры. В строке 16 главная программа вызывает подпрограмму с помощью команды CALL с именем процедуры. При исполнении команда CALL выполняет следующие действия:

помещает в стек адрес возврата на следующую после CALL команду (строка 17);
помещает в регистр IP/EIP/RIP или в регистровую пару CS:IP адрес подпрограммы и после этого делает безусловный переход на установленный в IP/EIP/RIP адрес. В нашем примере на строку 38. Затем с этой точки программа продолжает исполнять команды подпрограмм до тех пор, пока не дойдет до команды RET в строке 41.

Команда RET выполнит следующие действия:

поместит из вершины стека адрес возврата в регистр IP/EIP/RIP или в регистровую пару CS:IP;
сделает безусловный переход на установленный в IP/EIP/RIP адрес. В нашем примере на строку 17, откуда продолжится выполнение главной программы (рис. 15.2.1).

Рис. 15.2.1. Вызов и возврат из подпрограммы

Команда CALL

Синтаксис команды:

CALL <операнд>

Семантика команды

:

передача управления близкой или дальней процедуре с запоминанием в стеке адреса точки возврата;
переключение задач.

Алгоритм работы

(определяется типом операнда):

метка ближняя — в стек заносится содержимое указателя команд EIP/IP и в этот же регистр загружается новое значение адреса, соответствующее метке;
метка дальняя — в стек заносится содержимое указателя команд EIP/IP и CS. Затем в эти же регистры загружаются новые значения адресов, соответствующие дальней метке;
регистр или ячейка памяти, которая содержит смещение в текущем сегменте команд, куда передается управление. При передаче управления в стек заносится содержимое указателя команд EIP/IP;
указатель на память — определяет ячейку памяти, содержащую 4 или 6-байтный указатель на вызываемую процедуру. Структура такого указателя 2+2 или 2+4 байта. Интерпретация такого указателя зависит от режима работы микропроцессора:

в реальном режиме — в зависимости от размера адреса (use16 или use32) первые два байта трактуются как сегментный адрес, вторые два/четыре байта как смещение целевой метки передачи управления. В стеке запоминается содержимое регистров CS и EIP/IP;
в защищенном режиме — интерпретация цели передачи управления зависит от значения байта AR дескриптора, определяемого селекторной частью указателя. Целью здесь является дальний вызов процедуры без изменения уровня привилегий, дальний вызов процедуры с изменением уровня привилегий или переключение задачи.

При переключении задачи значения флажков изменяются в соответствии с информацией о регистре EFLAGS в сегменте состояния TSS задачи, на которую производится переключение.

Применение

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

AssemblerВыделить код

1
2

call a1
a1: pop eax; Получение значение eip в eax

Процедура MessageBox должна получить через стек четыре параметра, но вместо этого параметр-ссылку на адрес строки 'Привет!' мы передаем через вызов процедуры, как адрес возврата из процедуры.

.686P

.model flat

include windows.inc

includelib user32.lib

extern

_imp__MessageBoxA@16:dword

.code

start: push MB_OK

push 0

call a1

Hello db 'Привет!',0

a1: push 0

call _imp__MessageBoxA@16

ret

end start

.686P

.model flat

include windows.inc

includelib user32.lib

extern

_imp__MessageBoxA@16:dword

.code

start: push MB_OK

push 0

push offset Hello

push 0

call _imp__MessageBoxA@16

ret

.data

Hello db 'Привет!',0

end start

Команды RET/RETN/RETF

(Возврат ближний /дальний из процедуры =

RETURN Near/Far from procedure

)

Синтаксис команды

:

RET
RET <смещение>

Семантика команды

: возврат управления из процедуры вызывающей программе.

Алгоритм работы

(работа команды зависит от типа процедуры):

для процедур ближнего типа — восстановить из стека содержимое EIP/IP;
для процедур дальнего типа — последовательно восстановить из стека содержимое EIP/IP и сегментного регистра CS.
если команда RET имеет операнд смещение, то увеличить содержимое ESP/SP на величину смещения; при этом учитывается атрибут режима адресации — use16 или use32:

если use16, то SP=(SP+смещение), то есть указатель стека сдвигается на число байт, равное значению смещения;
если use32, то ESP=(ESP+2*смещение), то есть указатель стека сдвигается на число слов, равное значению смещения.

Применение

: команду RET необходимо применять для возврата управления вызывающей программе из процедуры, управление которой было передано по команде CALL. На самом деле микропроцессор имеет два варианта команды возврата RET — это RETN (return near) и команда RETF (return far). Они отличаются типами процедур, в которых используются. Команда RETN служит для возврата из процедур ближнего типа. Команда RETF — команда возврата для процедур дальнего типа. Какая конкретно команда будет использоваться, определяется компилятором; программисту лучше использовать команду RET и доверить транслятору самому сгенерировать ее ближний или дальний вариант. Количество команд RET в процедуре должно соответствовать количеству точек выхода из нее.

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

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

AssemblerВыделить код

1
2
3
4

MY_PROC PROC
. . .
RET 6
MY_PROC ENDP

Команду RET можно также использовать для завершения программы.

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

байт и встречается в программе

раз. Если эта группа команд оформлена как процедура, то в подпрограмме понадобится дополнительная команда RET для выхода из процедуры (1 байт), и кроме того, по одной команде с CALL

адрес_процедуры

в каждом из

мест, где необходим вызов процедуры (ближний вызов процедуры в 32-разрядном режиме — 5 байт). В сумме это дает

байт, а не

, и поэтому использование процедур должно уменьшить основную программу на:

байт

Если процедура вызывается всего один раз (

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

). Если тело процедуры содержит 6 байт (

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

, число

должно быть больше 7 (мы должны вызвать процедуру не менее 7 раз).

Количество потерянного времени равно общей длительности выполнения дополнительных команд CALL и RET (

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

раз, то требуется

дополнительных тактов синхронизации.

Косвенный вызов процедур

Предположим, что вы пишете программную оболочку наподобие

«Far», «Волков» или «Norton Commander». Помните, как они устроены?

Если вы нажали на функциональную клавишу

F1 — вызывается помощь
F2 — вызов меню
F3 — вызов программы просмотрщика
...
F10 — выход из программы

На языке ассемблера это выглядит примерно так:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

F0: MOV AH,0;функция 00h прерывания 16h - чтение данных из буфера
    INT 16h; клавиатуры с ожиданием нажатия клавиши. При выходе
              ; из функции в регистре AL находится ASCII-код клавиши,
              ; в регистре AH - Scan-код нажатой клавиши
    CMP AH,3Bh; Scan-код клавиши F1 равен 3Bh. Сравниваем, что находится
    JNE F2; в регистре AH. Если нажали на клавишу F1 - вызываемая
    CALL HelpProc; процедура вывод помощи на экран. После завершения
    JMP F0; процедуры возвращаемся к началу программы, где снова
             ; ожидаем нажатия какой-либо клавиши. Если нажатая клавиша
             ; не F1, переходим на следующую метку F2 и так далее.
F2:  CMP AH,3Ch;проверяем Scan-код клавиши F2
  JNE F3
  CALL MenuProc
  JMP F0
F3:  CMP AH,3Dh;проверяем Scan-код клавиши F3
  JNE F4
  CALL ViewerProc
  JMP F0
  ...
F10:  CMP AH,44h;проверяем Scan-код клавиши F10.Если нажали на F10 -
  JNE F0;завершаем программу, если нажатая клавиша не F10 -
  INT 20h;возвращаемся в начало программы

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

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

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

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

table DW HelpProc ;F1=3Bh;адрес процедуры HelpProc
   DW MenuProc ;F2=3Ch;адрес процедуры MenuProc
   DW ViewerProc ;F3=3Dh;адрес процедуры ViewerProc
   ...;остальные процедуры
   DW ExitProc ;F10=44h;адрес процедуры ExitProc
f0:mov ah,0;ожидаем нажатия на клавишу
  int 16h
  xchg bx,ax
  cmp bh,44h;scan-код клавиши F10
  ja f0;этой клавиши нет в таблице.
  shr bx,8
  sub bl,3Bh;scan-код клавиши F1
  jb f0;и этой клавиши нет в таблице.
  shl bx,1;адрес процедуры - слово
  call word ptr table[bx];выполняем подпрограмму, соответствующую
             ;нажатой клавише
  jmp F0;возврат в начало программы

Другие варианты команды CALL

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

Примеры:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

NA DW P
FA DD Q
P PROC
P1: ...
P ENDP
Q PROC FAR
Q1: ...
Q ENDP
CALL P1 ;близкий переход на P1 с возвратом
CALL FAR PTR Q1 ;дальний переход на Q1 с возвратом
CALL Q1 ;близкий переход на Q1 с возвратом
CALL NA ;близкий вызов процедуры P
CALL FA ;дальний вызов процедуры Q
LEA BX,Q
CALL [BX] ;близкий вызов процедуры Q
CALL DWORD PTR [BX] ;дальний вызов процедуры Q

При использовании этих вариантов команды CALL надо соблюдать осторожность.

Первое

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

Второе

: как и в команде JMP, при использовании в команде CALL ссылок вперед или косвенных ссылок следует уточнить тип этих ссылок (по умолчанию при ссылке вперед ассемблер транслирует близкий прямой вызов, а при косвенной ссылке — близкий косвенный вызов).

Если вы вызываете процедуру FP, описанную как дальнюю, командой CALL FP, или вы вызываете процедуру командой CALL FAR PTR FP, транслятор генерирует последовательность команд PUSH CS и CALL FP.

Машинный

код

Интерпретация

команды
Ассемблерный текст
00: E81C00
CALL 0000001F
CALL F1; ближний вызов F1
03: 0E
PUSH CS
CALL FAR PTR F; дальний
04: E81900
CALL 00000020
вызов процедуры F
07: 9AFFFF0000
CALL 0000:FFFF
db 9Ah,0FFh,0FFh,0,0; дальний вызов

процедуры с указанием непосредственно-

го адреса (только в кодах)
0C: FF13
CALL [BP+DI]
CALL [BP+DI]
0E: FF1B
CALL FAR [BP+DI]
db 0FFh,1Bh; дальний вызов [BP+DI]
0E: FF97009A
CALL [BX+9A00]
CALL [BX+9A00h]
12: FF9F009A
CALL FAR [BX+9A00]
db 0FFh,9Fh,0,9Ah; дальний вызов проце-

дуры с регистровой относительной адре-

сацией (только в кодах)
16: 26
ES:
CALL ES:[BX]
17: FF17
CALL [BX]
19: 26
ES:
db 26h,0FFh,1Fh; дальний вызов
1A: FF1F
CALL FAR [BX]
ES:[BX] (только в кодах)
1D: C3
RETN RET;

выход из программы

F1 PROC NEAR; ближняя процедура F1
1E: C3
RETN
RET

F1 ENDP; выход из процедуры F1

F PROC FAR: дальняя процедура F
1F: CB
RETF

RET

ENDP: выход из процедуры F

Чтобы сгенерировать дальние вызовы процедур и сэкономить 1 байт на команде PUSH CS (код 0E), необходимо написать код вызова процедуры и добавить ко второму байту команды 8, например: код FF17 соответствует команде CALL [BX], а код FF17+0008=FF1F — команде CALL FAR [BX].

Попытка напрямую оттранслировать команду «CALL FAR PTR ES:[BX]» вызывает ошибку транслятора «Argument to operation or instruction has illegal size». Попытка оттранслировать вот такую команду «CALL FAR PTR ES:[BX]» вызывает следующую ошибку транслятора «Illegal immediate». При трансляции же команды «CALL FAR PTR [BX]» будет сгенерирован код FF57FE, почему-то соответствующий команде «CALL [BX-2]», которая дальним вызовом не является, вероятнее всего это ошибка транслятора. Вывод — программируйте дальние вызовы в машинных кодах.

Чтобы обмануть хакера, можно:

1) использовать команду «CALL метка» вместо «JMP метка», правда, помните, что в стеке останется адрес выхода;
использовать сочетание двух команд «PUSH адрес возврата» и
«JMP адрес процедуры» вместо команды «CALL процедура».

Например:

AssemblerВыделить код

1
2
3
4
5
6

push offset a
jmp b
a: …
…
b: тело процедуры
ret

В микропроцессоре Intel 8080 имелись следующие команды, которых нет в x86:

Название

команды
Содержание команды

Усло-

вие
CC
переход к подпрограмме по единичному значению флага переноса
CF=1
CNC
переход к подпрограмме по нулевому значению флага переноса
CF=0
CZ
переход к подпрограмме по нулевому значению результата
ZF=1
CNZ
переход к подпрограмме по ненулевому значению результата
ZF=0
CP
переход к подпрограмме по положительному значению результата
SF=0
CM
переход к подпрограмме по отрицательному значению результата
SF=1
CPE
переход к подпрограмме по четному значению кода результата
PF=1
CPO
переход к подпрограмме по нечетному значению кода результата
PF=0
RC
выход из подпрограммы по единичному значению флага переноса
CF=1
RNC
выход из подпрограммы по нулевому значению флага переноса
CF=0
RZ
выход из подпрограммы по нулевому значению результата
ZF=1
RNZ
выход из подпрограммы по ненулевому значению результата
ZF=0
RP
выход из подпрограммы по положительному значению результата
SF=0
RM
выход из подпрограммы по отрицательному значению результата
SF=1
RPE
выход из подпрограммы по четному значению кода результата
PF=1
RPO
выход из подпрограммы по нечетному значению кода результата
PF=0

Используя сочетание двух команд «PUSH адрес возврата» и «Jcc адрес процедуры» вместо команды «CALL процедура» можно воскресить эти команды.

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

Передача параметров процедуре

Нашей подпрограмме AddReg для хранения складываемых значений были необходимы четыре регистра: al,bl,cl,dl. Подпрограмма возвращала полученную сумму в регистре eax. Передача значений в подпрограмму через регистры является наиболее общим методом получения подпрограммами данных для обработки. Кроме передачи данных через регистры данные могут передаваться через глобальные переменные, а также через стек.

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

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

Передача параметров через регистры
Передача параметров по значению

Иногда требуется передать процедуре фактические параметры. Простейший способ — передавать параметры через регистры: основная программа записывает фактические параметры в какие-то регистры, а процедура затем берет их оттуда и использует в своей работе. Результат процедура может также записать в какой-нибудь регистр, а основная программа затем извлекает его оттуда. Через какие регистры будут переданы параметры и через какие регистры будет возвращен результат — это личное дело автора программы, хотя эта проблема более подробно обсуждается в разделе «Модульное конструирование процедур».

Для примера вычислим d=min(a,b)+min(5,a-1). Вычисление функции min(x,y) опишем как процедуру, первый параметр передадим через регистр EAX, а второй — через EBX, результат вернем через регистр EAX:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

;процедура EAX=min(EAX,EBX)
MIN PROC
CMP EAX,EBX
CMOVBE EAX,EBX
RET
MIN ENDP
;основная программа
MOV EAX,a
MOV EBX,b
CALL MIN ;EAX=min(a,b)
MOV d,EAX ;d=min(a,b)
MOV EAX,5
MOV EBX,a
DEC EBX ;EBX=a-1
CALL MIN ;EAX=min(5,a-1)
ADD d,EAX ;d= min(a,b)+ min(5,a-1)

Передача параметров по ссылке

Пусть имеется процедура D с параметром x и пусть процедура производит следующее вычисление x=x/16. В основной программе есть обращения к нашей процедуре D(A) и D(B), где A и B имена переменных, значениями которых являются неотрицательные числа. Процедуре надо знать адреса ячеек A и B, в которые она должна сделать запись, и эти адреса обязана ей сообщить основная программа. Передача параметров по ссылке означает передачу адреса ячейки, соответствующей фактическому параметру. Передача адреса происходит через регистр — основная программа записывает в регистр адрес фактической переменной, а процедура берет его оттуда. Пусть для косвенной адресации в процедуре D мы выбрали регистр EBX, то есть к началу выполнения процедуры в регистре EBX будет находиться адрес той ячейки (A или B), содержимое которой процедура изменяет:

AssemblerВыделить код

1
2
3
4
5
6

;Процедура D(x)
D PROC; EBX = адрес x
SHR DWORD PTR [EBX],4; x=x/16
RET
D ENDP
. . .

фрагмент основной программы

AssemblerВыделить код

1
2
3
4
5

mov EBX,offset A; EBX = адрес A
CALL D ; D(A)
. . .
mov EBX,offset B; EBX = адрес B
CALL D ; D(B)

Сохранение регистров в процедуре

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

Передача параметров сложных типов

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

Пусть имеются массивы из чисел:

и требуется записать в регистр DL сумму максимальных элементов этих массивов DL=max(X[i])+max(Y[j]). Поскольку дважды приходится находить максимальный элемент массива, то, чтобы не повторяться, опишем это действие в виде процедуры MAX. Пусть начальный адрес массива передается через регистр BX, количество элементов — через регистр CX, а результат возвращается через регистр AL:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11

MAX PROC;AL=max(W[0…N-1]),где EBX=начальный адрес W, ECX=N
      PUSH EBX;сохраняем регистры
      PUSH ECX
      MOV AL,0; начальное значение максимума AL=0
MAX1:  CMP [EBX+ECX-1],AL
     CMOVA AL,[EBX+ECX-1];присваиваем AL значение W[ECX-1]
     LOOP MAX1;переход на следующий элемент
     POP ECX;восстанавливаем регистры
     POP EBX
     RET
MAX ENDP

фрагмент основной программы для вычисления DL=max

AssemblerВыделить код

1
2
3
4
5
6
7
8

MOV EBX,offset X;EBX= начальный адрес массива X
MOV ECX,100;ECX=элементов в массиве X
CALL MAX;AL=max(X[i])
MOV DL,AL;DL=max(X[i])
MOV EBX,offset Y;EBX=начальный адрес массива Y
MOV ECX,25;ECX=элементов в массиве Y
CALL MAX;AL=max(Y[j])
ADD DL,AL;DL=max(X[i])+max(Y[j])

Передача параметров через стек

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

Изменим нашу подпрограмму AddReg, чтобы передавать ей параметры через стек:

AssemblerВыделить код

1
2
3
4
5

push 1
push 2
push 3
push 4
call AddReg

Далее мы должны эти данные передать в регистры eax, ebx, ecx и edx.

AssemblerВыделить код

1
2
3
4
5
6
7
8

AddReg proc
pop edx;извлекаем параметры подпрограммы
pop ecx
pop ebx
pop eax
...;остальные команды
ret
AddReg ENDP

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

Для правильного решения сначала сохраним адрес возврата, вытолкнем параметры в регистры eax, ebx, ecx и edx и затем снова вернем адрес возврата в стек:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10

AddReg proc
pop esi;сохраним в esi адрес возврата
pop edx;извлекаем параметры подпрограммы
pop ecx
pop ebx
pop eax
push esi;вернули в стек адрес возврата
...
ret
AddReg ENDP

Вроде бы все заработало, только должен вас огорчить, так через стек параметры не передают. Правильнее будет вот так. Наша подпрограмма AddReg имеет четыре параметра AddReg(1,2,3,4). Перед обращением к процедуре основная программа записала параметры в стек. Параметры записываются в стек слева направо: сначала первый параметр, затем второй и т.д. Команды основной программы, реализующие обращение к процедуре:

AssemblerВыделить код

1
2
3
4
5

push 1
push 2
push 3
push 4
call AddReg

| | |
| --- | --- |
| ESP | Адрес возврата |
| ESP+ 4 | 4 |
| ESP+ 8 | 3 |
| ESP+12 | 2 |
| ESP+16 | 1 |

Рис. 15.6.1. Состояние стека после входа в процедуру

Как процедуре добраться до параметров? Как добраться к элементам стека без считывания из стека? Используем косвенную адресацию через регистр ESP.

AssemblerВыделить код

1
2
3
4
5
6
7
8

AddReg proc
mov edx,[esp+4];извлекаем параметры подпрограммы
mov ecx,[esp+8]
mov ebx,[esp+12]
mov eax,[esp+16]
...
ret
AddReg ENDP

Но это был всего лишь частный случай, а если нам придет в голову использовать внутри процедуры команды PUSH и POP? Воспользуемся регистром EBP, засылаем в него адрес вершины стека (содержимое регистра ESP), а затем используем выражение вида [EBP+i] для доступа к параметрам процедуры. Мы можем испортить содержимое регистра EBP, а он может использоваться в основной программе. Поэтому сначала сохраним значение этого регистра в стеке, а затем пересылаем в регистр EBP значение регистра ESP:

AssemblerВыделить код

1
2
3
4
5
6

AddReg PROC
PUSH EBP
MOV EBP,ESP
...;команды процедуры AddReg
POP EBP
RET

| | |
| --- | --- |
| ESP,EBP | EBPстарое |
| EBP+ 4 | Адрес возврата |
| EBP+ 8 | 4 |
| EBP+12 | 3 |
| EBP+16 | 2 |
| EBP+20 | 1 |

Рис. 15.6.2.

Входные и выходные действия процедуры

После записи в стек старого значения EBP (EBPстарое) для доступа к параметрам процедуры надо использовать следующие выражения: [EBP+8] — для доступа к последнему параметру, [EBP+12] — для доступа к предпоследнему параметру и так далее. Например, считывание последнего параметра в регистр EDX осуществляется командой MOV EDX,[EBP+8]. После того как процедура отработает, стек должен быть в том же состоянии, в каком он был до использования процедуры. Восстанавливаем командой POP EBP старое значение EBP и после этого возможны два варианта завершения процедуры:

После выполнения команды RET в стеке остаются использованные процедурой параметры и основная программа должна очистить стек. Но было бы лучше, если очистку стека будет делать сама процедура, так как обращений к процедуре может быть много и в основной программе команду ADD
ESP,4*k придется повторять многократно (рис. 15.6.3).
Существует правило: если что-то может сделать и основная программа и процедура, то лучше, если это будет делать процедура.

AssemblerВыделить код

1
2
3
4
5
6
7


push 1;первая локальная переменная
push 2;вторая локальная переменная
push 3;третья локальная переменная
push 4;четвертая локальная переменная
call AddReg;команды процедуры
ADD ESP,44;принудительная очистка стека
      ;от 4 параметров - двойных слов

Рис. 15.6.3

Процедура сама очищает стек от параметров и передает управление по адресу возврата (рис. 15.6.4):

AssemblerВыделить код

1
2
3
4
5

...;команды процедуры
POP EBP;восстановить старое значение EBP
RET 4k;принудительная очистка стека
AddReg ENDP;от k параметров - двойных слов
; возврат из процедуры

Рис. 15.6.4

По команде RET число из стека удаляется адрес возврата, затем стек очищается на указанное операндом число байтов и для перехода по адресу возврата выполняются следующие действия:

Значение из стека в IP/EIP
[значение из стека в CS]
ESP=ESP+число

(действие по восстановлению значения CS из стека выполняется лишь при дальнем возврате).

Команда RET — это на самом деле команда RET 0, то есть возврат без очистки стека. Операнд команды RET указывает на сколько байтов, а не слов надо очищать стек. Для очистки стека от k параметров, каждый из которых имел размер двойного слова, был указан операнд 4*k. В операнде не должен учитываться адрес возврата — команда RET считывает его до очистки стека.

C- и PASCAL-процедуры

При передаче аргументов через стек командой PUSH можно пренебречь. Расширенный синтаксис инструкции CALL в TASM

CALL имя_процедуры язык [, аргументы]

Параметр команды CALL «язык» определяет способ передачи аргументов процедуре и способ очистки стека от переданных аргументов. В качестве «языка» допускается применение ключевых слов: C, CPP, PASCAL, BASIC, PROLOG или NOLANGUAGE. Если тип языковых соглашений не задан, то используется тип, указанный в директиве MODEL или явное указание типа языковых соглашений в объявлениях PROC или PUBLIC перекрывает для данного идентификатора тип, назначенный директивой MODEL. В языках PASCAL, BASIC и PROLOG аргументы процедуры будут переданы в стек в том же порядке, в котором они указываются в операторе вызова процедуры, а в языках C/C++ используется обратный порядок. Языковые соглашения определяют порядок очистки стека от переданных аргументов. В языках C/C++ это возлагается на вызывающую процедуру, а во всех остальных на вызываемую. Следующая программа, написанная под TASM, вызывает одну и туже процедуру t1 с параметром PASCAL и C. Процедура t1 закончена директивой RET, а компилятор, в зависимости от языкового соглашения, сам подставит RETN/F или RETN/F

число

:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12

.386
.model flat
.code
start: call t1 PASCAL,1,2,3 ;вызываем t1(1,2,3) по
; соглашению о стиле вызова функций как в языке PASCAL
call t1 C,1,2,3;вызываем t1(1,2,3) по соглашению о стиле
; вызова функций как в языке C/С++
retn; выход из программы
t1 proc; описание процедуры t1
ret; выход из подпрограммы
t1 endp
end start

Будет скомпилирована в следующую последовательность команд:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11

push 1
push 2
push 3
call 4010A1h
push 3
push 2
push 1
call 4010A1h
add esp,12; очистка стека от 12 байт (в исходном тексте отсутствует)
retn
4010A1h: retn

Видно, что в зависимости от параметра PASCAL или C, изменился порядок следования аргументов процедуры t1, а после call t1 C,1,2,3 появилась инструкция add esp,12, которой не было в первоначальном тексте.

MASM не позволяет записать аргументы CALL через запятую — для этого в MASM есть директива INVOKE. Для использования INVOKE требуется первоначально определить прототип процедуры, то есть указать количество аргументов и их тип:

имя_ процедуры PROTO [расстояние] [язык] [, [параметр]:тип]...

Скомпилированные программы будут отличаться в зависимости от параметров директивы model, proto, proc или от ключей, использованных при компиляции (/Gc или /Gd). Следующая программа использует соглашение о стиле вызова функций как в FORTRAN или в PASCAL:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10

.386
.model flat
.code
t1 PROTO pascal,:DWORD,:DWORD,:DWORD; прототип процедуры t1
start: invoke t1, 1, 2, 3
retn; выход из программы
t1 proc pascal, a:DWORD,b:DWORD,d:DWORD; описание процедуры t1
ret; выход из подпрограммы
t1 endp
end start

Эта программа компилируется в следующую последовательность команд:

AssemblerВыделить код

1
2
3
4
5
6

push 1
push 2
push 3
call 4000D0h
retn; выход из программы
4000D0: retn 0Ch; очистка стека от аргументов будет сделана t1

А эта программа использует соглашение о стиле вызова функций как C/C++ в директиве

model

:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10

.386
.model flat, c
.code
t1 PROTO :DWORD,:DWORD,:DWORD
start: invoke t1, 1, 2, 3
retn
t1 proc a:DWORD,b:DWORD,d:DWORD; функции будет передано три аргумента a, b и d
ret
t1 endp
end start

и компилируется в в следующую последовательность команд:

AssemblerВыделить код

1
2
3
4
5
6
7

push 3
push 2
push 1
call 4000D3h
add esp,0Ch; очистка стека от аргументов будет сделана основной программой
retn; выход из программы
4000D3: retn; выход из подпрограммы без очистки от аргументов

Локальные данные процедуры

Локальные данные процедуры — это величины, используемые только на время выполнения процедуры. Обычно локальные данные размещают в регистрах, но если в процедуре много локальных данных, регистров может не хватить, а размещать локальные данные в памяти не рационально, так как после выполнения процедуры это место в памяти будет расходоваться напрасно. Поэтому для локальных данных выделяют место в стеке — при входе в процедуру в вершине стека захватывается нужное число байт для локальных данных, а перед выходом из процедуры это место освобождается. Для этого в стеке запоминается текущее значение регистра EBP, а затем в регистр EBP устанавливается значение ESP, уменьшенное на число байт, требуемых для локальных данных. Например, процедуре P требуется 6 байт (4 байта под локальную переменную A и 2 байта под локальную переменную B):

AssemblerВыделить код

1
2
3
4

P PROC
PUSH EBP
MOV EBP,ESP
SUB ESP,8

ESP→
0
B
-8
Локальные
A
-4
данные
EBP
←EBP
Адрес возврата

Доступ к локальным данным осуществляется с помощью выражения вида [EBP-k]: [BP-4] — адрес локальной переменной A, [BP-8] — адрес локальной переменной B. После завершения работы процедуры необходимо выполнить команды:

AssemblerВыделить код

1
2
3
4

MOV ESP,EBP ;отказ от места для локальных переменных
POP EBP ;восстановление старого значения BP
RET ;возврат из процедуры
P ENDP

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

прологом

(prolog), а выход из функции —

эпилогом

(epilogue). Во время пролога программа готовится к доступу к локальным переменным и параметрам функции. Этот доступ называется

кадром стека

(stack frame).

AssemblerВыделить код

1
2
3
4
5

;стандартный пролог
PUSH EBP;сохранить значения регистра кадра стека
MOV EBP,ESP;записать в регистр кадра стека значение из ESP
SUB ESP,N;увеличить объем стека для хранения N байт
;локальных переменных

Была даже специально создана команда ENTER N,0 — эквивалент последовательности команд PUSH EBP/MOV EBP,ESP/SUB ESP,N, но выполняется команда ENTER N,0 медленнее

AssemblerВыделить код

1
2
3

;стандартный эпилог
MOV ESP,EBP;восстановить значение стека
POP EBP;восстановить значение регистр кадра стека

Команда LEAVE является эквивалентом последовательности команд MOV ESP,EBP/POP EBP, но выполняется команда LEAVE медленнее.

Рекурсивные процедуры

Иногда алгоритм определяется в собственных терминах, другими словами, алгоритм вкладывает сам в себя таким образом, что вычислительная процедура вызывает саму себя. Например, алгоритм Гонера вычисления полинома ((…(a

n

x+a

n-1

)x+a

n-2

)x+…)x+a

1

)x+a

0

включает в себя перемножение

x

и

a

n

, прибавление к результату a

n-1

, умножение на

x

полученного результата, прибавление к новому результату

a

n-2

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

x

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

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

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

рекурсивными

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

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

Ниже даны примеры рекурсивных процедур для вычисления чисел Фибоначчи и для вычисления факториала.

Функция, вычисляющая число Фибоначчи по правилу

если n=0 или n=1

если n≥2

Аргумент функции передается через регистр EAX, а значение возвращается через регистр EBX:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

mov eax,N ;вычисляем N-ое число Фибоначчи
call fibonacci ;вызываем процедуру
push ebx
push offset format
push offset buffer
call _imp__wsprintfA
add esp,12 ;выравниваем стек после wsprintfA
push 0
push offset caption
push offset buffer
push 0
call _imp__MessageBoxA@16;выводим результат на экран
ret;выходим из программы
fibonacci proc
;Аргумент функции передается через регистр EAX, а
;значение возвращается через регистр EBX:
; EBX=F(N) - число Фибоначчи с номером N (EAX=N)
cmp eax,1 ;если N>1 перейти к метке f
ja short f1
;нерекурсивная ветвь
mov ebx,1 ;если N£1 EBX=F(N)=1
retn
;рекурсивная ветвь
f: push eax ;сохранить EAX
dec eax ;EAX=N-1
call fibonacci
push ebx ;сохранить EBX=F(N-1)
dec eax ;EAX=N-2
call fibonacci;EBX=F(N-2)
pop eax ;EAX=F(N-1)
add ebx,eax ;EBX=F(N)=F(N-2)+F(N-1)
pop eax ;восстановить исходное значение в EAX
retn
fibonacci endp
format db 'fibonacci=%u',0
buffer db 50 dup (0)
caption db 'fibonacci',0

Процедура вычисления факториала

если n =0

если n≥1

Параметр [ESP+4] представляет собой отыскиваемый факториал.

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

push N ;вычисляем N факториал
call fact ;вызываем процедуру
push edi ;edi=N! здесь результат
push offset format
push offset buffer
call _imp__wsprintfA
add esp,12 ;выравниваем стек после wsprintfA
push 0
push offset caption
push offset buffer
push 0
call _imp__MessageBoxA@16 ;выводим результат на экран
ret;выходим из программы
;структура стекового кадра
;[esp] сохраненный адрес возврата из процедуры
;[esp+4] место для N
fact proc
mov ecx,[esp+4];ecx=N
jecxz done ;N равно 0?
dec ecx ;для следующего вызова
push ecx ;включить в стек ecx=N-1
call fact
mov eax,edi ;в edi значение предыдущего результата
mul dword ptr [esp+4] ;eax=Nedi
jmp short rtrn
done: mov eax,1 ;eax=1
rtrn: mov edi,eax;возвращаем новое значение результата в edi
retn 4 ;удаляем ненужные локальные переменные
fact endp
format db 'factorial=%lu',0
buffer db 50 dup (0)
caption db ' factorial‘,0

Разложение в ряд

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

во-первых

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

все

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

Пусть

x

— любое число, а

f(x)

— некоторая функция, зависящая от

x

, непрерывная и имеющая все производные при

x

=

a

, тогда

f(x)

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

где

— известное число близкое к

;

— известное значение этой функции.

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

— число, из которого нужно извлечь кубический корень.

,

— известное число близкое к

;

,

,

поэтому

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

3

=8, 3

3

=27, 4

3

=64, 5

3

=125, 6

3

=216,… Попробуем извлечь кубический корень, например из 100, используя только сложение, деление и умножение.

Ближайшее к 100 число 125 с известным кубическим корнем 5.

Куб величины

можно сравнить с исходным числом 100. Если данная точность вас почему-либо не устраивает, то придется использовать вместо a число 101,62963, а вместо

— 4,6667 и повторить вычисления.

Модульное конструирование процедур

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

Сохраняйте и восстанавливайте всегда все регистры (их значения), кроме тех случаев, когда процедура передает значение в регистре.
Будьте последовательны в применении для передачи информации одних и тех же регистров.
Например:

DL, DX, EDX — для передачи байта, слова или двойного слова.
AL, AX, EAX — для возвращения байта, слова или двойного слова.
EBX — передача и возвращение адреса
ECX — счетчик повторения и другие счетчики
CF — устанавливается при ошибке; код ошибки возвращается в одном из регистров, например в AL, AX или EAX.

Определяйте все внешние взаимодействия процедуры в заголовке-комментарии:

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

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

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

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

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

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

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

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

Резервируйте регистр ECX для использования в качестве счетчика повторений. Старайтесь использовать регистр ECX всегда, когда вам понадобится счетчик повторений или когда вы захотите вернуть результат какого-либо подсчета, например, числа символов, считанных с клавиатуры.

Наконец, устанавливайте флаг переноса (CF) всякий раз, когда происходит ошибка.

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

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

Комментарий-заголовок

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

AssemblerВыделить код

1
2
3
4
5
6
7

;Это пример полного заголовка. В этом месте обычно приводится полное
;описание того, что данная процедура делает. Например: процедура
;записывает сообщение «Sector» в первую строку. EDX содержит
;адрес сообщения «Sector»
;calls (вызываемые процедуры): GOTO_XY, WRITE_STRING
;Reads (переменные, считываемые из памяти): STATUS_LINE_NO
;Writes (переменные, считываемые и записываемые в память): DUMMY

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

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

Кодировка CALL и RET

Внешние процедуры

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

При компиляции программы ассемблер сгенерирует соответствующую команду вызова процедуры, однако поле адреса этой процедуры он оставит незаполненным. На этапе компиляции адрес внешней процедуры (так называется процедура, находящаяся в библиотеке объектных модулей) не известен ассемблеру. Адрес определяется компоновщиком на этапе сборки исполняемого файла. Компоновщик находит указанную процедуру в указанной программистом библиотеке объектных модулей, копирует ее машинный код в исполняемый файл пользователя и прописывает адрес этой процедуры в тех местах программы, где эта процедура вызывается с помощью команды CALL.

В идеологии работы приложений под управлением Windows и DOS имеются коренные различия. При написании DOS-программ обращение к операционной системе осуществляется посредством вызова различных программных прерываний (обращение к DOS через прерывание 21h, обращение к BIOS через прерывание 10h, обращение к клавиатуре через прерывание 16h и так далее), а также напрямую (через порты ввода-вывода) к ресурсам компьютера. Программы, написанные под Windows, непосредственно к ресурсам компьютера никогда не обращаются. Для реализации взаимодействия с многозадачной операционной системой Windows используется интерфейс, базирующийся на вызове функций, специально определенных в операционной системе. Это множество системных функций (более 2500) называется интерфейсом прикладного программирования Windows (Windows Application Programming Interface — Windows API).

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

С точки зрения программирования, функции Windows API аналогичны «обычным» функциям. Они имеют набор входных и выходных параметров, иногда возвращают через регистр eax в вызвавшую их программу некоторое значение. Так как функций Windows API более двух тысяч, можно предположить, что каждая программа для операционной системы Windows должна быть связана с большим количеством программных библиотек, что должно приводить к дублированию большого объема кода.

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

Вместо него он добавляет только инструкции для загрузки функции из DLL-файлов, поэтому ваши файлы будут содержать только имена Windows API-функций и имена DLL, в которых эти функции находятся. При выполнении программы все необходимые функции Windows API также загружаются в память. Таким образом, при построении программы код функций Windows API фактически не используется — он добавляется только тогда, когда программа загружается в память для выполнения.

Наши программы под Windows будут состоять из нескольких модулей. Один модуль (файл с расширением asm) мы будем писать сами. В другом модуле (файл с расширением inc —

include

— подключенный) объявлены стандартные константы и типы данных. Третий модуль (файл с расширением lib —

library

— библиотека) сообщит транслятору, из каких системных библиотек нужно взять Windows API-функции. Когда ассемблер транслирует asm-модуль, то он еще ничего не знает о dll-модулях (описанных в lib-файле) и именах функций, описанных в dll-модулях.

Поэтому, встретив в asm-модуле имена API функций и не найдя их описания в этом файле, ассемблер зафиксирует ошибку «неописанное имя». Что-бы не было такой ошибки, ассемблеру сообщают, что имена функций хотя и используются в asm-модуле, но описаны все же в другом модуле, откуда они будут взяты. Такое сообщение делается с помощью директивы EXTERN (

external

— внешний), имеющей следующий вид:

EXTERN <имя>:<тип>, … , <имя>:<тип>

Директива EXTERN может указываться любое число раз и в любых местах модуля. Для связи asm-модуля с другими модулями необходимо использовать директиву INCLUDE (включить). В общем случае обращение к директиве INCLUDE имеет следующий вид:

INCLUDE <имя файла>

Имя файла должно быть заключено в угловые скобки, если оно содержит символы: «/» (прямой слэш), «;» (точка с запятой), «>» (больше), «<» (меньше), «‘» (одиночная кавычка), «”» (двойная кавычка).

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

INCLUDELIB <имя библиотеки>

Строка includelib user32.lib — это сообщение транслятору, что наша программа должна быть связана с библиотекой user32.lib. Основные системные библиотеки обычно находятся в папке Windows\System и содержат большое количество библиотек функций Windows API. В число основных входят три библиотеки:

kernel32.dll. Эта библиотека предназначена для работы с объектами ядра операционной системы Windows, ее функции позволяют управлять памятью и другими системными ресурсами.
библиотека пользователя user32.dll. В этой библиотеке сосредоточены функции для управления окнами, обработка сообщений, работа с меню, таймерами и т. д.
библиотека графического интерфейса устройств gdi32.dll — функции управления выводом на экран дисплея, управление выводом на принтер, функции для работы со шрифтами и так далее.

Контрольные вопросы и упражнения

Что произойдет при выполнении программы, если в процедуре будет отсутствовать команда RET?
Какая разница между кодированием в директиве PROC операнда с типом FAR и с типом NEAR?
Каким образом программа может начать выполнение процедуры?
Для какой цели в поле операнда RET допускается употребление выражения?
Напишите набор процедур для выполнения беззнаковых арифметических операций. Процедуры получают аргументы через стек и возвращают результат в регистрах EDX:EAX. В набор входят процедуры:

ADDITION (сумма двух аргументов);
SUBTRACT (разность двух аргументов. Уменьшаемое передается в стек первым);
MULTIPLY (умножение двух аргументов);
DIVIDE (деление двух аргументов и возвращение в EAX частного, а в EDX остатка. Делимое передается в стек первым).

Напишите рекурсивную процедуру для вычисления полинома по правилу Горнера: . Коэффициенты находятся в смежных областях памяти и адреса всех параметров передаются через стек.

Вернуться к обсуждению:

Электронный учебник Assembler