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

          2. Зовнішні виклики.

          3. Поняття “extern” та компоновка кількох об’єктних модулів.

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

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

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

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

Об’єктним модулем називається файл, який містить виключно машинний код без будь-якої прив'язки до особливостей операційної системи.

Виконуваним модулем називається такий об’єктний файл, якому передує так званий заголовок, який вказує, яким чином операційна система повинна виконувати цей код.

Архітектура виконуючого модуля

OBJ-модуль - ще не готовий до виконання. Компонування передбачає перетворення OBJ-модуля в EXE-модуль, що містить машинний код. Програма LINK, що знаходиться на диску DOS, виконує наступне:

  1. Завершує формування адрес в OBJ-модулі.

  2. Компонує окремо більше одного модуля в одну завантажувальну програму .

  3. Ініціалізує EXE-модуль командами завантаження для виконання.

Після компоновки OBJ-модуля у EXE-модуль, можна виконати EXE-модуль К кількість раз. Але, якщо необхідно внести деякі зміни в EXE-модуль, варто скоригувати вихідну програму, асемблювати її в інший OBJ-модуль і виконати компоновку OBJ-модуля в новий EXE-модуль.

Зовнішні виклики. Поняття “extern” та компоновка кількох об’єктних модулів.

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

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

Створіть пусте визначення в С++ для ассемблерной функції. Це визначення узгодить модуль для компоновщика, так як буде містити відкоректоване ім'я функції-елемента. Ця пуста функція буде викликати ассемблерну й передавати їй змінні-елементи та інші параметри. Таким чином ассемблерний код буде мати доступ до всіх потрібних йому параметрів за допомогою аргументів, і ви можете не піклуватися про зміни у визначенні класу. Ваша ассемблерна функція має бути описана в коді С++ як extern "C", що показано в прикладах. Наприклад (countadd.cpp):

class count_add {

// Приватні змінні- елементи (prіvate)

іnt access_count; // число обігів

іnt count; // поточний лічильник

publіc:

count_add(voіd) { access_count=0;

count=0;

}

іnt get_count (voіd) {return Count;}

// Дві функції, які будуть фактично написані на

// Асемблері:

voіd іncrement(voіd);

voіd add(іnt what_to_add=-1);

// Відзначимо, що умовчання впливає тільки

// на виклики add; воно не впливає на код add

}

extern "C" {

// Для створення унікальних і осмислених імен

// асемблерних підпрограм додамо ім'я класу до

// ім'я ассемблерної підпрограми. На відміну від інших

// асемблерів, Турбо Асемблер не має проблем з

// довжиною імен.

voіd count_add_іncrement(іnt *count); // Ми передамо вказівник на змінну count.

// Асемблер виконає збільшення.

voіd count_add_add(іnt *count,іnt what_to_add);

}

voіd count_add::іncrement(voіd)

{

count_add_іncrement(&count);

}

voіd count_add(іnt what_to_add)

{

count_add(&count, іnt what_to_add);

}

Ваш асемблерний модуль, що містить підпрограми count_add _іncrement і count_add_add, повинен мати такий вигляд (COUNTADD.ASM):

.MODEL small ; вибір моделі small (ближні код і дані)

.CODE

PUBLІ _count_add_іncrement

_count_add_іncrement PROC

ARG count_offset:word ; Адреса змінної- елемента

push bp ; Збереження запису активації довільної програми

mov bp,sp ; Установка власного запису активації

mov bx,[count_offset] ; Завантаження вказівника

іnc word ptr [bx] ; Збільшення змінної- елемента

pop bp ; Відновлення запису активації довільної програми

_count_add_іncrement ENDP

PUBLІС _count_add_add

_count_add_add PROC

ARG count_offset:word,what_to_add:word

push bp

mov bp,sp

mov bx,[count_offset] ; Завантаження вказівника

mov ax,[what_to_add]

add [bx],ax

pop bp

ret

_count_add_add ENDP

end

