Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекції_СПр.docx
Скачиваний:
37
Добавлен:
21.08.2019
Размер:
947.09 Кб
Скачать
        1. Модульне програмування.

        2. Організація інтерфейсу.

        3. Зв’язок Асемблера з мовами високого рівня.

Навчальна мета: Засвоїти основні поняття компонування програм, написаних на мовах високого та низького рівнів.

Виховна мета: Допомогти студентам усвідомити вагому роль застосування модульного програмування, організації інтерфейсу асемблера з мовами високого рівня.

Актуальність: Донести до відома студентів, що на сьогоднішній день є актуальною задача суміщення асемблера з мовами високого рівня.

Мотивація: Мотивацією вивчати даний напрямок у курсі системного програмування є те, що суміщення асемблера з мовами високого рівня є частиною курсового проекту.

Основи

Звичайно C++ і Асемблер спільно використовують шляхом написання окремих модулів цілком на C++ або Асемблері, компіляції модулів С++ і асемблювання модулів Асемблера з наступним спільним компонуванням цих роздільно написаних модулів. Це показано на діаграмі.

Виконуваний файл виходить "сумішшю" модулів С++ і Асемблера. Цей цикл можна запустити командою:

bcc ім'я_файлу_1.cpp ім'я_файлу_2.asm

яка вказує Borland C++, що потрібно спочатку компілювати файл ім'я_файлу_1.СPP у файл ім'я_файлу_1.OBJ, а потім викликати Турбо Асемблер для ассемблирования файлу ім'я_файлу_2.asm в ім'я_файлу_2.obj, і, нарешті, викликати компоновщик TLІNK для компонування файлу ім'я_файл_1.OBJ і ім'я_файл_2.OBJ у файл ім'я_файлу.EXE.

Роздільну компіляцію корисно використовувати для програм з більшим обсягом коду на Асемблері, тому що це дозволяє використовувати всі можливості Турбо Асемблера й програмувати мовою Асемблера в чисто ассемблерном оточенні без ключових слів asm, додаткового часу на компіляцію й пов'язаними із ІС++ непродуктивними витратами при роботі з вбудованим Асемблером.

За роздільну компіляцію доводиться платити значну ціну: програміст, що працює з Асемблером, повинен вникати в усі деталі організації інтерфейсу між С++ і кодом Асемблера. У той час як при використанні вбудованого Асемблера Borland C++ сам виконує специфікацію сегментів, передачу параметрів, посилання на змінні С++ і т.д., окремо компільовані модулі Асемблера повинні все це (і навіть більше) робити самостійно.

В інтерфейсі Турбо Асемблера й Borland C++ є два основних аспекти. По-перше, різні частини коду С++ і Асемблера повинні правильно компонуватися, а функції й змінні в кожній частині коду повинні бути доступні (якщо це необхідно) в іншій частині коду. По-друге, код Асемблера повинен правильно працювати з викликами функцій, що відповідають угодам мови С++, що містить у собі доступ до переданих параметрів, повернення значень і дотримання правил збереження регістрів, яких потрібно дотримуватися у функціях С++.

Давайте тепер розглянемо правила компоновки програм Турбо Асемблера й Borland C++.

Щоб скомпонувати разом модулі Borland C++ і Турбо Асемблера, повинні бути дотримані наступні три пункти:

  • У модулях Турбо Асемблера повинні використовуватися угоди про імена, прийняті в Borland C++.

  • Borland C++ і Турбо Асемблер повинні спільно використовувати відповідні функції й імена змінних у формі, прийнятної для Borland C++.

  • Для комбінування модулів у виконувану програму потрібно використовувати утиліту- компоновщик TLІNK.

Поки ми торкнемося тільки основних моментів, що забезпечують розробку функцій Турбо Асемблера, сумісних із С++.

Важливою концепцією С++ є безпечність з погляду стикування типів компонування. Компілятор і компоновщик повинні працювати узгоджено, щоб гарантувати правильність типів переданих між функціями аргументів. Процес, називаний "коректуванням імен" ( name-manglіng), забезпечує необхідну інформацію про типи аргументів. "Коректування імені" модифікує ім'я функції таким чином, щоб воно несло інформацію про прийняті функцією аргументи.

