На главную
Подписка
Новости










Главная / MS-DOS / MS-DOS. РУКОВОДСТВО РАЗРАБОТЧИКА / Глава 2 / Передача данных в стек Сделать домашней страницей Добавить в избранное Написать писмо

НАЗАД СОДЕРЖАНИЕ ВПЕРЁД

Передача данных в стек

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


Что же такое "правильное положение" при загрузке в BP? Это не сам SP (stack pointer - указатель стека), поскольку SP указывает на адрес возврата в стек. Данные обычно начинаются с ячейки SP+2 или ячейки SP+4. Почему плюс 2, или плюс 4? Потому, что для вызова процедуры near (близкий) процессор запоминает только текущее смещение (указатель инструкции) в стеке (2 байта), в то время как для вызова процедуры far (далекий) процессор запоминает смещение и сегмент программы в стеке (4 байта). Вызываемая программа может быть закодирована для начала доступа в правильном положении (в зависимости от типа программы) при использовании следующей адресации:




             NEAR                          FAR
             mov  bp,sp                    mov  bp,sp
             mov  <1-й аргумент>,[bp+2]
             mov                           <1-й аргумент>,[bp+4]
             ...                             ...

Заметим, что если необходимо сохранить содержимое регистра BP, то обычно в этом случае вызываемая программа должна поместить BP в стек, изменив адрес первого параметра на [BP+4] для программы near и на [BP+6] для программы far. Чтобы избежать это изменение адресов, необходимо перед тем, как поместить параметры в стек, передать вызывающей программе ответственность за сохранение BP. Однако, из-за причин обеспечения совместимости это не рекомендуется. Вместо этого более предпочтительным способом передачи параметров является структура, показанная в листинге 2-1. Использование этой структуры, заимствованной большинством языков высокого уровня, поможет при разработке мобильных, многократно используемых программ. Эти программы могут быть собраны в "инструментальный набор", который необходимо использовать во многих местах для облегчения программирования и повышения производительности работы.


При возврате вызываемой программы параметры, которые были помещены в стек, теперь должны быть удалены. Вызывающая программа может удалить параметры либо извлечением из стека (путем использования инструкции POP), либо просто добавлением хранимых параметров в регистр SP, например, по инструкции add SP,N, где N представляет собой количество байтов, занимаемых параметрами. Этот способ, показанный в листинге 2-1, эффективно урезает стек в первоначальное положение. Альтернативно ответственность за очистку стека может быть назначена вызываемой программе путем использования инструкции RET N, где N опять количество байтов, занимаемое параметрами. При любом способе N равно количеству помещенных с помощью инструкции PUSH слов, умноженное на 2.


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



                      Листинг 2-1. Передача параметров в стек
         -----------------------------------------------------------------
                                ; Вызывающая процедура

              ...          ...
             push         ; пересылка последнего аргумента
              ...          ...
             push         ; пересылка   второго   аргумента
             push         ; пересылка первого аргумента
             call             ; вызов процедуры
             add       sp,<2N>        ; очистка стека
              ...          ...
                                ; Вызываемая процедура

          PROC NEAR           ; пример вызова процедуры near
             push      bp             ; сохранение старого BP
             mov       bp,sp          ; указатель ссылки на стек
              ...          ...
             mov       ,[bp+4] ; доступ к первому параметру
             mov       ,[bp+6] ; доступ ко второму параметру
              ...          ...
             mov       ,[bp+2+2N] ; доступ к последнему параметру
              ...          ...
             mov       sp,bp          ; восстановление SP
             pop       bp             ; удаление сохраненного BP
             ret                      ; возврат в вызывающую программу
          ENDP
         ----------------------------------------------------------------

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


