Современные решения

для защиты Windows приложений

и восстановления исходного кода

Написание 16-битного кода (DOS, Windows 3/3.1) в ассемблере NASM

В данной главе рассмотрены некоторые общие вопросы создания 16-битного кода, выполняющегося под MS-DOS или Windows 3.x: как скомпоновать программы для получения .EXE или .COM файлов, как создать драйвер устройства .SYS, а также как ассемблерный код взаимодействует с 16-битными компиляторами и с Borland Pascal.

7.1 Получение .EXE файлов

Любые большие программы, написанные под DOS, необходимо создавать как .EXE файлы: только они имеют необходимую внутреннюю структуру для захвата более одного 64К сегмента. Программы Windows также требуется создавать как .EXE файлы, так как .COM файлы Windows не поддерживает.

Обычно .EXE файлы генерируются при помощи выходного формата obj (при этом создаются один или более .OBJ файлов, связываемых затем друг с другом компоновщиком). Однако при помощи выходного формата bin и некоторых макросредств NASM поддерживает также непосредственное создание простых .EXE файлов DOS (заголовок .EXE файла конструируется при помощи DB и DW). Спасибо Yann Guidon за содействие при кодировании этого.

В будущем NASM может быть станет поддерживать и "родной" выходной .EXE формат.

7.1.1 Использование формата obj для получения .EXE файлов

В данном параграфе описан обычный способ создания .EXE файлов путем компоновки друг с другом .OBJ файлов.

Большинство 16-битных языков программирования поставляются с собственным компоновщиком; если у вас нет ни одного, возьмите с ftp://x2ftp.oulu.fi/pub/msdos/programming/lang/ свободно распространяемый компоновщик VAL, упакованный в формате LZH. LZH-архиватор можно найти на ftp://ftp.simtel.net/pub/simtelnet/msdos/arcers имеется еще один бесплатный компоновщик FREELINK (только он без исходников), и наконец, на http://www.delorie.com/djgpp/16bit/djlink/ можно взять компоновщик djlink, написанный DJ Delorie.

При компоновке нескольких .OBJ файлов в один .EXE файл вы должны убедиться, что только один из них (.OBJ) имеет точку входа (при помощи специального символа ..start, определяемого obj форматом: см. параграф 6.2.6). Если ни один из модулей не определяет точки входа, компоновщик не будет знать, какое значение записать в заголовок выходного файла в качестве стартового адреса; если же точка входа определена в нескольких файлах, компоновщик не сможет понять, какое именно значение использовать.

Здесь приводится пример исходного файла, который ассемблируется NASMом в .OBJ файл и им же компонуется в .EXE. На этом примере продемонстрированы основные принципы определения стека, инициализации сегментных регистров и объявления точки входа. Данный файл содержится также в подкаталоге test NASM-архивов под именем objexe.asm.

          segment code 

..start:  mov ax,data 
          mov ds,ax 
          mov ax,stack 
          mov ss,ax 
          mov sp,stacktop

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

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

          mov dx,hello 
          mov ah,9 
          int 0x21

Здесь начинается основная программа: загрузка в DS:DX указателя на приветствующее сообщение (hello является неявной ссылкой на сегмент data, загруженный в DS настроечным кодом, поэтому полный указатель корректен) и вызов DOS-функции вывода строки на экран.

          mov ax,0x4c00 
          int 0x21

Здесь программа завершается при помощи другого системного DOS-вызова.

          segment data 
hello:    db 'Привет, фуфел!', 13, 10, '$'

Сегмент данных содержит строку, которую нужно отобразить на экране.

          segment stack stack 
          resb 64 
stacktop:

Данный код объявляет сегмент стека, содержащий 64 байта неинициализированного стекового пространства, где символ stacktop указывает на его вершину. Директива segment stack stack определяет сегмент под именем stack, тип которого также STACK. В нашем случае не требуется дальнейшее выполнение программы, но если в ней не определить сегмент STACK, компоновщики вероятнее всего выдадут предупреждение или сообщение об ошибке.

