CodeNet / Языки программирования / Ассемблер / СПРАВОЧНИК по системе программирования ТУРБО АССЕМБЛЕР 2.0
СПРАВОЧНИК по системе программирования ТУРБО АССЕМБЛЕР 2.0
Подпрограммы ----------------------------------------------------------------- До сих пор мы рассматривали только программы, представляющие собой одну длинную последовательность исходного кода. Каждая программа начиналась с раздела кода, выполняла поочередно каждую инструкцию (иногда изменяя свой маршрут при выполнении циклов или принятии решений), а затем завершалась в конце раздела кода. Все это хорошо для небольших программ, но в больших программах требу- ется использования таких конструкций, которые называются подпрог- раммами. Возможно вы уже знакомы с подпрограммами в языках высокого уровня. В языке Си подпрограммы называются функциями, в Паскале и Бейсике - процедурами и функциями. Подпрограммы, процедуры и фу- нкции представляют собой, в общем, одно и то же - отдельную часть кода, воспринимающую определенные входные данные, выполняющую оп- ределенные действия и, возможно, возвращающую полученное в ре- зультате значение. Подпрограммы позволяют вам строить программы по модульному принципу. При этом подпрограммы позволяют "скрывать" специфичес- кие детали (то есть убирать их на нижний уровень) и сосредоточить внимание на общем алгоритме программы. Подпрограммы позволяют также сделать программы намного более компактными, поскольку от- дельную подпрограмму можно вызывать во многих местах программы и даже выполнять различные функции, передавая ей различные значе- ния. В больших программах (независимо от того, написаны они на Ассемблере, Паскале или Си) подпрограммы являются существенным средством для создания упорядоченного и легко обслуживаемого ис- ходного кода. Выполнение подпрограмм ----------------------------------------------------------------- Основные моменты выполнения подпрограммы иллюстрируются на Рис. 5.12. В вызывающей подпрограмму программе выполняется инст- рукция CALL, которая заносит адрес следующей инструкции в стек и загружает в регистр IP адрес соответствующей подпрограммы, осуще- ствляя таким образом переход на подпрограмму. После этого под- программа выполняется, как любой другой код. В подпрограммах мо- гут (часто это так и бывает) содержаться инструкции вызовов дру- гих подпрограмм. Фактически, должным образом построенные подпрог- раммы могут даже вызывать сами себя (это называется рекурсией). . . . . . . . . . . . . |-------------| |------------| 1000 | mov al,1 | (в IP загружается -->| shl al,1 | 1110 |-------------| 1110 и 1007 зано- | |------------| 1002 | mov bl,3 | сится в стек) | | add al,bl | 1112 |-------------| | |------------| 1004 | call DoCalc |-------------------- | and al,7 | 1114 |-------------| |------------| 1007 | mov ah,2 |<------------------- | add al,'0' | 1116 |-------------| Значение вершины | |------------| 1009 | int 21h | стека 1007 извле- ---| ret | 1118 |-------------| кается и заносится в |------------| . . IP . . . . . . . . . . Рис. 5.12 Выполнение подпрограммы. Когда подпрограмма заканчивает работу, она вызывает инстру- кцию RET, которая извлекает из стека адрес, занесенный туда со- ответствующей инструкцией CALL, и заносит его в IP. Это приводит к тому, что вызывающая программа возобновит выполнение с инструк- ции, следующей за инструкции CALL. Например, следующая программы выводит на экран три строки: Привет Пример строки Еще одна строка Для вывода строк вызывается подпрограмма PrintString: DOSSEG .MODEL SMALL .STACK 200h .DATA Message1 DB 'Привет',0dh,0ah,0 Message2 DB 'Пример строки',0dh,0ah,0 Message3 DB 'Еще одна строка',0dh,0ah,0 .CODE ProgramStart PROC NEAR mov ax,@Data mov ds,ax mov bx,OFFSET Message1 call PrintString ; вывести строку "Привет" mov bx,OFFSET Message2 call PrintString ; вывести строку "Пример строки" mov bx,OFFSET Message3 call PrintString ; вывести строку "Еще одна ; строка" mov ax,4ch ; функция DOS завершения ; программы int 21h ; завершить программу ProgramStart ENDP ; ; Подпрограмма вывода на экран строки, завершающейся ; нулевым символом ; ; Входные данные: ; DS:BX - указатель на выводимую строку. ; ; Нарушаемые регистры: AX, BX ; PrintString PROC NEAR PrintStringLoop: mov al[bx] ; получить следующий символ ; строки and dl,dl ; значение символа равно 0? jz EndPrintString ; если это так, то вывод ; строки завершен inc bx ; ссылка на следующий ; символ mov ah,2 ; функция DOS вывода символа int 21h ; вызвать DOS для вывода ; символа jmp PrintStringLoop ; вывести следующий символ, ; если он имеется EndPrintString: ret ; возврат в вызывающую ; программу PrintString ENDP END ProgramStart Здесь стоит отметить два момента. Во-первых, подпрограмма PrintString не настроена жестко на печать определенной строки. Она может печатать любую строку, на которую с помощью BX укажет вызывающая программа. Во-вторых, для выделения подпрограммы ис- пользованы две новых директивы - PROC и ENDP. Директива PROC используется для того, чтобы отметить начало процедуры. Метка, указанная в директиве PROC (в данном случае - PrintString), представляет собой имя процедуры, как если бы ис- пользовалось: PrintString LABEL PROC Однако, директива PROC делает большее. Она определяет, какую инструкцию RET (возврат управления) - ближнюю или дальнюю - сле- дует использовать в данной процедуре. Давайте рассмотрим последний оператор несколько подробнее. Вспомним, что когда выполняется переход на метку ближнего типа (NEAR), в IP загружается новое значение, а при переходе на даль- нюю метку (FAR) новые значения загружаются и в регистр IP, и в CS. Если инструкция CALL ссылается на дальнюю метку, загружаются и CS, и IP (как и при переходе). Вот почему при дальнем вызове в стек заносятся и регистр CS, и IP. Иначе откуда инструкция RET получит достаточную информацию для возврата в вызывающую программу? Ведь если дальний вызов заг- рузит CS и IP, а занесет в стек только IP, то при возврате можно будет только загрузить IP из вершины стека. Тогда в результате выполнения инструкции RET пара регистров CS:IP содержала бы зна- чение CS вызываемой программы, а IP - вызывающей, что очевидно не имеет смысла. Что же произойдет, когда в стек будут занесены оба регистра - CS и IP? Как Турбо Ассемблер узнает о типе возврата, генерируе- мом в соответствующей подпрограмме? Один из путей состоит в явном задании типа каждой инструкции возврата - RETN (возврат ближнего типа) или RETF (возврат дальнего типа), однако лучший способ зак- лючается в использовании директив PROC и ENDP. Директива ENDP используется для того, чтобы пометить конец подпрограммы, начатой с помощью директивы PROC. Директива ENDP отмечает конец подпрограммы, которая начинается с директивы PROC с той же меткой. Например, директивы: . . . TestSub PROC NEAR . . . TestSub ENDP . . . отмечают начало и конец подпрограммы TestSub. Директивы ENDP и PROC не генерируют выполняемого кода, ведь это директивы, а не инструкции. Все их действие заключаются в управлении типом инструкции RET данной подпрограммы. Если операндом директивы PROC является NEAR (ближний), то все инструкции RET между директивой PROC и соответствующей ди- рективой ENDP ассемблируются, как возвраты управления ближнего типа. Если же, с другой стороны, операндом директивы PROC являет- ся FAR (дальний), то все инструкции RET в данной процедуре ассем- блируются, как возвраты управления дальнего типа. Поэтому, чтобы, например, изменить тип всех инструкций RET в TestSub, измените директиву PROC следующим образом: TestSub PROC FAR В общем случае лучше там, где это возможно, использовать подпрограммы ближнего типа, так как дальние вызовы занимают боль- ше памяти и выполняются медленнее, а возвраты дальнего типа также выполняются медленнее, чем ближнего. Однако подпрограммы дальнего типа становятся необходимыми, когда объем кода вашей программы превышает 64К. Если вы используете упрощенные директивы определения сегмен- тов, то лучше использовать директиву PROC без операндов, напри- мер: TestSub PROC Когда Турбо Ассемблер встречает такую директиву, он автома- тически настраивает ее тип в соответствии с выбранной с помощью директивы .MODEL моделью памяти (по умолчанию это малая модель памяти). Программы со сверхмалой, малой и компактной моделями па- мяти могут иметь вызовы ближнего типа, а вызовы в программах со средней, большой и сверхбольшой моделью памяти имеют дальний тип. Например, в программе: . . . .MODEL SMALL ; малая модель памяти . . . TestSub PROC . . . подпрограмма TestSub вызывается с помощью ближнего вызова, а в программе: . . . .MODEL LARGE ; большая модель памяти . . . TestSub PROC . . . с помощью дальнего.