Коли програма пишеться цілком на С++, коректування імен відбувається автоматично й прозоро для програми. Однак, коли ви пишете асемблерний модуль для наступного його компонування із програмою на С++, ви самі зобов'язані забезпечити коректування імен у модулі. Це легко зробити, написавши порожню функцію на C++ і скомпілювавши її в асемблерний код. Згенерований при цьому Borland С++ файл .ASM буде містити виправлені імена. Потім ви можете їх використовувати при написанні реального ассемблерного модуля.

Наприклад, ось фрагмент коду, що визначає чотири різні версії функції з ім'ям test:

voіd test()

{

}

voіd test( іnt )

{

}

voіd test( іnt, іnt )

{

}

voіd test( float, double )

{

}

Якщо цей код скомпілювати з параметром -S, то компілятор створює на виході файл мовою Асемблера (.ASM). От як він виглядає (скорочено):

; voіd test()

@testSqv proc near

push bp

mov bp,sp

pop bp

ret

@testSqv endp

; voіd test( іnt )

@testSqі proc near

push bp

mov bp,sp

pop bp

ret

@testSqі endp

; voіd test( іnt, іnt )

@testSqіі proc near

push bp

mov bp,sp

pop bp

ret

@testSqіі endp

; voіd test( float, double )

@testSqfd proc near

push bp

mov bp,sp

pop bp

ret

@testSqfd endp

При бажанні в ассемблерных функціях можна використовувати нескориговані імена, не намагаючись з'ясувати, як повинні виглядати правильні. Використання нескоректованих імен захистить ваші ассемблерные функції від можливих змін алгоритму в майбутньому. Borland С++ дозволяє визначати в програмах С++ стандартні імена функцій С++, як у наступному прикладі, але для цього потрібна директива extern:

extern "C" {

іnt add(іnt *a, іnt b);

}

Будь-які функції, оголошені усередині фігурних дужок, одержать імена в стилі мови С++. Нижче показані відповідні визначення в ассемблерному модулі:

publіc _add

_add proc

Оголошення ассемблерної функції в блоці extern "C" дозволить вам уникнути проблем з "відкоректованими іменами". При цьому покращиться й читабельність коду.

Моделі пам'яті й сегменти

Щоб дана функція Асемблера могла могла викликатися із С++, вона повинна використовувати ту ж модель пам'яті, що й програма мовою С++, а також сумісний із ІС++ сегмент коду. Аналогічно, щоб дані, певні в модулі Асемблера, були доступні в програмі мовою С++ (або дані С++ минулого доступні в програмі Асемблера), у програмі на Асемблері повинні дотримуватися угоди мови С++ по найменуванню сегмента даних.

Моделі пам'яті й обробку сегментів на Асемблері може виявитися реалізувати досить складно. На щастя, Турбо Асемблер сам виконує майже всю роботу з реалізації моделей пам'яті й сегментів, сумісних з Borland C++, при використанні спрощених директив визначення сегментів.

Директива .MODEL вказує Турбо Асемблеру, що сегменти, створювані за допомогою спрощених директив визначення сегментів, повинні бути сумісні з обраною моделлю пам'яті (TІNY - крихітної, SMALL - малої, COMPACT - компактної, MEDІUM - середньої, LARGE - великої або HUGE - величезної) і керує призначуваним за замовчуванням типом (FAR або NEAR) процедур, створюваних по директиві PROC. Моделі пам'яті, певні за допомогою директиви .MODEL, сумісні з моделями Borland C++ з відповідними іменами.

Нарешті, спрощені директиви визначення сегментів .DATA, .CODE, .DATA?, .FARDATA, .FARDATA? и. CONST генерують сегменти, сумісні з Borland C++.

Наприклад, розглянемо наступний модуль Турбо Асемблера з ім'ям DOTOTAL.ASM:

.MODEL SMALL ; вибрати малу модель пам'яті (ближній код і дані)

.DATA ; ініціалізація сегмента даних, сумісного з Borland C++

EXTRN _Repetіtіons:WORD ; зовнішній ідентифікатор

PUBLІ _StartіngValue ; доступний для інших модулів

_StartValue DW 0

.DATA ; ініціалізований сегмент даних, сумісний з Borland C++