Приведенный выше файл будет ассемблироваться в .OBJ файл, затем компоноваться NASM в корректный .EXE файл, который при запуске будет выводить на экран строку 'Привет, фуфел!' и затем завершаться.

7.1.2 Использование формата bin для получения .EXE файлов

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

В архиве NASM имеется подкаталог misc, в котором находится файл макросов exebin.mac. В этом файле определены три макроса: EXE_begin, EXE_stack и EXE_end.

Для создания файла при помощи формата bin вы должны включить в свой исходный файл директиву %include exebin.mac, загружающую в него пакет требуемых макросов. Затем для генерации заголовка файла вы должны выполнить макрокоманду EXE_begin (не имеет аргументов). После этого следует обычный для формата bin код программы — вы можете использовать все три стандартные секции .text, .data и .bss. В конце файла вы должны вызвать макрос EXE_end (без аргументов), который для маркировки размеров секции определяет некоторые символы, ссылающиеся на заголовок кода, сгенерированный макросом EXE_begin.

В данной модели написанный вами код стартует с адреса 0x100, как и обычный .COM файл — в действительности, если вы удалите 32-битный заголовок из сгенерированного .EXE файла, то получите работающую .COM программу. Все базы сегментов в полученном файле одинаковы, поэтому размер программы ограничен 64К (опять же, как и .COM файл). Имейте в виду, что директива ORG используется макросом EXE_begin, поэтому вы не должны применять ее самостоятельно.

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

При запуске полученного .EXE файла пара SS:SP настраивается на указание вершины 2Кб стека. Вызвав макрос EXE_stack, вы можете изменить размер стека по умолчанию. Например, для изменения размера стека вашей программы до 64 байт вы должны вызвать EXE_stack 64.

В подкаталоге архива NASM содержится простая программа binexe.asm, из которой .EXE файл создается вышеописанным способом.

7.2 Получение .COM файлов

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

7.2.1 Использование формата bin для получения .COM файлов

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

          org 100h 
          section .text 
start:    ; сюда поместите код 
          section .data 
          ; сюда поместите данные 
          section .bss 
          ; здесь находятся неинициализированные данные

Формат bin помещает секцию .text в начале файла, поэтому вы можете объявлять данные или BSS перед написанием собственно кода.

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

Для ассемблирования приведенной выше программы вы должны использовать следующую командную строку:

nasm myprog.asm -fbin -o myprog.com

Если явно не указать имя выходного файла, формат bin создаст файл с именем myprog; в этом случае вы можете просто переименовать его так, как вам нужно.

7.2.2 Использование формата obj для получения .COM файлов

Если вы пишете .COM программу с применением более одного модуля, то возможно захотите ассемблировать несколько .OBJ файлов и затем собрать их в одну программу. Вы можете это сделать двумя путями: при помощи компоновщика, способного непосредственно создавать .COM файлы (TLINK это может) или применив конвертер EXE2BIN для преобразования .EXE файла, полученного на выходе компоновщика, в .COM файл.

Если вы это делаете, вам нужно позаботиться о нескольких вещах:

  • Кодовый сегмент первого объектного файла должен начинаться со строки вида RESB 100h. Это гарантирует начало кода по смещению 100h относительно начала сегмента, так чтобы компоновщик или программа конвертации не корректировали адресные ссылки при генерации .COM файла. Другие ассемблеры для данной цели используют директиву ORG, однако в NASM ORG является дополнительной директивой выходного формата bin и не означает то же самое, что в MASM-совместимых ассемблерах.
  • Вам не нужно определять сегмент стека.
  • Все ваши сегменты должны быть в одной и той же группе, чтобы все смещения на символы как в коде, так и в данных были смещениями относительно одной и той же базы сегмента. Это нужно для того, чтобы при загрузке .COM файла все сегментные регистры содержали одно и то же значение.

7.3 Получение .SYS файлов