С целью облегчения программирования следует заменить простой вызов внешними участками программы, использующими инструкции PUSH (записать в стек), MOV (переслать), POP (извлечь из стека) и прочие. Это как раз одно из мест для вывода известных и простых макросов для выполнения этих рутинных операций. Макросы, приведенные в листинге 2-2, помогают вызывающей программе поддерживать стек во время передачи параметров. Аналогично, макросы, приведенные в листинге 2-3, помогают вызываемой программе при доступе и возврате параметров из стека. Все регистры, используемые в этих макросах, должны быть длиной в слово, потому что инструкции PUSH и POP не работают с 8-битовыми регистрами.



         Листинг 2-2. Макросы @CallS и @FCallS для передачи параметров стек
         ------------------------------------------------------------------
         ;; **** Макрос @PushIm: запись в стек непосредственных данных
         ;;                                  через регистр BP
         @PushIm MACRO   arg
                 mov     cs:mem_16,&arg
                 push    cs:mem_16
                 ENDM
         ;; **** Макрос  Вызов подпрограммы: @Calls  имя,
         @CallS  MACRO   имя_программы,список_аргументов
         ?count  =       0
                 IRP     argn,<&arg_list>
                 push    &&argn          ; передача параметра
         ?count  =       ?count+1
                 ENDM
                 @PushIm %?count         ; передача количества параметров
                 call    &routine_name   ; вызов программы
                 add     sp,2*(1+?count) ; очистка стека
                 ENDM
         ;; **** Макрос  Вызов функции: @FCallS  имя,
         @FCallS MACRO   имя_пр-мы,список_арг-тов,возвращаемое_значение
         ?count  =       0
                 IRP     argn,<&arg_list>
                 push    &&argn          ; передача параметра
         ?count  =       ?count+1
                 ENDM
                 @PushIm %?count         ; передача количества параметров
                 call    &routine_name   ; вызов программы
                 pop &return_val         ; получение  возвращаемого знач-я
                 if ?count               ; если не нуль ...
                 add sp,2*?count         ; очистка стека
                 ENDIF    ENDM
         -----------------------------------------------------------------


                   Листинг 2-3. Макросы @Accept,@RetVal и @CRet
                 для приема в стек и возврата параметров из стека
         -----------------------------------------------------------------
         ;; **** Макрос  @RetVal: @RetVal регистр
         @retVal MACRO   возвращаемое_значение
                 mov     [bp+4],return_val ; возврат слова
                 ENDM
         ;; **** Макрос  @Accept: @Accept 
         @Accept MACRO   список_регистров
                 push    bp              ; сохранение указателя базы
                 mov     bp,sp           ; уст-ка BP для доступа к парам.
                 mov     &pnum,[bp+4]    ; получение количества парам-ов
         ?count  =       0
                 IRP     reg,<®_list>
         ?count  =       ?count+1
                 push    &®           ; сохр-е рег-ра для нового знач.
                 mov     &®,[bp+4+?count*2] ; получение параметра
                 ENDM
                 ENDM
         ;; **** Макрос  @CRet: @CRet 
         @CRet   MACRO   список_регистров
                 IRP     reg,<®_list>
                 pop     &®           ; восст-е сохраненного регистра
                 ENDM
                 pop     bp              ; восстановление указателя базы
                 ret                     ; возврат из программы
                 ENDM

Макрос @PushIm позволяет пользователям микропроцессоров 8086/8088 помещать непосредственные данные в стек. Для использования макроса сначала необходимо определить в программном сегменте местоположение слова mem_16. Несмотря на то, что передача непосредственных данных в стек медленная и принимаются большие коды, такой алгоритм работы создает большую свободу использования регистров.


Символ ?count в макросах @CallS и @FCallS используется для сообщения вызываемой программе количества предусмотренных параметров; для приема количества байтов, помещенных в стек; и для использования при очистке стека после вызова. Если целевая или вызываемая программа уже знает сколько параметров было в нее передано (обычно является случайным), то эти макросы должны быть модифицированы, чтобы обойтись без передачи и очистки счетчика параметров. Заметим, что счетчик параметров также используется в качестве поля возврата значения для вызова функций (макросы @FCallS и @RetVal).


Макрос @RetVal предназначен для использования с макросом @FCallS и замещает счетчик параметров, помещенный в стек с помощью макроса @FCallS, 16-битовым значением для возврата в вызывающую программу.


Макрос @Accept целевой программы работает либо с макросом @CallS, либо с макросом @FCallS для передачи параметров из стека в регистр. Этот макрос сохраняет регистры, используемые в процессе работы. Символ ?count используется здесь для определения смещения следующего параметра в стеке. В связи с тем, что макрос @Accept работает в направлении вверх по стеку (увеличение смещения), то этот макрос выбирает параметры из стека в порядке, обратном тому, в котором они были помещены в стек! Заметим также, что оба макроса @Accept и @RetVal предполагают вызов процедуры near (близкий), поскольку они допускают только 2-байтовый адрес возврата.


Последний целевой макрос @CRet восстанавливает регистры, которые были сохранены макросом @Accept. В связи с тем, что инструкции POP должны быть в обратном порядке по отношению к инструкциям PUSH, список аргументов для макроса @CRet должен располагаться в порядке, обратном тому, какой был при выполнении макроса @Accept. Последним действием, предпринимаемым перед инструкцией RET, является восстановление указателя базы, сохраненного макросом @Accept.