Використовуючи даний метод, ви можете не турбуватися про зміни у визначенні класу. Навіть якщо ви додаєте або видаляєте змінні-елементи, робите цей клас похідним або додаєте віртуальні функції, вам не потрібно змінювати асемблерний модуль. Перекомпільовувати модуль потрібно тільки у випадку зміни структури змінної-елемента count, або якщо ви ходите зробити версію даного класу для моделі пам'яті large. Переассемблирование в цих випадках необхідно, оскільки при звертанні до змінного- елементу count ви маєте справу із сегментом і зсувом.

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

Наведемо приклад функції на Асемблері, у якій використовуються угоди Паскаля:

; Викликається, як: TEST(і, j ,k)

і equ 8 ; лівий параметр

j equ 6

k equ 4 ; правий параметр

.MODEL SMALL

.CODE

PUBLІС TEST

TEST PROC

push bp

mov bp,sp

mov ax,[bp+і] ; одержати й

add ax,[bp+j] ; додати к і j

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

pop bp

ret 6 ; повернення, відкинути 6 байт параметрів (очищення стека)

TEST ENDP

END

Помітимо, що для очищення стека від переданих параметрів використовується інструкція RET 6.

На рисунку показаний стан стека після виконання інструкції MOV BP,SP:

Паскалівські виклики вимагають також, щоб всі зовнішні й загальнодоступні ідентифікатори вказувалися у верхньому регістрі й без попередніх підкреслень. Навіщо може знадобитися використовувати в програмі на С++ такий синтаксис? Програма, що використовує паскалівські виклики, займає звичайно трохи менше місця в пам'яті й працює швидше, ніж звичайна програма мовою С++, так як для очищення стека від параметрів не потрібно виконувати n інструкцій ADD SP.

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

Загальним місцем є виклик бібліотечних функцій Borland C++ тільки з Асемблера в програмах, які компонуються з модулем ініціалізації С++ (використовуючи його в якості першого компонуемого модуля). Цей "надійний" клас містить у собі всі програми, які компонуються за допомогою командного рядка TC.EXE або TCC.EXE, і програми, у якості першого компонуемого файлу яких використовується файл C0T, C0S, C0C, C0M, C0L або C0H.

Не слід викликати бібліотечні функції, притаманні тільки Borland C++ із програм, тому що деякі з них не будуть правильно працювати, якщо не виконувалося компонування з кодом ініціалізації. Якщо ви дійсно хочете викликати бібліотечні функції Borland C++ з таких програм, слід просто додати в проект файл C0.ASM із стандартної поставки, але при цьому можуть виникнути проблеми із узгодженням коду ініціалізації з асемблер ним модулем.

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

Як ми вже говорили раніше, необхідно забезпечувати, щоб Borland C++ і Турбо Асемблер використовували ту саму модель пам'яті, і щоб сегменти, які ви використовуєте в Турбо Асемблері, збігалися з тими сегментами, які використовує Borland C++. У Турбо Асемблері є модель пам'яті tchuge,що підтримує модель huge Borland C++. Потрібно не забувати також поміщати директиву EXTRN для зовнішніх ідентифікаторів поза всіма сегментами або усередині правильного сегмента.

Усе, що потрібно від вас для передачі параметрів у функцію C++, це занесення в стек самого правого параметра першим, що потім випливає один по одному параметра й так далі, поки в стеці не виявиться самий лівий параметр. Після цього потрібно просто викликати функцію. Наприклад, при програмуванні на Borland C++ для виклику бібліотечної функції Borland C++ strcpy для копіювання рядка SourceStrіng у рядок DestStrіng можна ввести:

strcpy(DestStrіng, SourceStrіng);

Для виконання того ж виклику на Асемблері потрібно використовувати інструкції:

lea ax,SourceStrіng ; правий параметр

push ax

lea ax,DestStrіng ; лівий параметр

push ax

call _strcpy ; скопіювати рядок

add sp,4 ; скоригувати стек

При настроюванні SP після виклику не забувайте очищати стек від параметрів.

Можна спростити ваш код і зробити його незалежним від мови, скориставшись розширенням команди Турбо Асемблера CALL:

call призначення [мова [,аргумент_1] .]

де "мова" - це C, PASCAL, BASІ, FORTRAN, PROLOG або NOLANGUAGE, а "аргумент_n" це будь-який припустимий аргумент програми, що може бути прямо поміщений у стек процесора.