Драйверы устройств MS-DOS — .SYS файлы — это чисто бинарные файлы, во всем похожие на .COM, за исключением того, что они запускаются по нулевому смещению, а не по смещению 100h. Поэтому если вы пишете драйвер при помощи формата bin, директива ORG вам не нужна, так как по умолчанию смещение для bin всегда нулевое. Соответственно вам не требуется указывать в начале кодового сегмента RESB 100h, если вы используете формат obj.

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

Дополнительную информацию о формате .SYS файлов и данных, содержащихся в их заголовочной структуре, вы можете почерпнуть в часто задаваемых вопросах конференции comp.os.msdos.programmer.

7.4 Взаимодействие с 16-битными C-программами

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

7.4.1 Внешние символьные имена

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

Если вам неудобно использовать знаки подчеркивания, вы можете определить макросы для замены директив GLOBAL и EXTERN следующим образом:

%macro cglobal 1 
          global _%1 
%define %1 _%1 
%endmacro

%macro cextern 1 
          extern _%1 
%define %1 _%1 
%endmacro

(Данные формы макросов принимают только один аргумент; если вам требуется больше, используйте конструкцию %rep).

Если вы определите внешний символ как

          cextern printf

макрос развернет его в следующие строки:

          extern _printf 
%define printf _printf

Thereafter, you can reference printf as if it was a symbol, and the preprocessor will put the leading underscore on where necessary.

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

7.4.2 Модели памяти

NASM прямо не поддерживает механизма различных моделей памяти, реализованных в С; вы должны отслеживать это самостоятельно. Это означает, что вы должны учитывать следующее:

  • В моделях, имеющих один сегмент кода (tiny, small и compact), функции являются ближними. Это значит, что указатели на функции при сохранении в сегменте данных или помещении в стек являются 16-битными и содержат только поле смещения (регистр CS никогда не изменяет свое значение и всегда содержит сегментную часть полного адреса функции) и что такие функции вызываются инструкцией near CALL и возврат из них производится при помощи RETN (что в NASM является синонимом RET). Следовательно, вы должны писать свои подпрограммы с использованием RETN, а также вызывать внешние С-подпрограммы при помощи ближней инструкции CALL.
  • В моделях, использующих более одного сегмента кода (medium, large и huge), функции являются дальними. Это значит, что длина указателей функций составляет 32 бита (16 бит смещение и 16 бит сегмент) и что функции вызываются при помощи CALL FAR (или CALL seg:offset) и возврат из них производится при помощи RETF. При использовании таких моделей вы должны писать свои подпрограммы так, чтобы возврат из них производился по RETF, а внешние подпрограммы вызывать при помощи CALL FAR.
  • В моделях, использующих единственный сегмент данных (tiny, small и medium), указатели на данные являются 16-битными, содержащими только поле смещения (регистр DS не изменяет своего значения и всегда представляет сегментную часть полного адреса).
  • В моделях, использующих более одного сегмента данных (compact, large и huge), длина указателей на данные составляет 32 бит, 16 бит из которых является смещением, а другие 16 бит — сегментом. Вы должны стараться в своих подпрограммах не модифицировать DS без необходимости, а после модификации — всегда восстанавливать. В то же время регистр ES свободен и вы можете использовать его для доступа к содержимому, на которое ссылается 32-битный указатель.
  • Модель памяти huge позволяет одиночным элементам данных превышать размер 64К. В любых других моделях вы можете получить доступ ко всем элементам данных простым арифметическим манипулированием переданного поля смещения (неважно, присутствует поле сегмента или нет). В модели памяти huge к вычислению указателей надо подходить более тщательно.
  • В большинстве моделей памяти имеется сегмент данных по умолчанию, сегментный адрес которого хранится в DS на протяжении всего выполнения программы. Этот сегмент данных обычно совпадает с сегментом стека, хранящемся в SS, поэтому и локальные переменные функций (хранящиеся в стеке), и глобальные элементы данных могут быть легко доступны без изменения DS. Большие элементы данных обычно хранятся в других сегментах. Однако некоторые модели памяти (хотя обычно они нестандартные) используют SS и DS по другому. Будьте внимательны в этом случае по отношению к локальным переменным функций.

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

