Cистемне програмування
ЛАБОРАТОРНА РОБОТА №1, 2.
Змішане програмування на мовах СІ та Асемблер.
Мета: Оволодіти навиками створення програм, частини яких написані різними мовами програмування Засвоїти правила взаємодії різних модулів.
Труднощі опису зв'язку програм мовою 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 перед поверненням.
Щоб скомпонувати разом модулі C++ і Асемблера, повинні бути дотримані наступні три пункти:
У модулях Асемблера повинні використовуватися угоди про імена, прийняті в C++.
C++ і Асемблер повинні спільно використовувати відповідні функції й імена змінних у формі, прийнятної для C++.
Для комбінування модулів у виконувану програму потрібно використовувати утіліту-компоновщик (TLINK, LINK тощо).
Підкреслення і мова СІ
Якщо ви пишете мовою СІ чи С++, то всі зовнішні мітки повинні починатися із символу підкреслення (_). Компілятор СІ і С++ вставляє символи підкреслення перед всіма іменами зовнішніх функцій і змінних при їхньому використанні в програмі на СІ/С++ автоматично, тому вам потрібно вставити їх самим тільки в кодах асемблера. Ви повинні переконатися, що всі асемблерні звертання до функцій і змінних СІ починаються із символу підкреслення, і крім того, ви повинні вставити його перед іменами всіх асемблерних функцій і змінних, котрі робляться загальними і викликаються з програми мовою СІ/С++.
Наприклад, наступна програма мовою СІ (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


Передача параметрів
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 і, нарешті, і.
При поверненні з функції занесені в стек параметри усе ще знаходяться там, але вони більше не використовуються. Тому безпосередньо після кожного виклику функції C++ налаштовує вказівник стеку назад у відповідності зі значенням, що він мав перед занесенням у стек параметрів (параметри, таким чином, відкидаються). У попередньому прикладі три параметри (по два байти кожен) займають у стеку разом 6 байт, тому 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.

ЗАВДАННЯ:
Створити програму, яка реалізовує обчислення, заданого виразу, згідно варіанту.
Програма повинна складатися з кількох модулів, передача параметрів між якими здійснюється через стек.
Основний модуль – створюється мовою С.
Він повинен забезпечувати:
ввід даних з клавіатури;
виклик підпрограми обчислення виразу;
вивід на екран результату обчислення виразу.
Модуль безпосередніх обчислень – здійснює всі обчислення виразу і створюється мовою Assembler;
Відлагодити та протестувати програму. Результати роботи програми продемонструвати викладачу.
Скласти звіт про виконану роботу з приведенням тексту програми та коментарів до неї, а також результатів її роботи.
Примітка. В лабораторній роботі № 2 використати взаємовиклики С – ASM – С. Для виводу результатів обчислень забезпечити виклик стандартної функції printf() із модуля ASM .
ВАРІАНТИ ЗАВДАНЬ
А, В, С, D, E, F - знакові операнди, довжиною в байтах, згідно з індексу,
значення К подано у 16-му форматі.

Вираз
K

1
X=A2+C1-D2+K
1254021

2
X=A4+C3-D1-K
202

3
X=K-B2+C2-E1
37788663

4
X=A4+C1-D4+K
45694

5
X=B4-A2*2-E2+K
505

6
X=K+B2/4-D2*4-E1
6DD02316

7
X=A4/2-4*(D1+E2-K)
717

8
X=A4-B2+K-D2/2+8*B2
88

9
X=4*B2-C2+D4/4
29

10
X=A4-B4/2+K+E2*4
2310

11
X=(A4-B3-K)*2+E4/4
311

12
X=K+B4/2-4*F2-E1
7055E0AC

13
X=A2/2+8*(D1+E2-K)
2513

14
X=A4-B1-K-D2/2+4*B1
614

15
X=A3+(4*C2)-D4/2+K
4569600F

16
X=A4/4+C2-D1*2+K
616

17
X=A4-K+C4/2-E1*8
1017

18
X=4*(B2-C1)+D2/4+K
56987018

19
X=A2*4+C1-D4/2+K
4019

20
X=K+B4/4-D1*2-E2
18932020

21
X=A4/8+2*(D2 – E1+K)
21

22
X=K-B1-C1-D2/2+4*B1
45781022

23
X=A2*8-C1+D4/2+K
7AA02023

24
X=K-B2/2+D3+E2*4
74569024

25
X=(K-B2-C1)*2+E4/42
2B05025

26
X=A2+K+C2/2-E1*8
6C26

27
X=A2*4+(K-E1*4)
A77627

28
X=K+B4/2+D3-E2/4
3FF28

29
X=K-B1*4+D2-F2/2
12A0C029

30
X=K+B3-D2/2+E1*4
25630

Порядок виконання
Запустити середовище розробки Microsoft Visual Studio 2005, для чого потрібно виконати: Start-> All programs->Microsoft Visual Studio 2005 -> Microsoft Visual Studio 2005
Створити новий проект:
2.1. В закладці File вибрати New - > Project (Рис. 1)

Рис. 1. Створення нового проекту в середовищі Microsoft Visual Studio 2005
2.2. Вибрати тип проекту Visual C++ та шаблон Win32 Console Application (Рис. 2) та натиснути

Рис. 2. Вибір типу проекту
У вікні, що з’явиться натиснути клавішу (Рис. 3)

Рис. 3. Інформаційне вікно, що вказує тип вибраного проекту
У додаткових опціях проекту встановити тип «Empty project» (Рис. 4) та натиснути .

Рис. 4. Встановлення параметрів проекту
У вікні менеджера проекту буде відображатись структура створеного проекту, зображена на рис. 5.

Рис. 5. Менеджер проекту
3. До створеного проекту додати файли вихідних текстів програми. Для цього потрібн натиснути праву клавішу миші на іконці проекту та у спливаючому вікні вибрати Add -> New Item (Рис. 6)
4. На основі наведених в методичних вказівках кодів програм підготувати два файли з розширеннями .срр та .asm.Для цього можна використати будь-який тексів редактор (наприклад Notepad). Зауваження: створенні файли повинні мати різні імена.
5. Додати створенні файли до проекту Microsoft Visual Studio 2005 так, як наведено на рис. 6-7.

Рис. 6. Додавання існуючих файлів з вихідними кодами
При додавання файлу з розширенням .asm у Microsoft Visual Studio 2005 з’явиться вікно, що дозволяє встановити правила компіляції програм, написаних мовою програмування assembler (Рис. 7). Вибрати єдине доступне правило та натиснути .

Рис. 7. Вибір правила компіляції асемблерних файлів
6. Скомпілювати створений проект, використавши вкладку build (рис. 7).
7. Модифікувати тестову програму відповідно до заданого варіанту, скомпілювати та запустити на виконання.
Приклад змішаної програми, що виконує додавання двох цілих чисел
1.cpp
2.asm

#include <stdio.h>
extern "C" void calc(void);

extern "C"
{
int A=0;
int B=0;
int X=0;
};
int main()
{
printf("Please, enter your numbers:\n");
printf("A = ");
scanf("%d",&A);
printf("B = ");
scanf("%d",&B);
calc();
printf("X = %d\n",X);
return 0;
}
.386
.model flat,c
EXTRN A:SDWORD, B:SDWORD, X:SDWORD
.data
Afl dd 0
Bfl dd 0
format db "X = %d\n"
.code
calc PROC
mov eax,A
mov ebx,B
add eax, ebx
mov X, eax
ret
calc ENDP
END