Приведенные макросы представлены здесь скорее в качестве примеров, нежели рабочих копий и могут быть улучшены для обеспечения более полного использования. Например, параметр инструкции PUSH (push &&argn) для обработки непосредственных данных в качестве параметров может быть замещен более общим макросом PushOp из главы 1. Одним из ограничений текущей версии является то, что инструкция mov [bp+4],return_value в макросе @RetVal не может возвращать переменные памяти в стек, потому что семейство микропро цессоров 8086 не поддерживает инструкцию пересылки память-память. Для распознавания пересылки память-память и генерации передачи через непосредственный регистр этот макрос должен быть переделан.


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


MASM обеспечивает для вызываемой программы некоторые средства, упрощающие доступ к данным в стеке. Благодаря описанию structure (структура), которая описывает данные в стеке и выравнивает указатель базы (BP) на начало структуры, к данным в стеке можно обращаться по символическим именам. Это помогает предотвращать фатальные ошибки кодирования, которые являются результатом указания неправильного смещения. Листинг 2-4 демонстрирует директиву MASM STRUC в этом контексте.



               Листинг 2-4. Символический доступ к содержимому стека
                                по директиве STRUC
         -----------------------------------------------------------------

                               ; Вызывающая процедура

                  ...        ...
                 push    ; пересылка 1-го аргумента
                 push    ; пересылка 2-го аргумента
                  ...        ...
                 push    ; пересылка последнего аргумента
                 call        ; вызов процедуры
                  ...        ...

                               ; Вызываемая процедура

         StackFrame     STRUC        ; описание шаблона стека
                 dw     ?            ; сохраненный BP
                 dв     ?            ; адрес возврата (используйте "dw"
         ;                             для NEAR (близкий))
         paramN  dw     ?            ; последний параметр
                  ...       ...
         param2  dw     ?            ; 2-й параметр
         param1  dw     ?            ; 1-й параметр
         StackFrame     ENDS         ; конец описания шаблона
         ;
         base    EQU    [bp]         ; база шаблона
         ;
          PROC  FAR          ; пример вызова far (далеко)
                 push   bp           ; сохранение старого BP
                 mov    bp,sp        ; указатель ссылки в стеке
                  ...      ...
                 mov    ,base.param1 ; доступ к 1-му параметру
                 mov    ,base.param2 ; доступ ко 2-му параметру
                  ...      ...
                 mov    ,base.paramN ; доступ к последнему пар-ру
                  ...      ...
                 mov    sp,bp        ; восстановление SP
                 pop    bp           ; сброс сохраненного BP
                 ret    (2N)         ; возврат в вызывающую программу
          ENDP
         ----------------------------------------------------------------

Листинги 2-1 и 2-4 различаются по трем важным аспектам. Первое отличие заключается в порядке помещения параметров в стек. В листинге 2-1 вызывающая программа размещает свои параметры в стеке в обратном порядке (от последнего к первому), в то время как в листинге 2-4 в прямом порядке (от первого к последнему). Для структуры StackFrame при работе с листингом 2-1 порядок параметров должен быть изменен на противоположный (т.е. реверсирован). (Назначение порядка "от первого к последнему" может привести к путанице в этой точке. На самом деле назначается порядок следования параметров "слева - направо", т.е. как они появлялись бы на языке высокого уровня).


Второе отличие между примерами заключается в способе очистки переданных параметров из стека. В примере, приведенном в листинге 2-1, вызывающая программа очищает параметры в стеке посредством инструкции add SP,<2N>. В листинге 2-4 вызываемая программа очищает стек, используя инструкцию ret (2N).


Последнее отличие заключается в том, что в листинге 2-1 показана программа near (близкий), в то время как в листинге 2-4 показана программа far (далекий). Если структура StackFram будет использоваться с процедурой near, то необходимо заменить директиву dd директивой dw. Это вызывает резервирование в шаблоне для адреса возврата вызывающей программы только двух байтов, в то время как для вызываемой процедуры far требуется четыре байта. С другой стороны, если структура будет использована в программе прерывания, то после директивы dd необходимо будет добавить дополнительную директиву dw для резервирования памяти для флажков процессора, помещаемых в стек при прерывании.


Директива STRUC не выполняет добавление никакого кода в конечную программу. Эта директива только описывает смещения, используемые с указателем базы BP, для облегчения задачи обращения к параметрам.


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


НАЗАД СОДЕРЖАНИЕ ВПЕРЁД

Hosted by uCoz