7.4.3 Определения и вызовы функций

Соглашения по вызовам С в 16-битных программах описываются ниже.

  • Вызывающая программа помещает параметры функции в стек один за другим в обратном порядке следования (так что первый аргумент функции помещается в стек последним).
  • Вызывающая программа выполняет инструкцию CALL для передачи управления подпрограмме. Эта инструкция в зависимости от модели памяти может быть как ближней, так и дальней.
  • Подпрограмма получает управление и обычно (несмотря на то, что это не требуется в функциях, которым не нужен доступ к своим параметрам) начинается с сохранения SP в BP с целью дальнейшего использования BP в качестве базы указателя для нахождения параметров в стеке. Однако частью соглашений о вызовах является сохранение содержимого BP любой функцией С. Следовательно подпрограмма, если она использует BP как указатель кадра, должна предварительно поместить в стек его содержимое.
  • Подпрограмма может получить свои параметры через BP. Слово [BP] хранит предыдущее значение BP, помещенное в стек; следующее слово, [BP+2], хранит смещение адреса возврата, помещенное в стек инструкцией CALL. В ближних функциях после этого ([BP+4]) начинаются параметры; в дальных функциях по адресу [BP+4] находится сегментная часть адреса возврата и параметры начинаются с [BP+6]. Самый левый параметр функции доступен по смещению из BP, т.к. в стек он был помещен последним; следующие параметры соответственно доступны по следующим смещениям. Таким образом, в функциях с переменным числом параметров, подобных printf, помещение параметров в стек в обратном порядке означает, что функция знает, где находится ее первый параметр, сообщающий число и тип оставшихся параметров.
  • Подпрограмма после этого может увеличить значение SP, например для распределения места в стеке для локальных переменных, которые после этого будут доступны по отрицательным смещениям от BP.
  • Подпрограмма, если она возвращает значение вызывающей программе, должна передавать это значение в AL, AX или DX:AX в зависимости от размера последнего. Результаты, являющиеся числами с плавающей точкой иногда (в зависимости от компилятора) возвращаются в регистре сопроцессора ST0.
  • Как только подпрограмма завершит свою работу, она восстанавливает значение SP из BP (если она распределяла локальное пространство стека), затем изымает из стека предыдущее значение BP и в зависимости от модели памяти возвращается в вызвавшую программу через RETN или RETF.
  • Когда вызывающая программа возвратит себе управление, параметры функции остаются в стеке, поэтому для удаления их к SP обычно прибавляется непосредственная константа (вместо выполнения серии медленных инструкций POP). Поэтому если функция случайно (например, из-за несоответствия прототипов) будет вызвана с неверным числом параметров, стек при возврате останется в осмысленном состоянии, т.к. вызвавшая программа, которая знает, сколько параметров она поместила в стек, удалит их.

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

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

          global _myfunc 
_myfunc:  push bp 
          mov bp,sp 
          sub sp,0x40            ; 64 байта локального пространства стека 
          mov bx,[bp+4]          ; первый параметр функции 
          ; некоторый код
          mov sp,bp              ; отмена "sub sp,0x40" выше
          pop bp 
          ret

Для больших моделей памяти вы должны заменить RET в данной функции на RETF и брать первый параметр не из [BP+4], а из [BP+6]. Естественно, если один из параметров будет указателем, смещения следующих параметров будут зависеть от модели памяти: дальние указатели занимают в стеке 4 байта, в то время как короткие — два.

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

          extern _printf 
          ; здесь идет супер-пупер-прога... 
          push word [myint]      ; целое значение - параметр 
          push word mystring     ; указатель на мой сегмент данных 
          call _printf 
          add sp,byte 4          ; 'byte' экономит размер 
          ; затем следует сегмент данных... 
          segment _DATA 
myint     dw 1234 
mystring  db 'Это число -> %d <- должно быть 1234, фуфел!',10,0

