Связь ассемблера с языками высокого уровня
На протяжении всего предыдущего материала мы неоднократно подчеркивали сильные и слабые стороны языка ассемблера как языка программирования. Писать на нем достаточно объемные программы утомительно. И всегда ли это нужно? Конечно, не всегда. Если программа не предназначена для решения каких-то системных задач, требующих максимально эффективного использования ресурсов компьютера, если к ней не предъявляются сверхжесткие требования по размеру и времени работы, если вы не «фанат» ассемблера — то, на мой взгляд, следует подумать о выборе одного из языков высокого уровня. Существует и третий, компромиссный путь — комбинирование программ на языке высокого уровня с кодом на ассемблере. Такой способ обычно используют в том случае, если в вашей программе есть участки, которые либо невозможно реализовать без использования ассемблера, либо его применение может значительно повысить эффективность работы программы.
Большинство компиляторов учитывают возможность комбинирования их «родного» кода с ассемблером. Как именно? Это зависит от конкретного компилятора языка высокого уровня, поэтому нет смысла рассматривать данный вопрос абстрактно. Учитывая, что большинство программистов работают или, по крайней мере, владеют основами программирования для языков С и Pascal, дальнейшее обсуждение будет вестись для компиляторов этих языков: Borland Pascal 7.0 и Visual C++ 4.0. Но не стоит думать, что если вы используете компиляторы других фирм или других версий, то информация, приведенная ниже, вам не понадобится. Принципиальные моменты останутся теми же. Единственное, что нужно будет сделать, — это уточнить в документации некоторые опции и, возможно, особенности организации связи с кодом на ассемблере. отразим наиболее принципиальные моменты связи программ на языках Pascal и С с ассемблером.
Вначале отметим общие моменты, актуальные как для языка С, так и для Pascal. Затем на примерах конкретных программ обсудим моменты, специфичные для каждого из этих языков.
Существуют следующие формы комбинирования программ на языках высокого уровня с ассемблером:
Использование операторов типа inline и ассемблерных вставок.( Под операторами типа inline здесь понимаются средства языка высокого уровня, подобные оператору inline() языка Pascal. В скобках указывается строка машинных кодов. Для получения такой строки целесообразно написать нужный фрагмент на ассемблере, скомпилировать исполняемый модуль, а затем запустить отладчик. В окне CPU отладчика вы увидите машинные коды ваших инструкций; их нужно переписать и вставить в скобки оператора inline. При этом нужно учитывать синтаксис языка). Эта форма сильно зависит от синтаксиса языка высокого уровня и конкретного компилятора. Она предполагает, что ассемблерные коды в виде команд ассемблера или прямо в машинных командах вставляются в текст программы на языке высокого уровня. Компилятор языка распознает их как команды ассемблера (машинные коды) и без изменений включает в формируемый им объектный код. Эта форма удобна, если надо вставить небольшой фрагмент.
o Использование внешних процедур и функций. Это более универсальная форма комбинирования. У нее есть ряд преимуществ:
• написание и отладку программ можно производить независимо;
• написанные подпрограммы можно использовать в других проектах;
• облегчаются модификация и сопровождение подпрограмм в течении жизненного цикла проекта. К примеру, если ваша процедура на ассемблере производит работу с некоторым внешним устройством, то при смене устройства вам нет необходимости перекраивать весь проект — достаточно заменить только работающую с ним процедуру, оставив неизменным интерфейс программы на языке высокого уровня с ассемблером.
Так как использование операторов inline и ассемблерных вставок сильно зависит от синтаксиса конкретного языка, то мы рассматривать их не будем, а уделим основное внимание связи через внешние процедуры и функции. Как вы понимаете, здесь возможны два вида связи — программа на языке высокого уровня вызывает процедуру на ассемблере и наоборот. Здесь мы также ограничимся рассмотрением связи только в одну сторону, когда программа на языке высокого уровня вызывает процедуру на ассемблере. Это наиболее часто используемый вид связи.
Вспомним синтаксис директивы ргос:
имя_процедуры PR0C [[модификатор_языка]язык] [расстояние]
Один из операндов — язык. Он служит для того, чтобы компилятор мог правильно организовать интерфейс (связь данных) между процедурой на ассемблере и программой на языке высокого уровня. Необходимость такого указания возникает вследствие того, что способы передачи аргументов при вызове процедур различны для разных языков высокого уровня.
TASM поддерживает несколько значений операнда язык. В табл. 14.1 для некоторых из этих значений приведены характерные особенности передачи аргументов и соглашения о том, какая процедура очищает стек — вызывающая или вызываемая. Под направлением передачи аргументов понимается порядок, в котором аргументы включаются в стек, по сравнению с порядком их следования в вызове процедуры. Так, к примеру, для языка Pascal характерен прямой порядок включения аргументов в стек: первым в стек записывается первый передаваемый аргумент из оператора вызова процедуры, вторым — второй аргумент и т. д. На вершине стека после записи всех передаваемых аргументов оказывается последний аргумент.
Для языка С, наоборот, характерен обратный порядок передачи аргументов. В соответствии с ним в стек сначала включается последний аргумент из оператора вызова процедуры (или функции), затем предпоследний и т. д. В конечном итоге на вершине стека оказывается первый аргумент. Что же касается очистки стека, то понятно, что должны быть определенные договоренности об этом. В языке Pascal эту операцию всегда совершает вызываемая процедура, в языке С — вызывающая.
При разработке программы с использованием только одного языка высокого уровня об этом задумываться не имеет смысла, но если мы собираемся связывать несколько разноязыковых модулей, то эти соглашения нужно иметь в виду.
Таблица 14.1. Передача аргументов в языках высокого уровня
Вспомним действия, которые мы делали для того, чтобы настроиться на аргументы в стеке. Теперь, указывая язык, с программой на котором будет осуществляться связь, все действия по настройке стека будут производиться компилятором. При этом в текст процедуры он включит дополнительные команды входа в процедуру (пролог) и выхода из нее (эпилог). При этом код эпилога может повторяться несколько раз — перед каждой командой ret. Для значения NOLANGUAGE и по умолчанию коды пролога и эпилога не создаются.
Для пояснения последних замечаний рассмотрим на конкретных примерах организацию связей между модулями на ассемблере и модулями наиболее популярных языков высокого уровня — С и Pascal.
Связь Pascal — асемблер
Организацию такой связи рассмотрим на примере следующей задачи: разработаем программу на языке Pascal, которая выводит символ заданное количество раз, начиная с определенной позиции на экране (листинги 14.12 и 14.13). Все числовые аргументы определяются в программе на Pascal. Вывод символа осуществляет процедура ассемблера. Очевидно, что основная проблема в этой задаче — организация взаимодействия модулей на Pascal и ассемблере.
Листинг 14.12. Взаимодействие Pascal—ассемблер (модуль на Pascal)
Листинг 14.13. Взаимодействие Pascal—ассемблер (модуль на ассемблере)
Типовая схема организации такой связи следующая:
o Написать процедуру на ассемблере дальнего (far) или ближнего типа (near). Назовем ее для примера asmproc. В программе на языке ассемблера (назовем ее prg14_13. asm), в которую входит процедура asmproc, необходимо объявить имя этой процедуры внешним с помощью директивы PUBLIC:
PUBLIC asmproc
Для того чтобы процедура на ассемблере при компоновке с программой на Pascal воспринималась компилятором Borland Pascal 7.0 как far или near, недостаточно будет просто объявить ее таковой в директиве ргос (строка 11 листинга 14.13). Кроме того, вам нужно включить или выключить опцию компилятора, доступную через меню интегрированной среды: Options |Compiler | Force far calls. Установка данной опции заставляет компилятор генерировать дальние вызовы подпрограмм. Альтернатива данной опции — ключ в программе {$F+} или {$F-} (соответственно, включено или выключено). Это — локальные ключи, то есть в исходном тексте программы на Pascal их может быть несколько, и они могут, чередуясь друг с другом, поочередно менять форму генерируемых адресов перехода — для одних подпрограмм дальние вызовы, для других — ближние.
o Произвести компиляцию программы prg14_13. asm с целью устранения синтаксических ошибок и получения объектного модуля программы prg14_13.obj:
tasm /zi prg14_13,,,
o В программе на Pascal prg14_12. pas, которая будет вызывать внешнюю процедуру на ассемблере, следует вставить директиву компилятора
{$L \путь\ргд_14_12. obj}. Эта директива заставит компилятор в процессе ком-пиляции программы prg14_12.pas загрузить с диска объектный модуль программы prg14_12.obj. В программе prg14_12.pas необходимо объявить процедуру asmproc как внешнюю. В итоге последние два объявления в программе на Pascal будут выглядеть так:
{$L my_asm}
procedure asmproc(ch:char;kol,x,y:integer); external;
o если вы собираетесь исследовать в отладчике работу программы, то необходимо потребовать, чтобы компилятор включил отладочную информацию в генерируемый им исполняемый модуль. Для этого есть две возможности. Первая заключается в использовании глобального ключа {$D+}. Этот ключ должен быть установлен сразу после заголовка программы на Pascal. Вторая, альтернативная возможность заключается в установке опции компилятора: Options|Compiler|Debug Information;
o выполнить компиляцию программы на Pascal. Для компиляции удобно использовать интегрированную среду. Для изучения особенностей связки Pascal — ассемблер удобно прямо в интегрированной среде перейти к работе в отладчике через меню Tools|Turbo Debugger (или клавишами Shift-F4). Будет загружен отладчик. Его среда вам хорошо знакома; в данном случае в окне Module вы увидите текст программы на Pascal. Нажимая клавишу F7, вы в пошаговом режиме будете исполнять программу на Pascal. Когда очередь дойдет до вызова процедуры на ассемблере, отладчик откроет окно с текстом программы на ассемблере. Но наш совет вам — не ждать этого момента, так как вы уже пропустили некоторые интересные вещи. Дело в том, что отладчик скрывает от вас момент перехода из программы на Pascal в процедуру на ассемблере. Поэтому лучше всего исполнять программу при открытом окне CPU отладчика. И тогда вы станете свидетелями тех процессов, которые мы будем обсуждать ниже.
Если бы взаимодействие программ ограничивалось только передачей и возвратом управления, то на этом обсуждение можно было бы и закончить. Но дело значительно усложняется, когда требуется передать аргументы (в случае процедуры) или передать аргументы и возвратить результат (в случае функции). Рассмотрим процессы, которые при этом происходят.
Передача аргументов при связи модулей на разных языках всегда производится через стек. Компилятор Pascal генерирует соответствующие команды при обработке вызова процедуры ассемблера. Это как раз те команды, которые отладчик пытался скрыть от нас. Они записывают в стек аргументы и генерируют команду call для вызова процедуры ассемблера. Чтобы убедиться в этом, просмотрите исполнительный код программы в окне CPU отладчика. После обработки вызова процедуры и в момент передачи управления процедуре asmproc содержимое стека будет таким, как показано на рис. 14.1, а. Для доступа к этим аргументам можно использовать различные методы; наиболее удобный из них — использование регистра bр.
Рис. 14.1. Изменение содержимого стека при передаче управления Pascal—ассемблер
Регистр bр, как уже отмечалось, специально предназначен для организации произвольного доступа к стеку. Когда мы рассматривали связь ассемблерных модулей, то говорили о необходимости добавления в текст вызываемого модуля фрагментов, настраивающих его на передаваемые ему аргументы. При организации связи разноязыковых модулей также нужно вставлять подобные дополнительные фрагменты кода. Они, кроме всего прочего, позволят учесть особенности конкретного языка. Фрагмент, вставляемый в самое начало вызываемого модуля, называется прологом модуля (процедуры). Фрагмент, вставляемый перед командами передачи управления вызывающему модулю, называется эпилогом модуля (процедуры). Его назначение — восстановление состояния вычислительной среды на момент вызова данного модуля.
Рассмотрим действия, выполняемые кодами пролога и эпилога при организации связи Pascal—ассемблер.
Действия, выполняемые кодом пролога:
1. Сохранить значение bр в стеке. Это делается с целью запоминания контекста вызывающего модуля. Стек при этом будет выглядеть как на рис. 14.1, б.
2. Записать содержимое sp в bр. Тем самым Ьр теперь тоже будет указывать на вершину стека (рис. 14.1, в).
После написания кода пролога все обращения к аргументам в стеке можно организовывать относительно содержимого регистра bр. Из рис. 14.1, в видно, что для обращения к верхнему и последующим аргументам в стеке содержимое bр необходимо откорректировать. Нетрудно посчитать, что величина корректировки будет отличаться для процедур дальнего (far) и ближнего (near) типов. Причина понятна: при вызове пеаг-процедуры, в зависимости от установленного режима адресации — use16 или use32 — в стек записывается 2/4 байта в качестве адреса возврата (содержимое ip/eip), а при вызове far-процедуры в стек записывается 4/8 байт (содержимое ip/eip и cs).
Таким образом, коды пролога для near- и far-процедур соответственно будут выглядеть следующим образом:
Далее доступ к переданным в стеке данным осуществляется, как показано в листинге 14.7.
Как видите, все достаточно просто. Но если мы вдруг решили изменить тип нашей процедуры ассемблера с far на near или наоборот, то нужно явно изменить и код пролога. Это не совсем удобно. TASM предоставляет выход в виде директивы ARG, которая служит для работы с аргументами процедуры. Формат директивы ARG следующий (рис. 14.2).
Рис. 14.2. Синтаксис директивы ARG
Несколько слов об обозначениях на рис. 14.2:
имя — идентификатор переменной, который будет использоваться в процедуре на ассемблере для доступа к соответствующей переменной в стеке;
тип — определяет тип данных аргумента (по умолчанию word для use16 и dword для use32);
значение_1 — определяет количество аргументов с данным именем. Место в стеке для них будет определено, исходя из расчета: значение_1 * значение_2 * (размер_типа). По умолчанию значение_1 = 1;
значение_2 — определяет, сколько элементов данного типа задает данный аргумент. По умолчанию его значение равно 1, но для типа byte значе- ние_2 = 2, так как стековые команды не работают с отдельными байтами. Хотя, если явно задать значение_2 = 1, то транслятор действительно будет считать, что в ячейку стека помещен один байт;
идентификатор — имя константы, значение которой присваивает транслятор. Об идентификаторе мы подробно поговорим чуть ниже.
Таким образом, директива ARG определяет аргументы, передаваемые в процедуру. Ее применение позволяет обращаться к аргументам по их именам, а не смещениям относительно содержимого bр. К примеру, если в начале рассматриваемой нами процедуры на ассемблере asmproc задать директиву ARG в виде arg kol:word,у:word,x:word,chr:byte, то к аргументам процедуры можно будет обращаться по их именам, без подсчета смещений. Ассемблер сам выполнит всю необходимую работу. В этом можно убедиться, запустив программу в отладчике. Обратите внимание: порядок следования аргументов в директиве arg является обратным порядку их следования в описании процедуры (строка procedure asmproc(ch:char;x,y,kol:integer); external; в программе на Pascal). Процедура asmproc с использованием директивы arg представлена в листинге 14.14.
После того как решена проблема передачи аргументов в процедуру и выполнены все необходимые действия, возникает очередной вопрос — как правильно возвратить управление? При возврате управления в программу на Pascal нужно помнить, что соглашения этого языка требуют, чтобы вызываемые процедуры самостоятельно очищали за собой стек. Программа на ассемблере также должна удовлетворять этому требованию и заботиться об очистке стека перед своим завершением. Для этого необходимо составить эпилог.
Действия, выполняемые кодом эпилога для связи Pascal—ассемблер:
1. Восстановить сохраненный в стеке регистр bр.
2. Удалить из стека переданные процедуре аргументы.
Для удаления из стека аргументов можно использовать различные способы:
o явно скорректировать значение sp, переместив указатель стека на необходимое количество байт в положительную сторону. Это — не универсальный способ, к тому же он чреват ошибками, особенно при частых модификациях программы;
o использовать в директиве arg после записи последнего аргумента операнд, состоящий из символа равенства «=» и идентификатора, указанного за ним в следующей синтаксической конструкции: =идентификатор. В этом случае TASM при обработке директивы arg подсчитает количество байт, занятых всеми аргументами, и присвоит их значение идентификатору. В нашем случае директиву arg можно определить так: arg ch: byte;х:word;у:word; kol:word=a_size. TASM после обработки данной директивы присвоит имени a_size значение 8 (байт). Это имя впоследствии нужно будет указать в качестве операнда команды ret:
ret a_size
Листинг 14.14. Использование директивы arg
Есть еще одна возможность организации данных Pascal—ассемблер — использование операндов директивы model. Вcпомним, что она позволяет задать модель памяти и учесть соглашения языков высокого уровня о вызове процедур. Для связи Pascal — ассемблер ее можно задавать в виде
MODEL large,pascal
Задание в таком виде директивы model позволяет:
o описать аргументы процедуры непосредственно в директиве ргос:
asmproc ргос near ch:byte,х:word,y:word, kol:word
o автоматически сгенерировать код пролога и эпилога в процедуре на ассемблере;
o для доступа к аргументам, объявленным в ргос, использовать их имена. В этом отношении данный вариант является аналогом предыдущего варианта с директивой arg.
Листинг 14.15 демонстрирует, как отразились особенности данного варианта на тексте процедуры ассемблера. Обратите внимание, что пролога уже нет, так как он формируется транслятором автоматически; вместо эпилога обязательно нужно задавать только ret без операндов. Интересно изучить текст листинга, который получится в результате трансляции листинга 14.15. В нем видны сформированные транслятором коды пролога и эпилога. Кроме того, транслятор заместил команду ret без операндов командой ret 0008, которая, в соответствии с требованиями к взаимодействию с программами на Pascal, удалит из стека аргументы, переданные вызываемой процедуре.
Листинг 14.15. Использование директивы MODEL
Листинг 14.16. Результат трансляции листинга 14.15
Таким образом, можно считать, что мы разобрались со стандартными способами вызова ассемблерных процедур из программ на Pascal и передачи им аргументов. Эти способы будут работать всегда, но компилятор может предоставлять и болем удобные средства. Их мы рассматривать не будем, так как они будут сводиться, в конечном счете, к разобранной нами процедуре.
Остались открытыми два вопроса:
1.Как быть с передачей данных остальных типов Pascal — ведь мы рассмотрели только данные размером в байт и слово?
2.Как возвратить значение в программу на Pascal?
Что касается ответа на первый вопрос, то необходимо вспомнить о том, что в языке Pascal существует два способа передачи аргументов в процедуру: по ссылке и по значению.
Тип аргументов, передаваемых по ссылке, совпадает с типом ассемблера dword и с типом pointer в Pascal. По сути, это указатель из четырех байт на некоторый объект. Структура указателя обычная: два младших байта — смещение, два старших байта — значение сегментной составляющей адреса. С помощью такого указателя в программу на ассемблере передаются адреса следующих объектов:
o всех аргументов, объявленные при описании в программе на Pascal как var, независимо от их типа;
o аргументов pointer и longint;
o строк string;
o множеств;
o массивов и записей, имеющих размер более четырех байт.
Аргументы по значению передаются следующим образом:
o для типов char и byte — как байт;
o для типа boolean — как байт со значением 0 или 1;
o для перечисляемых типов со значением 0...255 — как байт; более 255 — как два байта;
o для типов integer и word — как два байта (слово);
o для типа real — как шесть байт (три слова);
o массивы и записи, длина которых не превышает четырех байт, передаются «как есть».
Заметим, что аргументы таких типов, как single, double, extended и comp, передаются через стек сопроцессора.
Что касается ответа на второй вопрос, то рассмотрим его на конкретном примере листингов 14.17 и 14.18. Помните, что мы рассматриваем вызов из программы на Pascal внешней процедуры на ассемблере. Понятно, что вызов ради вызова вряд ли нужен — вызываемая процедура должна иметь возможность вернуть данные в вызывающую программу. Поэтому такую вызываемую процедуру правильнее рассматривать как функцию. В связке Pascal—ассемблер для того чтобы возвратить результат, процедура на ассемблере должна поместить его значение в строго определенное место (табл. 14.2).
Таблица 14.2. Возврат результата из процедуры на ассемблере
в программу на Pascal
В листинге 14.17 приведен текст вызывающего модуля на Pascal, а в листинге 14.18 — код вызываемого модуля на ассемблере. Программа на Pascal инициализирует две переменные valuel и value2, после чего вызывает функцию на ассемблере AddAsm для их сложения. Результат возвращается в программу на Pascal и присваивается переменной rez.
Листинг 14.17. Вызывающая программа на Pascal
Листинг 14.18. Вызываемая процедура на ассемблере
Здесь была использована еще одна возможность доступа к разделяемым данным — использование сегментов типа public. Совместное использование сегментов данных стало возможным благодаря тому, что компилятор Pascal создает внутреннее представление программы в виде сегментов, как и положено для программ, выполняющихся на микропроцессоре Intel. Сегмент данных в этом представлении тоже имеет название data, и директива segment для него выглядит так: data segment word public и т. д.
Связь С—асемблер
Общие принципы организации такой связи напоминают только что рассмотренное соединение Pascal и ассемблера. Поэтому коротко обсудим отличия на примере конкретных программ. Но прежде отметим, что язык C++ предоставляет дополнительные возможности связи программы с ассемблером, одновременно поддерживая традиционную организацию связи. Поэтому рассмотрим связь с ассемблером в стиле С как стандартную. При необходимости читатель, зная основы подобной связи, без труда разберется с нюансами дополнительных возможностей связи в стиле C++.
Нас по-прежнему интересуют три вопроса: как передать аргументы в процедуру на ассемблере, как к ним обратиться в процедуре на ассемблере и как возвратить результат?
Вначале отметим, что всегда нужно сохранять — и перед выходом из процедуры восстанавливать — содержимое регистров bp, sp, cs, ds и ss. Это делается перед вызовом процедуры. Остальные регистры нужно сохранять по необходимости, но хорошим тоном является сохранение и последующее восстановление всех регистров, которые подвергаются изменению внутри процедуры.
Передача аргументов в процедуру на ассемблере из программы на С осуществляется также через стек (рис. 14.3), но порядок их размещения в стеке является обратным тому, что рассмотрен выше для связи Pascal — ассемблер. В качестве примера используем ту же задачу. После передачи управления ближнего типа процедуре на ассемблере стек имеет вид как на рис. 14.3, а.
Рис. 14.3. Изменение содержимого стека при передаче управления С—ассемблер
Процедуры на ассемблере получают доступ к аргументам, переданным в стеке, посредством регистра Ьр. Принцип доступа — тот же, что и выше (рис. 14.3, б). Прежде всего в начало процедуры ассемблера необходимо вставить код пролога:
push bр
mov bp,sp
После этого доступ к аргументам в стеке осуществляется по смещению относительно содержимого bр, например:
При организации связи С—ассемблер также можно использовать директиву arg. Это избавит нас от необходимости подсчитывать смещения в стеке для доступа к аргументам и позволит обращаться к ним просто по именам.
Чтобы не повторяться, рассмотрим, как изменятся вызываемый и вызывающий модули (см. листинги 14.19 и 14.20) для связи С—ассемблер по сравнению с листингами 14.17 и 14.18.
Листинг 14.19. Вызывающий модуль на С (C++)
Листинг 14.20. Вызываемая процедура на ассемблере
Что касается передачи аргументов С—ассемблер, то здесь, как видите, все довольно прозрачно. Как видите, в листинге 14.20 мы используем директиву MODEL с операндом С и директиву PROC с указанием языка С. Этим мы доверяем компилятору самому сформировать коды пролога и эпилога, а также
организовать обращение к переменным в стеке по их именам. Но при использовании конкретных программных средств организация такой связи выглядит намного проблематичней. Не в последнюю очередь это связано с тем, что компиляторы языка C/C++ разрабатывают множество фирм — в отличие от Pascal, компилятор для которого выпускает практически одна фирма Borland1. Это обстоятельство, на мой взгляд, — основная причина сложности связи С- ассемблер, так как каждая фирма реализует ее по-своему (хотя суть и остается практически неизменной). Поэтому нет смысла рассматривать множество частных случав. Обращайтесь к документации на ваш компилятор C/C++. Опыт показывает, что достаточно хороший эффект дает применение ассемблерных вставок в программу на C/C++. Как правило, компиляторы позволяют связывать модули на C/C++ и ассемблере с использованием средств командной строки. Так как этот процесс достаточно стандартизован, есть смысл его рассмотреть. В качестве примера выберем компилятор Visual C++ 4.0 фирмы Microsoft. Скорее всего, для компиляторов других фирм изменятся только имена файлов транслятора и компоновщика. Типовая последовательность шагов выглядит примерно так:
o Составить текст программы на C++ (листинг 14.19). В этой программе — объявить процедуру asmproc внешней:
o Выполнить трансляцию модуля C++ и получить объектный модуль:
cl prg14_19.cpp
o Составить текст процедуры на ассемблере (листинг 14.20), в которой объявить процедуру asmproc общедоступной с помощью директивы PUBLIC. Заметьте, что идентификатору asmproc предшествует символ подчеркивания — asmproc. Компилятор добавляет знак подчеркивания ко всем глобальным идентификаторам. Поэтому для того, чтобы при компоновке программа link восприняла asmproc и asmproc как один и тот же идентификатор, ко всем именам, объявляемым глобальными в программе на ассемблере директивой PUBLIC, требуется добавить знак подчеркивания.
o Выполнить трансляцию программы на ассемблере
masm /zi prg14_20,,,
o Выполнить объединение объектных модулей с помощью программы-компоновщика link из пакета Visual C++ 4.0:
link prg14_19.obj prg14_20.obj, исполняемому модулю будет присвоено имя prg14_19.ехе.
Как возвратить результат в программу на С из процедуры на ассемблере? Для этого существуют стандартные соглашения (табл. 14.3). Перед возвратом управления в программу на С в программе на ассемблере необходимо поместить результат или сформировать указатель в указанных регистрах. Для иллюстрации работы с функцией С, текст которой написан на ассемблере, рассмотрим листинги 14.21 и 14.22. В них функция, написанная на ассемблере, подсчитывает сумму элементов массива. В функцию передаются адрес массива и его длина. Результат суммы элементов массива возвращается обратно в вызывающую программу на С.
Таблица 14.3. Возврат аргументов из процедуры на ассемблере в программу
на C/C++
Листинг 14.21. Вызывающая программа на С
Листинг 14.22. Вызываемая процедура на ассемблере
Таким образом, мы рассмотрели связь модулей на языках высокого уровня с модулями на ассемблере. Главное — мы разобрались с принципами. Теперь дело за малым: взять литературу — лучше документацию для конкретного языка высокого уровня — и разбираться с тем, как в языке разработчики реализовали связь с ассемблером. Затем следует понять техническое осуществление этой связи, используя особенности и настройки конкретной среды программирования.
Подведем некоторые итоги:
ü Язык ассемблера содержит достаточно мощные средства поддержки структурного программирования. В языке ассемблера эта технология поддерживается в основном с помощью механизма процедур и частично с использованием макрокоманд.
ü 0 Гибкость интерфейса между процедурами достигается за счет разнообразия вариантов передачи аргументов в процедуру и возвращения результатов. Для этого могут использоваться регистры, общие области памяти, стек, директивы extrn и public.
ü Средства TASM поддерживают связи между языками. Ключевой момент при этом — организация обмена данными. Обмен данными между процедурами на языках высокого уровня и ассемблера производится через стек. Для доступа к аргументам используется регистр bр или (что более удобно) директива arg.
ü Можно доверить компилятору самому формировать коды пролога и эпилога, указав язык в директиве MODEL. Кроме того, указание языка позволяет задействовать символические имена аргументов, переданных процедуре в стеке, вместо прямого использования регистра bр для доступа к ним. Тем самым повышаются мобильность разрабатываемых вами программ и устойчивость их к ошибкам.
ü Для возврата результата в программу на языке высокого уровня необходимо использовать конкретные регистры. Через них можно передать как сами данные, так и указатели.