Cистемне програмування ЛАБОРАТОРНА РОБОТА №1. Змішане програмування на мовах СІ та Асемблер. Мета: Оволодіти навиками створення програм, частини яких написані різними мовами програмування Засвоїти правила взаємодії різних модулів. Труднощі опису зв'язку програм мовою C і асемблерних програм полягає в тому, що різні версії мови C мають різні угоди про зв'язки і для більш точної інформації варто користатися посібником з наявної версії мови C. Більшість версій мови C забезпечують передачу параметрів через стек у зворотній (у порівнянні з іншими мовами) послідовності. Звичайно доступ, наприклад, до двох параметрів, переданих через стек, здійснюється в такий спосіб: MOV ES,BP MOV BP,SP MOV DH,[BP+4] MOV DL,[BP+6] ... POP BP RET Деякі версії мови C розрізняють великі і малі букви, тому ім'я асемблерного модуля повинне бути представлено в тому ж символьному регістрі, який використовують для посилання C-програми. У деяких версіях мови C потрібно, щоб асемблерні програми, що змінюють регістри DI і SI, записували їхній вміст у стек при вході і відновлювали ці значення зі стека при виході. Ассемблерні програми повинні повертати значення, якщо це необхідно, у регістрі AX (одне слово) чи в регістро парі DX:AX (два слова). Для деяких версій мови C, якщо ассемблерна програма встановлює прапор DF, те вона повинна скинути його командою CLD перед поверненням. Щоб скомпонувати разом модулі Borland C++ і Турбо Асемблера, повинні бути дотримані наступні три пункти: У модулях Турбо Асемблера повинні використовуватися угоди про імена, прийняті в Borland C++. Borland C++ і Турбо Асемблер повинні спільно використовувати відповідні функції й імена змінних у формі, прийнятної для Borland C++. Для комбінування модулів у виконувану програму потрібно використовувати утіліту-компоновщик TLINK. Важливою концепцією С++ є безпечне, з погляду узгодження типів, компонування. Компілятор і компоновщик повинні працювати узгоджено, щоб гарантувати правильність типів переданих між функціями аргументів. Процес, названий "коректуванням імен" (name-mangling), забезпечує необхідну інформацію про типи аргументів. "Коректування імені" модифікують ім’я функції таким чином, щоб воно несло інформацію про аргументи, що приймаються функцією. Коли програма пишеться цілком на С++, коректування імен відбувається автоматично і прозоро для програми. Однак, коли ви пишете асемблерний модуль для наступного його компонування з програмою на С++, ви самі зобов’язані забезпечити коректування імен у модулі. Це легко зробити, написавши порожню функцію на С++ і скомпілювавши її з асемблерним модулем. Згенерований при цьому Borland С++ файл .ASM буде містити виправлені імена. Потім ви можете їх використовувати при написанні реального асемблерного модуля. Щоб дана функція Асемблера могла викликатися з С++, вона повинна використовувати ту ж модель пам’яті, що і програма мовою С++, а також сумісний із С++ сегмент коду. Аналогічно, щоб дані, визначені в модулі Асемблера, були доступні в програмі мовою С++(чи дані С++ були доступні в програмі Асемблера), у програмі на Асемблері повинні дотримуватися угоди мови С++ по найменуванню сегмента даних. Моделі пам’яті й обробку сегментів на Асемблері може виявитися реалізувати досить складно. На щастя, Турбо Асемблер сам виконує майже всю роботу по реалізації моделей пам’яті і сегментів, сумісних з Borland C++, при використанні спрощених директив визначення сегментів. Спрощені директиви визначення сегментів і borland c++ Директива .MODEL указує Турбо Асемблеру, що сегменти, створювані за допомогою спрощених директив визначення сегментів, повинні бути сумісні з обраною моделлю пам’яті (TINY - крихітної, SMALL - малої, COMPACT - компактної, MEDIUM - середньої, LARGE великий чи HUGE - величезної) і керує призначуваним за замовчуванням типом (FAR чи NEAR) процедур, створюваних по директиві PROC. Моделі пам’яті, визначені за допомогою директиви .MODEL, сумісні з моделями Borland C++ з відповідними іменами. Нарешті, спрощені директиви визначення сегментів .DATA, .CODE, .DATA?, .FARDATA, .FARDATA? і .CONST генерують сегменти, сумісні з Borland C++. Наприклад, розглянемо наступний модуль Турбо Асемблера з ім’ям DOTOTAL.ASM: .MODEL SMALL ; вибрати малу модель пам’яті ; (ближній код і дані) .DATA ; ініціалізація сегмента даних, ; сумісного з Borland C++ EXTRN _Repetitions:WORD ; зовнішній ідентифікатор PUBLIC _StartValue ; доступний для інших модулів _StartValue DW 0 .DATA? ; ініціалізований сегмент ; даних, сумісний з Borland C++ RunningTotal DW ? .CODE ; сегмент коду, сумісний з ; Borland C++ PUBLIC _DoTotal _DoTotal PROC ; функція (у малій моделі пам’яті ; викликається за допомогою виклику ; ближнього типу) mov cx,[_Repetitions] ; лічильник виконання mov ax,[_StartValue] mov [RunningTotal],ax ; задати початкове ; значення TotalLoop: inc [RunningTotal] ; RunningTotal++ loop TotalLoop mov ax,[RunningTotal] ; повернути кінцеве ; значення (результат) ret _DoTotal ENDP END Написана на Асемблері процедура _DoTotal при використанні малої моделі пам’яті може викликатися з Borland C++ за допомогою оператора: DoTotal(); Помітимо, що в процедурі DoTotal передбачається, що десь в іншій частині програми визначена зовнішня змінна Repetitions. Аналогічно, змінна StartingValue оголошена, як загальнодоступна, тому вона доступна в інших частинах програми. Наступний модуль Borland C++ (який називається SHOWTOT.CPP) звертається до даних у DOTOTAL.ASM і забезпечує для модуля DOTOTAL.ASM зовнішні дані: extern int StartingValue; extern int DoTotal(word); int Repetitions; main() { int i; Repetitions = 10; StartingValue = 2; print("%d\n", DoTotal()); } Щоб створити з модулів DOTOTAL.ASM і SHOWTOT.CPP виконувану програму SHOWTOT.EXE, введіть команду: bcc showtot.cpp dototal.asm Загальнодоступні і зовнішні ідентифікатори Програми Турбо Асемблера можуть викликати функції С++ і посилатися на зовнішні змінні СІ. Програми Borland C++ аналогічним чином можуть викликати загальнодоступні (PUBLIC) функції Турбо Асемблера і звертатися до змінних Турбо Асемблера. Після того, як у Турбо Асемблері встановлюються сумісні з Borland C++ сегменти (як описано у попередніх розділах), щоб спільно використовувати функції і змінні Borland C++ і Турбо Асемблера, потрібно дотримувати кілька простих правил. Підкреслення і мова СІ Якщо ви пишете мовою СІ чи С++, то всі зовнішні мітки повинні починатися із символу підкреслення (_). Компілятор СІ і С++ вставляє символи підкреслення перед всіма іменами зовнішніх функцій і змінних при їхньому використанні в програмі на СІ/С++ автоматично, тому вам потрібно вставити їх самим тільки в кодах асемблера. Ви повинні переконатися, що всі асемблерні звертання до функцій і змінних СІ починаються із символу підкреслення, і крім того, ви повинні вставити його перед іменами всіх асемблерних функцій і змінних, котрі робляться загальними і викликаються з програми мовою СІ/С++. Наприклад, наступна програма мовою СІ (link2asm.cpp): extrn int ToggleFlag(); int Flag; main() { ToggleFlag(); } правильно компонується з наступною програмою на Асемблері (CASMLINK.ASM): .MODEL SMALL .DATA EXTRN _Flag:word .CODE PUBLIC _ToggleFlag _ToggleFlag PROC cmp [_Flag],0 ; прапор скинутий? jz SetFlag ; так, установити його mov [_Flag],0 ; ні, скинути його jmp short EndToggleFlag ; виконано SetFlag: mov [_Flag],1 ; установити прапор EndToggleFlag: ret _ToggleFlag ENDP END При використанні в директивах EXTERN і PUBLIC специфікатора мови СІ правильно компонується з наступною програмою на Асемблері (CSPEC.ASM): .MODEL SMALL .DATA EXTRN C Flag:word .CODE PUBLIC C ToggleFlag ToggleFlag PROC cmp [Flag],0 ; прапор скинутий? jz SetFlag ; так, установити його mov [Flag],0 ; ні, скинути його jmp short EndToggleFlag ; виконано SetFlag: mov [Flag],1 ; установити прапор EndToggleFlag: ret ToggleFlag ENDP END Розпізнавання великих і малих символів в ідентифікаторах В іменах ідентифікаторів Турбо Асемблер звичайно не розрізняє рядкові і прописні букви (верхній і нижній регістр). Оскільки в С++ вони розрізняються, бажано задати таке розходження й у Турбо Асемблері (принаймні для тих ідентифікаторів, що спільно використовуються Асемблером і С++). Це можна зробити за допомогою параметрів /ML і /MX. Перемикач (параметр) командного рядка /ML приводить до того, що в Турбо Асемблері у всіх ідентифікаторах рядкові і прописні символи будуть розрізнятися (вважатися різними). Параметр командного рядка /MX указує Турбо Асемблеру, що рядкові і прописні символи (символи верхнього і нижнього регістра) потрібно розрізняти в загальнодоступних (PUBLIC) ідентифікаторах, зовнішніх (EXTRN) ідентифікаторах, глобальних (GLOBAL) ідентифікаторах і загальних (COMM) ідентифікаторах. У більшості випадків варто також використовувати параметр /ML. Типи міток Хоча в програмах Турбо Асемблера можна вільно звертатися до будь-який змінній чи даних будь-якого розміру (8, 16, 32 біти і т.д.), у загальному випадку добре звертатися до змінного відповідно до їхнього розміру. Наприклад, якщо ви записуєте слово в байтову змінну, те звичайно це приводить до проблем: . . . SmallCount DB 0 . . . mov WORD PTR [SmallCount],0ffffh . . . Тому важливо, щоб в операторі Асемблера EXTRN, у якому описуються змінні С++, задавався правильний розмір цих змінних, тому що при генерації розміру доступу до змінного С++ Турбо Асемблер ґрунтується саме на цих описах. Якщо в програмі мовою С++ міститься оператор: char c то код Асемблера: . . . EXTRN c:WORD . . . inc [c] . . . може привести до дуже неприємних помилок, оскільки після того, як у коді мовою С++змінна c збільшиться чергові 256 разів, її значення буде скинуто, а тому що вона описана, як змінна розміром у слово, то байт за адресою OFFSET c + 1 буде збільшуватися некоректно, що приведе до непередбачених результатів. Узгодження типів (СІ++ та Assembler) Між типами даних С++ а Асемблера існує наступне співвідношення: Тип даних С++ Тип даних Асемблера
unsigned char byte
char byte
enum word
unsigned short word
short word
unsigned int word
int word
unsigned long dword
long dword
float dword
double qword
long double tbyte
near* word
far* dword
Взаємодія між Турбо Асемблером і Borland C++ Тепер, коли ви розумієте, як потрібно будувати і компонувати сумісні з С++ модулі Асемблера, потрібно знати, який код можна поміщати у функції Асемблера, викликувані з С++. Тут потрібно проаналізувати три моменти: одержання переданих параметрів, використання регістрів і повернення значень у вихідну програму. Передача параметрів Borland C++ передає функціям параметри через стек. Перед викликом функції С++ спочатку заносить передані цієї функції параметри, починаючи із самого правого параметра і кінчаючи лівим, у стек. У С++ виклик функції: . . . Test(i, j, 1); . . . компілюється в інструкції: mov ax,1 push ax push word ptr DGROUP:_j push word ptr DGROUP:_i call near ptr _Test add sp,6 де видно, що правий параметр (значення 1), заноситься в стек першим, потім туди заноситься параметр j і, нарешті, і. При поверненні з функції занесені в стек параметри усе ще знаходяться там, але вони більше не використовуються. Тому безпосередньо після кожного виклику функції Borland C++ налаштовує вказівник стеку назад у відповідності зі значенням, що він мав перед занесенням у стек параметрів (параметри, таким чином, відкидаються). У попередньому прикладі три параметри (по два байти кожен) займають у стеку разом 6 байт, тому Borland C++ додає значення 6 до вказівника стека, щоб відкинути параметри після звертання до функції Test. Важливий момент тут полягає в тім, що відповідно до використовуваних за замовчуванням угод Си/C++ за видалення параметрів зі стеку відповідає викликаюча програма. Функції Асемблера можуть звертатися до параметрів, переданих у стеку, щодо регістра BP. Наприклад, припустимо, що функція Test у попередньому прикладі являє собою наступну функцію на Асемблері (PRMSTACK.ASM): .MODEL SMALL .CODE PUBLIC _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. (Якщо ви пам’ятаєте, BP адресується відносно сегмента стека.) Але звідки вона знає, де знайти параметри відносно BP? На рис.1 показано, як виглядає стек перед виконанням першої інструкції у функції Test: i = 25; j = 4; Test(1, j, 1); Рис. 1 Стан стеку перед виконанням першої інструкції функції Test Параметри функції Test являють собою фіксовані адреси відносно SP, починаючи з комірки, на два байти старшої від адреси, за якою зберігається адреса повернення, занесена туди при виклику. Після завантаження регістра BP значенням SP ви можете звертатися до параметрів відносно BP. Однак, ви повинні спочатку зберегти BP, тому що у викликаючій програмі передбачається, що при поверненні BP змінений не буде. Занесення в стек BP змінює всі зміщення в стеку. На рис. 2 показано стан стеку після виконання наступних рядків коду: . . . push bp mov bp,sp . . . Рис.2 Стан стеку після інструкцій PUSH і MOVE Організація передачі параметрів функції через стек і використання його для динамічних локальних змінних - це стандартний прийом для мови С++. Як можна помітити, неважливо, скільки параметрів має програма мовою С++: Самий лівий параметр завжди зберігається в стеку за адресою, що безпосередньо слідує за збереженою у стеку адресою повернення, наступний параметр, що повертається, зберігається безпосередньо після самого лівого параметра і т.д. Оскільки порядок і тип переданих параметрів відомі, їх завжди можна знайти в стеку. Простір для динамічних локальних змінних можна зарезервувати, віднімаючи від SP необхідну кількість байт. Наприклад, простір для динамічного локального масиву розміром у 100 байт можна зарезервувати, якщо почати функцію Test з інструкцій: . . . push bp mov bp,sp sub sp,100 . . . Використання директиви ARG У Турбо Асемблері передбачена директива ARG, за допомогою якої можна легко виконувати передачу параметрів у програмах на Асемблері. Директива ARG автоматично генерує правильні зміщення в стеку для заданих вами змінних. Наприклад: ARG FillArray:WORD, Count:WORD, FillValue:BYTE Тут задається три параметри: FillArray, параметр розміром у слово, Count, також параметр розміром у слово і FillValue - параметр розміром у байт. Директива ARG встановлює мітку FillArray у значення [BP+4] (мається на увазі, що код знаходиться в процедурі ближнього типу), мітку Count - у значення [BP+6], а мітку FillValue - у значення [BP+8]. Однак особливо важлива директива ARG тим, що ви можете використовувати визначені з її допомогою мітки не піклуючись про ті значення, у яких вони встановлені. Наприклад, припустимо, що у вас є функція FillSub яка викликається з С++у такий спосіб: extern "C" { void FillSub( char *FillArray, int Count, char FillValue); } main() { #define ARRAY_LENGTH 100 char TestArray[ARRAY_LENGTH]; FillSub(TestArray,ARRAY_LENGTH,’*’); } У FillSub директиву ARG для роботи з параметрами можна використовувати в такий спосіб: _FillSub PROC NEAR ARG FillArray:WORD, Count:WORD, FillValue:BYTE push bp ; зберегти вказівник стека ; програми, що викликає підпрограму mov bp,sp ; установити свій власний ; вказівник стека mov bx,[FillArray] ; одержати вказівник на ; заповнюваний масив mov cx,[Count] ; одержати заповнювану довжину mov al,[FillValue] ; одержати значення-заповнювач FillLoop: mov [bx],al ; заповнити символ inc bx ; посилання на наступний символ loop FillLoop ; обробити наступний символ pop bp ; відновити вказівник стека ; програми, що викликає підпрограму ret _FillSub ENDP Директива ARG автоматично враховує різні розміри повернень ближнього і далекого типу. Література: 1.Р.Джордейн.Справочник програмиста персональных компъютеров типа ІBM PC XT и AT. - M."Финансы и статистика",1992,стор.13-31. 2.Л.О.Березко,В.В.Троценко. Особливості програмування в турбо-асемблері. -Киів,НМК ВО,1992. 3.Л.Дао. Программирование микропроцессора 8088.Пер.с англ.-М."Мир",1988. 4.П.Абель.Язык ассемблера для ІBM PC и программирования. Пер. з англ.-М.,"Высшая школа",1992. ЗАВДАННЯ: Створити *.exe програму, яка реалізовує обчислення, заданого варіантом виразу. Програма повинна складатися з двох модулів, передача параметрів між якими може здійснюватися довільним чином. Керуючий модуль (основний) – написаний на мові СІ. Він повинен забезпечувати: ввід з клавіатури в десятковій формі зі знаком таких даних: довжини масиву – N (ціле значення) елементів масиву – аі (дійсне значення) змінних – c, d (дійсні значення) виклик підпрограми безпосередніх обчислень; вивід на екран вихідного масиву дійсних значень зі знаком в десятковій формі Xi . Модуль безпосередніх обчислень – здійснює всі необхідні арифметичні дії - на мові Assembler; В цьому модулі можливим є виконання арифметичних дій над багатобайтовими числами засобами мови СІ (тобто в модулі, написаному на Асемблері можуть викликатися СІ - функції). Переконатися у правильності роботи кожного модуля (утворити .obj файли обох програм) та програми загалом (об’єднати їх у єдиний виконавчий модуль програму типу .exe ) Скласти звіт про виконану роботу з приведенням тексту програми та коментарів до неї. ВАРІАНТИ ЗАВДАНЬ варіант завдання варіант завдання
1
16
2
17
3
18
4
19
5
20
6
21
7
22
8
23
9
24
10
25
11
26
12
27
13
28
14
29
15
30
Приклад виконання. Завдання: Написати програму, яка реалізовує задане варіантом обчислення і складається з модуля на мові СІ та модуля на мові Асемблер. Заданий вираз:
Виходячи з завдання, вхідними даними, що повинні вводитися під час виконання програми, є: довжина масиву – N – ціле невід’ємне число; елементи масиву – аі –дійсні числа; змінні – c, d – дійсні числа. Результатом роботи програми має бути вивід на екран вихідного масиву дійсних значень масиву Xi ., обрахованих за відповідною формулою. Оскільки, головний модуль має бути реалізовано на мові СІ, почати розробку доцільно саме з нього. Згідно до вимог завдання, засобами СІ слід реалізувати ввід даних, виклик підпрограми обчислення та вивід результату. Однак, для полегшення аналізу отриманих з асемблерного модуля результатів, доцільно продублювати самі обчислення і в головній програмі на СІ. Повний текст СІ – програми приведено нижче. Очевидно, що в ній присутні глобальні змінні, що визначають довжину масиву N, вхідні дані С і D, статично описують вхідний масив А та вихідні масиви Х і Х_asm для результатів отриманих засобами СІ та засобами Асемблера, відповідно, а також декларується зовнішня процедура extern void calculation(), що буде написана на Асемблері. Далі, здійснюється ввід даних за допомогою функції scanf(), вивід результатів обчислення на СІ, виклик асемблерної процедури та вивід її результатів. Типи даних вибрано таким чином, щоб уникнути можливого переповнення при обчисленнях, тобто якщо елементи вхідного масиву типу float (4 байти), то вихідний передбачає елементи вдвічі більшого діапазону, тобто double (8 байт). #include <stdlib.h> #include <stdio.h> #include <conio.h> #include <math.h> extern void calculation(); int N,i; float A[30]; double X[30]; double Xasm[30]; float C,D; int main() { printf("\n for C>D X[i]=(2*C*C-2*(D+A[i]))/(C/3+1)\n"); printf("\n for C<=D X[i]=(40-C/4)/(C/tg(D+A[i])\n"); printf("\n N="); scanf("%d",&N); for (i=0;i<N;i++){printf(" A[%d]=",i+1);scanf("%f",&A[i]);} printf("\n C="); scanf("%f",&C); printf("\n D="); scanf("%f",&D); if (C>D){ printf("\n%f > %f ----> X[i]=(2*C*C-2*(D+A[i]))/(C/3+1)",C,D); printf("\n \n CI result is: \n"); for (i=0;i<N;i++) {X[i]=(2*C*C-2*(D+A[i]))/(C/3+1); printf("\n X[%d]=%f",i+1,X[i]);} } else { printf("\n %f <= %f ----> X[i]=(40-C/4)/(C/tg(D+A[i])",C,D); for (i=0;i<N;i++) {X[i]=(40-C/4)/(C/tan(D+A[i])); printf("\n X[%d]=%f",i+1,X[i]);} } calculation(); printf("\n \n Assembler result is: \n"); for (i=0;i<N;i++){printf("\n X[%d]=%f",i+1,Xasm[i]);} getch(); return 0; } Асемблерна частина проекту повинна реалізовувати обчислення, згідно умов, що залежать від вхідних змінних C і D. Оскільки для всіх елементів масиву А, значення C і D не міняються, то доцільно винести обчислення, пов’язані з ними, в окремий блок, а не у загальний цикл. Для цього можна видозмінити задані формули, згідно правил математики: Для випадку C>D маємо: . Позначивши , отримуємо: . (1) Для випадку C<=D маємо: . Позначивши отримуємо: . (2) Таким чином, ввівши додаткові змінні в асемблерному модулі, які визначають константи «3»(К3), «6»(К6), «0.25»(К1_4), «40»(К40), легко обчислити отримані проміжні результати К1,К2, К1R, а вже у циклі використати їх для знаходження чергового елементу вихідного масиву _Xasm. Перевірка умови здійснюється шляхом використання команди fcomp математичного співпроцесора, яка встановлює ознаки в регістрі стану (SR). Далі ці ознаки записуються у регістр прапорців центрального процесора, через проміжну змінну О1 та регістр АХ. Оскільки співпроцесор не встановлює прапорця відповідного SF у FLAGS, то використання команд умовних переходів для знакових даних не дасть бажаного результату. Тому, використовується команда jbe. Основний цикл обчислень визначається за мітками L2 чи L3, залежно від значення умови, і здійснює перетворення за отриманими спрощеними формулами (1) чи (2). Оскільки вхідні дані визначені як DWORD , а результат як QWORD, то переміщення по масивах _А та _Xasm відбувається зі зміщенням на 4 та 8 відповідно. .MODEL SMALL EXTRN _N:WORD, _C:DWORD, _D:DWORD ,_A:DWORD, _Xasm:QWORD .DATAK3 dd 03.0 K6 dd 06.0 K1 dd 0.0 K2 dd 0.0 K1_4 dd 0.25 K40 dd 40.0 K1R dd 0.0 O1 dw 0 .CODE _calculation PROC PUBLIC _calculation finit XOR SI,SI XOR DI,DI mov cx,_N mov ax,O1 sahf FLD _C fcomp _D fstsw O1 mov ax,O1 sahf jbe C_l_e_D FLD _C FADD K3 FSTP K3 FLD _C FMUL _C FSUB _D FMUL K6 FDIV K3 FSTP K1 FLD K6 FDIV K3 FSTP K2 mov cx,_N L2: FLD K1 FLD _A[di] FMUL K2 FSUB fstp _Xasm[si] ADD di,4 ADD si,8 LOOP L2 jmp exit C_l_e_D: FLD K40 FDIV _C FSUB K1_4 FSTP K1R L3: FLD _A[di] FADD _D FPTAN FDIV FMUL K1R fstp _Xasm[si] ADD di,4 ADD si,8 LOOP L3 exit: ret _calculation ENDP END