Этот ассемблерный код, использующий модель памяти small, эквивалентен С-коду

    int myint = 1234; 
    printf("Это число -> %d <- должно быть 1234, фуфел!\n", myint);

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

          push word [myint] 
          push word seg mystring ; Теперь сохраняем в стеке сегмент, и... 
          push word mystring     ; ... смещение "mystring" 
          call far _printf 
          add sp,byte 6

Целое число по прежнему будет занимать в стеке одно слово, так как большая модель памяти не влияет на размер типа данных int. В то же время первый аргумент для printf (помещаемый в стек последним), является указателем и поэтому состоит из двух частей — сегмента и смещения. Сегмент должен сохраняться в памяти вторым, поэтому в стек он помещается первым. (Конечно PUSH DS будет иметь более короткую инструкцию, чем PUSH WORD SEG mystring, если DS инициализирован так, как подразумевается в приведенном примере). Затем следует дальний вызов call far, как это определено для больших моделей памяти; после возврата из подпрограммы регистр стека увеличивается на 6 (а не на 4) с целью коррекции на размер дополнительного слова, помещенного туда ранее.

7.4.4 Доступ к элементам данных

Для получения доступа к переменным С или объявления переменных, к которым С в свою очередь может обратиться, вы должны всего лишь объявить имена как EXTERN или GLOBAL соответственно. (Имена требуют лидирующего знака подчеркивания, см. параграф 7.4.1.) Таким образом, объявленная в С переменная int i может быть доступна из ассемблера как

          extern _i 
          mov ax,[_i]

Чтобы объявить собственную целую переменную, к которой С-программа сможет обратиться как extern int j, вы должны сделать следующее (убедитесь, что эта переменная находится в сегменте _DATA):

          global _j 
_j        dw 0

Для получения доступа к С-массиву вам нужно знать размер компонетов последнего. Например, переменные типа int имеют размер два байта (слово), поэтому если С-программа объявляет массив как int a[10], вы можете обратиться к элементу a[3] при помощи инструкции mov ax,[_a+6]. (Байтовое смещение 6 получено путем умножения индекса 3 требуемого элемента на размер элементов массива 2). Размеры базовых типов С для 16-битных компиляторов: 1 для char, 2 для short и int, 4 для long и float, 8 для double.

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

Чтобы правильно использовать структуры С, вы должны изучить руководство по вашему С-компилятору, чтобы знать, как он организует структуры данных. NASM не делает специального выравнивания для членов его собственных структур STRUC, поэтому если С-компилятор делает это, вы можете задать такое смещение самостоятельно. Обычно вы можете предположить, что структура наподобие

struct { 
    char c; 
    int i; 
} foo;

будет иметь длину 4 байта, а не 3, так как поле int выравнивается по двухбайтной границе. Однако такие особенности имеют тенденцию конфигурироваться компилятором С при помощи ключей командной строки, либо директив #pragma, поэтому вы должны выяснить, как именно это делает ваш компилятор.

7.4.5 c16.mac: Макросы для 16-битного C-интерфейса

В подкаталоге misc архива NASM имеется файл макросов c16.mac. В нем определены три макроса: proc, arg и endproc. Они предназначены для использования в определениях С-подобных процедур и автоматизируют большинство работ по слежению за соблюдением соглашения о вызовах.

Ниже приведен пример ассемблерной функции, использующей этот набор макросов:

          proc _nearproc 
%$i       arg 
%$j       arg 
          mov ax,[bp + %$i] 
          mov bx,[bp + %$j] 
          add ax,[bx] 
          endproc

Здесь определяется процедура _nearproc, принимающая два аргумента, первый (i) — это целое и второй (j) — указатель на целое. Процедура возвращает i + *j.

Заметьте, что макрос arg при его разворачивании содержит в первой строке EQU, которая в результате определяет %$i как смещение от BP. При этом используются контекстно-локальные переменные (локальные к контексту, сохраняемому в контекстном стеке макросом proc и удаляемому оттуда макросом endproc), поэтому в других процедурах может быть использовано то же самое имя аргумента. Конечно, вы можете этого не делать.