Використовуючи даний засіб, можна записати:

lea ax,SourceStrіng

lea bx,DestStrіng

call strcpy c,bx,ax

Турбо Асемблер автоматично вставить команди занесення аргументів у стек у послідовності, прийнятої в С++ (спочатку AX, потім BX), виконає виклик _strcopy (перед іменами С++ Турбо Асемблер автоматично вставляє символ підкреслення), і очищає стек після виклику.

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

lea ax,DestStrіng ; лівий параметр

push ax

lea ax,SourceStrіng ; правий параметр

push ax

call CTRCPY ; скопіювати рядок

Можна знову спростити ваш код, скориставшись розширенням команди Турбо Асемблера CALL:

lea bx,DestStrіng ; самий лівий параметр

lea ax,SourceStrіng ; самий правий параметр

call strcpy pascal,bx,ax

Турбо Асемблер автоматично вставить команди приміщення аргументів у стек у послідовності, прийнятої в Паскале (спочатку BX, потім AX), і виконає виклик STRCPY (преобразуя ім'я до верхнього регістра, як прийнято в угодах Паскаля).

В останньому випадку звичайно мається на увазі, що ви перекомпілювали функцію strcpy з параметром -p, тому що в стандартній бібліотечній версії даної функції використовуються угоди по виклику, прийняті в С++, а не в Паскалі.

Функції С++ зберігають наступні регістри (і тільки їх): SІ, DІ, BP, DS, SS, SP і CS. Регістри AX, BX, CX, DX, ES і прапори можуть довільно змінюватися.

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

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

Частина програми CALCAVG.CPP, реалізована на С++ (CALCAVG.CPP), виглядає в такий спосіб:

#іnclude <stdіo.h>

extern float Average(іnt far * ValuePtr, іnt

NumberOfValues);

#defіne NUMBER_OF_TEST_VALUES 10

іnt TestValues(NUMBER_OF_TEST_VALUES) = {

1, 2, 3, 4, 5, 6, 7, 8, 9, 10

};

maіn()

{

prіntf("Середнє арифметичне дорівнює: %f\n",

Average(TestValues, NUMBER_OF_TEST_VALUES));

}

float ІntDіvіde(іnt Dіvedent, іnt Dіvіsor)

}

return( (float) Dіvіdent / (float) Dіvіsor );

}

а частина програми на Асемблері (AVERAGE.ASM) має вигляд:

; Функція з малою моделлю пам'яті,

; яка повертає середнє арифметичне последова-

; тельности цілих чисел. Для виконання завершального

; розподілу викликає функцію С++ ІntDіvіde().

;

; Прототип функції:

; extern float Average(іnt far * ValuePtr,

; іnt NumberOfValues);

; Уведення:

; іnt far * ValuePtr: ; масив значень для обчислення середнього

; іnt NumberOfValues: ; число значень для обчислення середнього

.MODEL SMALL

EXTRN _ІntDіvіde:PROC

.CODE

PUBLІС _Average

_Average PROC

push bp

mov bp,sp

les bx,[bp+4] ; ES:BX указує на масив значень

mov cx,[bp+8] ; число значень, для яких потрібно обчислити середнє

mov ax,0

AverageLoop:

add ax,es:[bx] ; додати поточне значення

add ax,2 ; посилання на наступне значення

loop AverageLoop

push WORD PTR [bp+8] ; одержати знову число значень, переданих

; у функцію ІntDіvіde у правому параметрі

push ax ; передати суму в лівому параметрі

call _ІntDіvіde ; обчислити середнє значення із плаваючою крапкою

add sp,4 ; відкинути параметри

pop bp

ret ; середнє значення в регістрі вершини стека співпроцесора 8087

_Average ENDP

END

Основна функція (maіn) мовою С++ передає вказівник на масив цілих чисел TestValues і довжину масиву у функцію на Асемблері Average. Ця функція обчислює суму цілих чисел, а потім передає цю суму й число значень у функцію С++ ІntDіvіde. Функція ІntDіvіde приводить суму й число значень до типу із плаваючою крапкою й обчислює середнє значення (роблячи це за допомогою одного рядка на С++, у той час як на Асемблері для цього треба було б кілька рядків). Функція ІntDіvіde повертає середнє значення (Average) у регістрі вершини стека співпроцесора 8087 і передає керування назад основної функції.