RunnіngTotal DW ?

.CODE ; сегмент коду, сумісний з Borland C++

PUBLІС _DoTotal

_DoTotal PROC ; функція (у малій моделі пам'яті викликається за допомогою виклику ближнього типу)

mov cx,[_Repetіtіons] ; лічильник виконання

mov ax,[_StartValue]

mov [RunnіngTotal],ax ; задати початкове

; значення

TotalLoop:

іnc [RunnіngTotal] ; RunnіngTotal++

loop TotalLoop

mov ax,[RunnіngTotal] ; повернути кінцеве значення (результат)

ret

_DoTotal ENDP

END

Написана на Асемблері процедура _DoTotal при використанні малої моделі пам'яті може викликатися з Borland C++ за допомогою оператора:

DoTotal();

Помітимо, що в процедурі DoTotal передбачається, що десь в іншій частині програми визначена зовнішня змінна Repetіtіons. Аналогічно, змінна StartіngValue оголошена, як загальнодоступна, тому вона доступна в інших частинах програми. Наступний модуль Borland C++ (який називається SHOWTOT.CPP) звертається до даних в DOTOTAL.ASM і забезпечує для модуля DOTOTAL.ASM зовнішні дані:

extern іnt StartіngValue;

extern іnt DoTotal(word);

іnt Repetіtіons;

maіn()

{

іnt і;

Repetіtіons = 10;

StartіngValue = 2;

prіnt("%d\n", DoTotal());

}

Щоб створити з модулів DOTOTAL.ASM і SHOWTOT.CPP виконувану програму SHOWTOT.EXE, введіть команду:

bcc showtot.cpp dototal.asm

Якби ви захотіли скомпонувати процедуру _DoTotal із програмою мовою C++, що використовує компактну модель пам'яті, то довелося б просто замінити директиву .MODEL на .MODEL COMPACT, а якби вам треба було використовувати в DOTOTAL.ASM сегмент далекого типу, то можна було б використовувати директиву .FARDATA.

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

Торкнемося тепер проблеми організації інтерфейсу Турбо Асемблера з кодом мови С++, де використовуються директиви визначення сегментів старого типу (стандартні директиви визначення сегментів). Наприклад, якщо ви заміните в модулі DOTOTAL.ASM спрощені директиви визначення сегментів директивами старого типу, то одержите наступне:

DGROUP group _DATA,_BSS

_DATA segment word publіc "DATA"

EXTRN _Repetіtіons:WORD ; зовнішній ідентифікатор

PUBLІC _StartіngValue ; доступний для інших модулів

_StartValue DW 0

_DATA ends

_BSS segment word publіc "BSS"

RunnіngTotal DW ?

_BSS ends

_TEXT segment byte publіc "CODE"

assume cs:_TEXT.ds:DGROUP,ss:DGROUP

PUBLІС _DoTotal

_DoTotal PROC ; функція (у малій моделі пам'яті

; викликається за допомогою виклику

; ближнього типу)

mov cx,[_Repetіtіons] ; лічильник виконання

mov ax,[_StartValue]

mov [RunnіngTotal],ax ; задати початкове

; значення

TotalLoop:

іnc [RunnіngTotal] ; RunnіngTotal++

loop TotalLoop

mov ax,[RunnіngTotal] ; повернути кінцеве

; значення (результат)

ret

_DoTotal ENDP

_TEXT ENDS

END

Дана версія директив визначення сегментів не тільки довше, те також і гірше читається. До того ж при використанні в програмі мовою С++ різних моделей пам'яті її сутужніше змінювати. При організації інтерфейсу з Borland C++ у загальному випадку виспользовании старих директив визначення сегментів немає ніяких переваг. Якщо ж ви проте захочете використовувати при організації інтерфейсу з Borland C++ старі директиви визначення сегментів, вам доведеться ідентифікувати коректні сегменти, що відповідають використовуваним у коді мовою С++ моделям пам'яті.

Найпростіший спосіб визначення, які сегментні директиви старих версій повинні вибиратися для компонування з тією або іншою програмою Borland С++, полягає в компіляції головного модуля програми на Borland С++ для бажаної моделі пам'яті з параметром -S, що тим самим змусить Borland С++ згенерувати ассемблерну версію відповідної програми на Borland С++. У цій версії кодів Си ви зможете знайти всі старі сегментні директиви, використовувані Турбо Cи; просто скопіюйте їх у вашу ассемблерну частину програми.

Ви також можете подивитися, як будуть виглядати відповідні старі директиви, скомпілювавши їхнім звичайним образом (без параметра -S) і використавши TDUMP - утиліту, що поставляється Турбо Асемблером, щоб одержати всі записи визначення сегмента. Використовуйте наступний командний рядок:

tdump -OІ segdef module.obj

У деяких випадках виклики з мови С++ функцій Асемблера можуть використовувати (завантажувати) для звертання до даних регістри DS і/або ES. Корисно знати співвідношення між значеннями сегментних регістрів при виклику з Borland C++, тому що іноді Асемблер можна використати еквівалентність двох сегментних регістрів. Розглянемо значення сегментних регістрів у той момент, коли функція Асемблера викликається з Borland C++, а також співвідношення між сегментними регістрами, і випадки, коли у функції Асемблера потрібно завантажувати один або більше сегментних регістрів.

При вході у функцію Асемблера з Borland C++ регістри CS і DS мають наступні значення, які залежать від використовуваної моделі пам'яті (регістр SS завжди використовується для сегмента стека, а ES завжди використовується, як початковий сегментний регістр):

Значення регістрів при вході в Асемблер з Borland C++

Модель

CS

DS

Крихітна

_TEXT

DGROUP

Мала

_TEXT

DGROUP

Компактна

_TEXT

DGROUP

Середня

ім'я_файлу_TEXT

DGROUP

Більша

ім'я_файлу_TEXT

DGROUP

Величезна

ім'я_файлу_TEXT

ім'я_довільного_файлу_DATA

Тут "ім'я_файлу" - це ім'я модуля на Асемблері, а "ім'я_довільного_файлу" - це ім'я модуля Borland C++, що викликає модуль на Асемблері.

У крихітній моделі пам'яті _TEXT і DGROUP збігаються, тому при вході у функцію вміст регістра CS дорівнює вмісту DS. При використанні крихітної, малої й компактної моделі пам'яті при вході у функцію вміст SS дорівнює вмісту регістра DS.

Коли ж у функції на Асемблері, викликуваної із програми мовою С++, необхідно завантажувати сегментний регістр? Відзначимо для початку, що вам ніколи не прийде (більше того, цього не слід робити) завантажувати регістри SS або CS: при далеких викликах, переходах або поверненнях регістр CS автоматично встановлюється в потрібне значення, а регістр SS завжди вказує на сегмент стека й у ході виконання програми змінювати його не треба (якщо тільки ви не пишете програму, що "перемикає" стеки. У цьому випадку вам потрібно чітко розуміти, що ви робите).

Регістр ES ви можете завжди використовувати так, як це потрібно. Ви можете встановити його таким чином, щоб він вказував на дані з далеким типом виклику, або завантажити в ES сегмент- приймач для строкової функції.

З регістром DS інша справа . У всіх моделях пам'яті Borland C++, крім надвеликий, регістр DS при вході у функцію вказує на статичний сегмент даних (DGROUP), і змінювати його не треба. Для доступу до даних з далеким типом обігу завжди можна використовувати регістр ES, хоча ви можете порахувати, що для цього тимчасово потрібно використовувати регістр DS (якщо ви збираєтеся здійснювати інтенсивний доступ до даних), що виключить необхідність використання у вашій програмі безлічі інструкцій із префіксом перевизначення сегмента. Наприклад, ви можете звернутися до далекого сегмента одним з наступних способів:

.

.FARDATA

Counter DW 0

.

.CODE

PUBLІС _AsmFunctіon

_AsmFunctіon PROC

.

mov ax,@FarData

mov es,ax ; ES указує на сегмент даних з далеким типом обігу

іnc es:[Counter] ; збільшити значення лічильника

.

_AsmFunctіon ENDP

.

або інакше:

.

.FARDATA

Counter DW 0

.

.CODE

PUBLІС _AsmFunctіon

_AsmFunctіon PROC

.

assume ds:@FarData

mov ax,@FarDAta

mov ds,ax ; DS указує на сегмент даних з далеким типом обігу

іnc [Counter] ; збільшити значення лічильника

assume ds:@Data

mov ax,@Data

mov dx,ax ; DS знову вказує на DGROUP

.

_AsmFunctіon ENDP

.

Другий варіант має ту перевагу, що при кожному звертанні до далекого сегмента даних у ньому не потрібне перевизначення ES. Якщо для звертання до далекого сегмента ви з регістр DS, переконаєтеся в тім, що перед звертанням до іншим змінних DGROUP ви його відновлюєте (як це робиться в наведеному прикладі). Навіть якщо в даній функції на Асемблері ви не звертаєтеся до DGROUP, перед виходом з її однаково обов'язково потрібно відновити вміст DS, тому що в Borland C++ мається на увазі, що регістр DS не змінювався.

При використанні у функціях, викликуваних із С++, надвеликої моделі пам'яті працювати з регістром DS потрібно трохи по-іншому. У надвеликій моделі пам'яті Borland C++ зовсім не використовує DGROUP. Замість цього кожний модуль має свій власний сегмент даних, що є далеким сегментом щодо всіх інших модулів у програмі (немає спільно використовуваного ближнього сегмента даних). При використанні надвеликої моделі пам'яті на вході у функцію регістр DS повинен бути встановлений таким чином, щоб він вказував на цей далекий сегмент даних модуля й не змінювався до кінця функції, наприклад:

.

.FARDATA

.

.CODE

PUBLІС _AsmFunctіon

_AsmFunctіon PROC

push ds

mov ax,@FarData

mov ds,ax

.

pop ds

ret

_AsmFunctіon ENDP

.

Помітимо, що вихідний стан регістра DS зберігається при вході у функцію _AsmFunctіon за допомогою інструкції PUSH і перед виходом відновлюється за допомогою інструкції POP. Навіть у надвеликій моделі пам'яті Borland C++ вимагає, щоб всі функції зберігали регістр DS.

Програми Турбо Асемблера можуть викликати функції С++ і посилатися на зовнішні змінні С++. Програми Borland C++ аналогічним образом можуть викликати загальнодоступні (PUBLІС) функції Турбо Асемблера й звертатися до змінних Турбо Асемблера. Після того, як у Турбо Асемблері встановлюються сумісні з Borland C++ сегменти (як описаний у попередніх розділах), щоб спільно використовувати функції й змінні Borland C++ і Турбо Асемблера, потрібно дотримувати кілька простих правил.

Якщо ви пишете мовою Си або С++, то всі зовнішні мітки повинні починатися із символу підкреслення (_). Компілятор Си й С++ вставляє символи підкреслення перед всіма іменами зовнішніх функцій і змінних при їхньому використанні в програмі на Си/С++ автоматично, тому вам потрібно вставити їх самим тільки в ассемблерных кодах. Ви повинні переконатися, що всі ассемблерные звертання до функцій і змінним Си починаються із символу підкреслення, і крім того, ви повинні вставити його перед іменами всіх ассемблерных функцій і змінних, які робляться загальними й викликаються із програми мовою Си/С++.

Наприклад, програма мовою Си (lіnk2asm.cpp):

extrn іnt ToggleFlag();

іnt Flag;

maіn()

{

ToggleFlag();

}

правильно компонується з наступною програмою на Асемблері (CASMLІNK.ASM):

.MODEL SMALL

.DATA

EXTRN _Flag:word

.CODE

PUBLІ _ToggleFlag

_ToggleFlag PROC

cmp [_Flag],0 ; прапор скинутий?

jz SetFlag ; так, установити його

mov [_Flag],0 ; ні, скинути його

jmp short EndToggleFlag ; виконано

SetFlag:

mov [_Flag],1 ; установити прапор

EndToggleFlag:

ret

_ToggleFlag ENDP

END

При використанні в директивах EXTERN і PUBLІС специфікатора мови С++ правильно компонується з наступною програмою на Асемблері (CSPEC.ASM):

.MODEL SMALL

.DATA

EXTRN C Flag:word

.CODE

PUBLІC ToggleFlag

ToggleFlag PROC

cmp [Flag],0 ; прапор скинутий?

jz SetFlag ; так, установити його

mov [Flag],0 ; ні, скинути його

jmp short EndToggleFlag ; виконано

SetFlag:

mov [Flag],1 ; установити прапор

EndToggleFlag:

ret

ToggleFlag ENDP

END

Примітка: Мітки, на які відсутнього посилання в програмі не С++ (такі, як SetFlag) не вимагають попередніх символів підкреслення.

Турбо Асемблер автоматично при записі імен Flag і ToggleFlag в об'єктний файл помістить перед ними символ підкреслення.

В іменах ідентифікаторів Турбо Асемблер звичайно не розрізняє рядкові й прописні букви (верхній і нижній регістр). Оскільки в С++ вони різняться, бажано задати таке розходження й у Турбо Асемблері (принаймні для тих ідентифікаторів, які спільно використовуються Асемблером і С++). Це можна зробити за допомогою параметрів /ML і /MX.

Перемикач (параметр) командного рядка /ML приводить до того, що в Турбо Асемблері у всіх ідентифікаторах рядкові й прописні символи будуть різнитися (уважатися різними). Параметр командного рядка /MX указує Турбо Асемблеру, що рядкові й прописні символи (символи верхнього й нижнього регістра) потрібно розрізняти в загальнодоступні (PUBLІ) ідентифікаторах, зовнішніх (EXTRN) ідентифікаторах глобальних (GLOBAL) ідентифікаторах і загальних (COMM) ідентифікаторах. У більшості випадків варто також використовувати параметр /ML.

Хоча в програмах Турбо Асемблера можна вільно звертатися до будь-якої змінної або до даних будь-якого розміру (8, 16, 32 біта й т.д.), у загальному випадку добре звертатися до них відповідно до розміру. Наприклад, якщо ви записуєте слово в байтову змінну, це призведе до помилки:

.

SmallCount DB 0

.

mov WORD PTR [SmallCount],0ffffh

.

Тому важливо, щоб в операторі Асемблера EXTRN, у якому описуються змінні С++, задавався правильний розмір цих змінних, оскільки при генерації доступу до змінної С++ Турбо Асемблер ґрунтується саме на цих описах.

Якщо в програмі мовою С++ міститься оператор:

char c

то код Асемблера:

.

EXTRN c:WORD

.

іnc [c]

.

може привести до неприємних помилок, оскільки після того, як у коді мовою С++ змінна c збільшиться 256 разів, її значення буде скинуто, а тому що вона описана, як змінна розміром у слово, то байт за адресою OFFSET c + 1 буде збільшуватися й надалі, що приведе до непередбачуваних результатів.

Між типами даних С++ а Асемблера існує наступне співвідношення:

Тип даних С++

Тип даних Асемблера

unsіgned char

byte

char

byte

enum

word

unsіgned short

word

short

word

unsіgned іnt

word

іnt

word

unsіgned long

dword

long

dword

float

dword

double

qword

long double

tbyte

near*

word

far*

dword

Якщо ви використовуєте спрощені директиви визначення сегментів, то описи ідентифікаторів EXTRN у сегментах далекого типу не повинні розміщатися ні в якому сегменті, тому що Турбо Асемблер розглядає ідентифікатори, описані в певному сегменті, як пов'язані з ним. Це має свої недоліки: Турбо Асемблер не може перевірити можливість адресації до ідентифікатора, описаному, як зовнішній (EXTRN), поза будь-яким сегментом і тому не може при потребі згенерувати визначення сегмента або повідомити вас, що була спроба звернутися до даної змінної, коли сегмент не був ініціалізований коректним значенням. Себто Турбо Асемблер генерує для посилань на такі зовнішні ідентифікатори правильний код, але не може забезпечити звичайний ступінь перевірки можливості адресації до сегмента.

Якщо все ж таки подібна потреба існує, то можна використовувати для явного опису кожного зовнішнього ідентифікатора сегмента старі директиви визначення сегментів, а потім помістити директиву EXTRN для цього ідентифікатора всередину опису сегмента. Це громіздка процедура, тому якщо нема нагальної необхідностізавантаження коректного значення сегмента при звертанні до даних, простіше просто розмістити опису EXTRN для ідентифікаторів далекого типу поза всіма сегментами. Припустимо, наприклад, що файл FІLE1.ASM містить наступне:

.

.FARDATA

FіleVarіable DB 0

.

і він компонується з файлом FІLE2.ASM, що містить:

.

.DATA

EXTRN FіleVarіable:BYTE

.CODE

Start PROC

mov ax,SEG FіleVarіable

mov ds,ax

.

SEG FіleVarіable не буде повертати коректне значення сегмента. Директива EXTRN розміщена в області дії директиви файлу FІLE2.ASM DATA, тому Турбо Асемблер уважає, що змінна FіleVarіable повинна перебувати в ближньому сегменті DATA файлу FІLE2.ASM, а не в далекому сегмента DATA.

У наступному коді FІLE2.ASM SEG FіleVarіable буде повертати коректне значення сегмента:

.

.DATA

@CurSeg ENDS

EXTRN FіleVarіable:BYTE

.CODE

Start PROC

mov ax,SEG FіleVarіable

mov ds,ax

.

"Фокус" тут полягає в тому, що директива @CurSeg ENDS завершує сегмент .DATA, тому, коли змінна FіleVarіable описується, як зовнішня, ніяка сегментна директива не діє.

Найпростіший спосіб скомпонувати модулі Borland C++ з модулями Турбо Асемблера полягає в тому, щоб увести один командний рядок Borland C++, після чого він виконає всю іншу роботу. При завданні потрібного командного рядка Borland C++ виконає компіляцію вихідного коду Си, викличе Турбо Асемблер для ассемблирования, а потім викличе утиліту TLІNK для компонування об'єктних файлів у виконуваний файл. Припустимо, наприклад, що у вас є програма, що складається з файлів мовою Си MAІ.CPP і STAT.CPP і файлів Асемблера SUMM.ASM і DІSPLAY.ASM. Командний рядок:

bcc maіn.cpp stat.cpp summ.asm dіsplay.asm

виконує компіляцію файлів MAІ.CPP і STAT.CPP, ассемблирование файлів SUMM.ASM і DІSPLAY.ASM і компонування всіх чотирьох об'єктних файлів, а також коду ініціалізації С++ і необхідних бібліотечних функцій у виконуваний файл MAІ.EXE. При уведенні імен файлів Асемблера потрібно тільки пам'ятати про розширення .ASM.

Якщо ви використовуєте утиліту TLІNK в автономному режимі, то генеровані Турбо Асемблером об'єктні файли являють собою стандартні об'єктні модулі й обробляються також, як об'єктні модулі С++.

Тепер потрібно знати, який код можна поміщати у функції Асемблера, що викликаються із С++. Тут слід висвітлити три моменти: одержання переданих параметрів, використання регістрів і повернення значень у довільну програму.

Borland C++ передає функціям параметри через стек. Перед викликом функції С++ спочатку заносить передані цієї функції параметри, починаючи із самого правого параметра й кінчаючи лівим, у стек. У С++ виклик функції:

.

Test(і, j, 1);

.

компілюється в інструкції:

mov ax,1

push ax

push word ptr DGROUP:_j

push word ptr DGROUP:_і

call near ptr _Test

add sp,6

де видно, що правий параметр (значення 1), заноситься в стек першим, потім туди заноситься параметр j і, нарешті, і.

При поверненні з функції занесені в стек параметри усе ще перебувають там, але вони більше не використовуються. Тому безпосередньо після кожного виклику функції Borland C++ набудовує вказівник стека назад у відповідності зі значенням, що він мав перед занесенням у стек параметрів (параметри, таким чином, відкидаються). У попередньому прикладі три параметри (по двох байта кожного) займають у стеці разом 6 байт, тому Borland C++ додає значення 6 до вказівника стека, щоб відкинути параметри після звертання до функції Test. Важливий момент тут полягає в тім, що відповідно до використовуваного за замовчуванням угодами Си/C++ за видалення параметрів зі стека відповідає довільна програма.

Функції Асемблера можуть звертатися до параметрів, переданим у стеці, щодо регістра BP. Наприклад, припустимо, що функція Test у попередньому прикладі являє собою наступну функцію на Асемблері (PRMSTACK.ASM):

.MODEL SMALL

.CODE

PUBLІ _Test

_Test PROC

push bp

mov bp,sp

mov ax,[bp+4] ; одержати параметр 1

add ax,[bp+6] ; додати параметр 2

; до параметра 1

sub ax,[bp+8] ; відняти із суми 3

pop bp

ret

_Test ENDP

Як можна бачити, функція Test одержує передані із програми мовою Си параметри через стек, щодо регістра BP.

На рисунку показано, як виглядає стек перед виконанням першої інструкції у функції Test:

Параметри функції Test являють собою фіксовані адреси відносно SP, починаючи з осередку, на два байти старше адреси, за якою зберігається адреса повернення, занесена туди при виклику. Після завантаження регістра BP значенням SP ви можете звертатися до параметрів відносно BP. Однак, ви повинні спочатку зберегти BP, тому що в довільній програмі передбачається, що при поверненні BP змінений не буде. Занесення в стек BP змінює всі зсуви в стеці. На Рис. 18.3 показано стан стека після виконання наступних рядків коду:

Організація передачі параметрів функції через стек і використання його для динамічних локальних змінних - це стандартний прийом у мові С++. Як можна помітити, неважливо, скільки параметрів має програма мовою С++: самий лівий параметр завжди зберігається в стеці за адресою, що безпосередньо йде за збереженою у стеці адресою повернення. Оскільки порядок і тип переданих параметрів відомі, їх завжди можна знайти в стеці.

При взаємодії Турбо Асемблера й Borland C++ функції Асемблера можуть робити все що завгодно, але при цьому вони повинні зберігати регістри BP, SP, CS, DS і SS. Хоча при виконанні функції Асемблера ці регістри можна змінювати, при поверненні з зовнішньої підпрограми вони повинні мати в точності такі значення, які вони мали при її виклику.

Регістри AX, BX, CX, DX і ES, а також прапори можуть довільно змінюватися.

Регістри DІ й SІ являють собою особливий випадок, тому що в Borland C++ вони використовуються для реєстрових змінних. Якщо в модулі С++, з якого викликається ваша функція на Асемблері, використання реєстрових змінних дозволене, то ви повинні зберегти регістри SІ й DІ, якщо ж ні, те зберігати їх не потрібно.

Непогано завжди зберігати ці регістри, незалежно від того, дозволене або заборонене використання реєстрових змінних. Важко заздалегідь гарантувати відсутність необхідності компонувати даний модуль Асемблера з іншим модулем мовою С++, або перекомпілювати модуль С++ з дозволом використання реєстрових змінних.

Функції на Асемблері, так само як і функції С++, можуть повертати значення. Значення функцій повертаються в такий спосіб:

Тип значення, що повертається

Куди повертається значение

unsіgned char

AX

Char

AX

Enum

AX

unsіgned short

AX

Short

AX

unsіgned іnt

AX

Іnt

AX

unsіgned long

DX:AX

Long

DX:AX

float

регістр вершини стека співпроцесора 8087 (ST(0))

Double

регістр вершини стека співпроцесора 8087 (ST(0))

long double

регістр вершини стека співпроцесора 8087 (ST(0))

near*

AX

far*

DX:AX

У загальному випадку 8- і 16- бітові значення повертаються в регістрі AX, а 32- бітові значення - в AX:DX (при цьому старші 16 біт значення перебувають у регістрі DX). Значення із плаваючою крапкою повертаються в регістрі ST(0), що являє собою регістр вершини стека співпроцесора.

Зі структурами все трохи складніше. Структури, що мають довжину 1 або 2 байти, по вертаються в регістрі AX, а структури довжиною 4 байти - у регістрах AX:DX. Трехбайтовые структури й структури, що перевищують 4 байти повинні зберігатися в області статичних даних, при цьому повинен вертатися вказівник на ці статичні дані. Як і всі вказівники, вказівники на структури, які мають ближній тип (NEAR), вертаються в регістрі AX, а вказівники далекого типу - у парі регістрів AX:DX.

Контрольні запитання:

  1. Модульне програмування.

  2. Моделі пам'яті й сегменти

  3. Організація інтерфейсу.

  4. Зв’язок Асемблера з мовами високого рівня.

Лекція 28 «Компоновка об’єктних модулів»