По умолчанию представленный набор макросов создает код для ближних функций (модели памяти tiny, small и compact). Чтобы генерировать код для дальних функций (модели medium, large и huge), вы должны определить %define FARCODE. Данное определение изменяет тип возвращаемой endproc инструкции, а также начальную точку смещения аргументов. Набор макросов совершенно не зависит от того, являются ли указатели данных дальними или нет.

Макрос arg может принимать дополнительный параметр, представляющий собой размер аргумента. Если размер не задан, по умолчанию принимается 2, т.к. большинство параметров функций вероятно будут иметь тип int.

Эквивалент представленной выше функции для модели памяти large будет таким:

%define FARCODE 
          proc _farproc 
%$i       arg 
%$j       arg 4 
          mov ax,[bp + %$i] 
          mov bx,[bp + %$j] 
          mov es,[bp + %$j + 2] 
          add ax,[bx] 
          endproc

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

7.5 Взаимодействие с программами Borland Pascal

Взаимодействие с программами на Паскале в концепции схоже по взаимодействию с 16-битными С-программами, однако имеются следующие различия:

  • Требуемые для взаимодействия с С-программами ведущие символы подчеркивания в Паскале не нужны.
  • Модель памяти всегда большая: функции и указатели данных являются дальними, но длина отдельного элемента данных не должна превышать 64К. (На самом деле некоторые функции остаются ближними, но эти функции локализованы в модуле Паскаля и никогда не вызываются извне. Все функции ассемблера, осуществляющие Паскаль-вызовы, а также все функции Паскаля, обращающиеся к ассемблерному коду, должны быть дальними). В то же время все объявленные в Паскаль-программе статические данные помещаются в сегменте данных по умолчанию, т.е. его сегментный адрес при передаче управления вашему ассемблерному коду будет содержаться в DS. В сегменте данных не располагаются только локальные переменные (они находятся в сегменте стека) и переменные, память для которых выделяется динамически. Однако все указатели данных являются дальними.
  • Соглашение о вызовах функций отличается от С — описание приведено далее.
  • Некоторые типы данных, такие как строки, хранятся по другому.
  • Имеются ограничения на имена сегментов, которые вам разрешено использовать — Borland Pascal будет игнорировать код или данные, объявленные в сегменте с неподходящим именем. Эти ограничения также описаны ниже.

7.5.1 Соглашение о вызовах в Pascal

Ниже описываются соглашения о вызовах в 16-битных Паскаль-программах.

  • Вызывающая программа помещает параметры функции в стек один за другим в обычном порядке (слева-направо, так что первый аргумент функции помещается также первым).
  • Вызывающая программа для передачи управления подпрограмме выполняет дальнюю инструкцию CALL.
  • Подпрограмма получает управление и обычно (несмотря на то, что это не требуется в функциях, которым не нужен доступ к своим параметрам) начинается с сохранения SP в BP с целью дальнейшего использования BP в качестве базы указателя для нахождения параметров в стеке. Однако частью соглашений о вызовах является сохранение содержимого BP любой функцией. Следовательно подпрограмма, если она использует BP как указатель кадра, должна предварительно поместить в стек его содержимое.
  • Подпрограмма может получить доступ к своим параметрам относительно BP. Слово [BP] адресует в стеке предыдущее значение BP. Следующее слово, [BP+2], адресует смещение адреса возврата, а [BP+4] — сегмент адреса возврата. Параметры начинаются со смещения [BP+6]. По этому смещению от BP доступен самый "правый" параметр функции, так как в стек он был помещен последним; следующие параметры идут соответственно по более большим смещениям.
  • В процессе выполнения подпрограмма может увеличить значение SP с целью выделения в стеке места под свои локальные переменные. Эти переменные будут доступны по отрицательным от BP смещениям.
  • Подпрограмма должна передавать результат выполнения назад в вызвавшую программу через AL, AX или DX:AX, в зависимости от размера значения. Результаты в виде чисел с плавающей точкой возвращаются через регистр ST0. Результаты типа Real (собственные типы Борланда — числа с плавающей точкой, прямо не обрабатываемые в FPU) возвращаются в группе DX:BX:AX. Чтобы возвратить результат типа String, вызывающая программа перед помещением в стек параметров помещает туда указатель на временную строку, а подпрограмма по этому адресу возвращает строковое значение. Указатель не является параметром и не должен удаляться из стека инструкцией RETF.
  • Когда подпрограмма заканчивает свою работу, она восстанавливает содержимое SP из BP, достает из стека предыдущее значение BP и возвращается через RETF. Здесь используется форма RETF с непосредственным операндом, представляющим собой число байт, снимаемых с вершины стека в качестве параметров. Снятие параметров со стека — это побочный эффект инструкции возврата.
  • Дополнительных действий от вызвавшей программы не требуется, так как параметры функции при получении ей управления уже удалены из стека.