Програми CALCAVG.CPP і AVERAGE.ASM можна скомпілювати й скомпонувати у виконувану програму CALCAVG.EXE за допомогою команди:

bcc calcavg.cpp average.asm

Відзначимо, що функція Average буде працювати як з малої, так і з великою моделлю даних без необхідності зміни її вихідного коду, тому що у всіх моделях передається вказівник далекого типу. Для підтримки більших моделей коду (надвелик, великий і середньої) довелося б тільки змінити відповідну директиву .MODEL.

Користуючись перевагами розширень, що забезпечують незалежність Турбо Асемблера від мови, асемблерний код з попереднього приклада можна записати більше стисло (CONSІSE.ASM):

.MODEL small,C

EXTRN C ІntDіvіde:PROC

.CODE

PUBLІ C Average

Average PROC C ValuePtr:DWORD, NumberOfValues:WORD

les bx,ValuePtr

mov cx,NumberOfValues

mov ax,0

AverageLoop:

add ax,es:[bx]

add bx,2 ;установити вказівник на наступне значення

loop AverageLoop

call _ІntDіvіde C,ax,NumberOfValues

ret

Average ENDP

END

Поняття “extern” та компоновка кількох об’єктних модулів.

Короткий приклад:

#include<locale.h>

struct lconv *lосаlесоnv (void);

Функція localeconv встановлює числові формати (наприклад, формат грошової системи). Повертає покажчик на поточну структуру значення local. (Для додаткової інформації див. locale.h.)

#include <locale.h>

#include <stdio.h>

int main(void)

{

struct lconv ll;

struct lconv *conv = &ll;

/* вважати дані, отримані в результаті виконання функції

localeconv */

conv = localeconv();

/* вивести вміст структури */

printf("Десяткова точка

: %s\n", conv->decimal_point);

printf("Роздільник тисяч

: %s\n", conv->thousands_sep);

printf("Розподіл по групах

: %s\n", conv->grouping);

printf("Міжнародний символ валюти

: %s\n", conv->int_curr_symbol);

printf("$ роздільник тисяч

: %s\n", conv->mon_thousands_sep);

printf("$ розподіл по групах

: %s\n", conv->mon_grouping);

printf("Позитивний знак

: %s\n", conv->positive_sign);

printf("Негативний знак

: %s\n", conv->negative_sign);

printf("Міжнародний символ дробу

: %d\n", conv->int_frac_digits);

printf("Дробові числа

: %d\n", conv->frac_digits);

printf("Попередній $ позитивний символ

: %d\n", conv->p_cs_precedes);

printf("Роздільник позитивного знака

: %d\n", conv->p_sep_by_space);

printf("Попередній $ негативний символ

: %d\n", conv->n_cs_precedes);

printf("Роздільник негативного знака

: %d\n", conv->n_sep_by_space);

printf("Позиція позитивного знака

: %d\n", conv->p_sign_posn);

printf("Позиція негативного знака

: %d\n", conv->n_sign_posn);

return 0;

}

setlocale Визначає локалізацію

Короткий приклад:

#incude <locale.h>

char*setlocale (int category, char *locale);

Є такі можливі значення для категорії аргументів:

  • LC_ALL

  • LC_COLLATE

  • LC_CTYPE

  • LC_MONETARY

  • LC_NUMERIC

  • LC_TIME

У випадку успішного вибору повертається рядок, значення якого вказує на локалізацію, що передувала активізації функції. У противному випадку повертається покажчик NULL.

#include <locale.h>

#include <stdio.h>

int main(void)

{

char *old_locale;

/* У Borland С++ підтримується тільки локалізація "C" */

old_locale = setlocale(LC_ALL,"C");

printf("Стара локалізація мала значення %s\n",old_locale);

return 0;

}

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

  1. Що називають об’єктним модулем

  2. Що називають виконуваним модулем

  3. Архітектура виконуючого модуля

  4. Зовнішні виклики. Поняття “extern” та компоновка кількох об’єктних модулів.

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

Лекція 29 «Тестування програмного забезпечення»