Исходя из вышесказанного, вы должны определить функцию в стиле Паскаль, принимающую два аргумента типа Integer, следующим образом:

          global myfunc 
myfunc:   push bp 
          mov bp,sp 
          sub sp,0x40            ; резервируется 64 байта в стеке 
          mov bx,[bp+8]          ; первый аргумент функции 
          mov bx,[bp+6]          ; второй аргумент функции 
          ; код, который наверное что-то делает
          mov sp,bp              ; отмена "sub sp,0x40" выше 
          pop bp 
          retf 4                 ; общий размер аргументов 4

С другой стороны, для вызова Паскаль-функции из вашего ассемблерного кода, вы должны сделать что-то вроде следующего:

          extern SomeFunc 
          ; тут идет какой-то код...
          push word seg mystring ; Теперь в стек помещается сегмент и...
          push word mystring     ; ... смещение строки "mystring"
          push word [myint]      ; одна из переменных 
          call far SomeFunc

Этот код эквивалентен следующим строкам на Паскале:

procedure SomeFunc(String: PChar; Int: Integer); 
    SomeFunc(@mystring, myint);

7.5.2 Ограничение имен сегментов в Borland Pascal

Так как внутренний формат модуля Borland Pascal полностью отличается от OBJ, при компоновке модуля с реальным OBJ файлом выполняется очень поверхностная работа по чтению и пониманию различной информации из последнего. Вследствие этого объектные файлы, предназначенные для компоновки с Паскаль-программами, должны удовлетворять нескольким ограничениям:

  • Процедуры и функции должны находиться в сегменте с именем CODE, CSEG, или заканчивающимся на _TEXT.
  • Инициализированные данные должны находиться в сегменте с именем CONST или заканчивающимся на _DATA.
  • Неинициализированные данные должны находиться в сегменте с именем DATA, DSEG, или заканчивающимся на _BSS.
  • Любые другие сегменты, имеющиеся в объектном файле, полностью игнорируются. Директивы GROUP и атрибуты сегментов также игнорируются.

7.5.3 Использование c16.mac с Pascal-программами

Пакет макросов c16.mac, описанный в параграфе 7.4.5, может быть также использован для облегчения написания функций, вызываемых из программ на Паскале. Для этого вам нужно определить %define PASCAL. Данное определение делает все функции дальними (это подразумевает FARCODE), а также генерирует инструкции возврата из подпрограммы в форме, имеющей операнд.

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

%define PASCAL 
          proc _pascalproc 
%$j       arg 4 
%$i       arg 
          mov ax,[bp + %$i] 
          mov bx,[bp + %$j] 
          mov es,[bp + %$j + 2] 
          add ax,[bx] 
          endproc

Концептуально здесь определяется та же самая подпрограмма, что и в параграфе 7.4.5: функция принимает два аргумента, целое число и указатель на целое и возвращает сумму целого и содержимого, на которое ссылается указатель. Отличие между этим кодом и версией С для большой модели памяти состоит в том, что вместо FARCODE определяется PASCAL, а аргументы объявляются в обратном порядке.

Перейти на содержание