Міністерство освіти і науки України
Національний університет “Львівська політехніка”
/
Лабораторна робота № 2
Тема:
Змішане програмування на мовах С та Асемблер
Мета: оволодіти навиками створення програм, частини яких написані різними мовами програмування. Засвоїти правила взаємодії між програними модулями.
ТЕОРЕТИЧНІ ВІДОМОСТІ
Труднощі опису зв’язку програм написаних мовою C і асемблерних програм полягає в тому, що різні версії мови C мають різні угоди з виклику функцій та передавання їм параметрів. Для більш точної інформації варто користатися посібником з наявної версії мови C. До особливостей угод щодо виклику функцій та передавання їм параметрів можна віднести:
більшість версій мови C забезпечують передачу параметрів через стек у зворотній (у порівнянні з іншими мовами) послідовності. Зазвичай доступ, наприклад, до двох параметрів, переданих через стек, здійснюється в
такий спосіб:
PUSH EBP
MOV EBP,ESP
MOV EAX,[EBP+8]
MOV EDX,[EBP+12]
; тіло підпрограми ...
POP EBP
RET
у мові C розрізняють великі і малі літери, тому ім’я асемблерного модуля повинне бути представлено в тому ж символьному регістрі, який використовують для посилання C-програми;
у деяких версіях мови C потрібно, щоб асемблерні програми, що змінюють регістри EDI і ESI, записували їхній вміст у стек при вході і відновлювали ці значення зі стеку при виході;
ассемблерні програми повинні повертати значення, якщо це необхідно, у регістрі EAX (подвійне слово) чи в регістровій парі EDX:EAX (8 слів);
для деяких версій мови C, якщо ассемблерна програма встановлює прапор DF, то вона повинна скинути його командою CLD перед поверненням. Щоб скомпонувати разом модулі C++ і Макро асемблера, повинні бути
дотримані наступні три умови:
У модулях Макро Асемблера повинні використовуватися угоди про імена, прийняті в C++.
C++ і Макро Асемблер повинні спільно використовувати відповідні функції й імена змінних у формі, прийнятної для C++.
Для комбінування модулів у виконувану програму потрібно використовувати утіліту-компоновщик (TLINK, LINK тощо).
Підкреслення і мова С
Для забезпечення взаємодії програми написаної асемблером з програмами написаними мовами С чи С++, всі зовнішні мітки повинні починатися із символу підкреслення (_). Компілятор С і С++ вставляє символи підкреслення перед всіма іменами зовнішніх функцій і змінних при трансляції в об’єктний код автоматично, тому в асемблерних програмах перший символ підкреслення необхідно вставити самостійно. Тому, ідентифікатори глобальних змінних та назви функцій які використовуються в асемблерній програмі для взаємодії з С модулями, чи навпаки, повинні починатися із символу підкреслення.
Наприклад, наступна програма мовою С (link2asm.cpp):
extrn int ToggleFlag(); int Flag;
main()
{
ToggleFlag();
}
правильно компонується з наступною програмою на асемблері (casmlink.asm):
.586
.MODEL FLAT
.DATA
EXTRN _Flag:dword
.CODE
PUBLIC _ToggleFlag
_ToggleFlag PROC
; прапорець скинутий?
cmp [_Flag], 0
jz SetFlag
; так, установити його
mov [_Flag], 0
; ні, скинути його
jmp EndToggleFlag
; виконано
SetFlag:
; встановити прапорець
mov [_Flag], 1
EndToggleFlag:
ret
_ToggleFlag ENDP
END
При використанні в директивах EXTERN і PUBLIC специфікатора мови С правильно компонується з наступною програмою на Асемблері (cspec.asm) (приклад для 16-ти бітної програми):
.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
Примітка: Мітки, на які відсутні посилання в програмі на С (такі, як SetFlag) не вимагають попередніх символів підкреслення. Турбо Асемблер (використовується тільки для 16-ти бітних програм) автоматично при записі імен Flag і ToggleFlag в об’єктний файл помістить перед ними символ підкреслення.
Розпізнавання рядкових і прописних символів в ідентифікаторах
В іменах ідентифікаторів Макро асемблер звичайно не розрізняє малі та великі літери (верхній і нижній регістр). Оскільки в С вони розрізняються, бажано задати таке розходження й у Макро Асемблері (принаймні для тих ідентифікаторів, що спільно використовуються Асемблером і С). Це можна зробити за допомогою макрокоманди OPTION CASEMAP:NONE.
Типи міток
Хоча в програмах Асемблера можна вільно звертатися до будь-якої змінної чи даних довільного розміру (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)
Між типами даних С та Макро асемблера існує співвідношення, що наведене в табл. 2.1.
Узгодження між типами С і Assembler
Тип даних С++
Тип даних Асемблера
unsigned char
byte
char
byte
enum
dword
unsigned short
dword
short
dword
unsigned int
dword
int
dword
unsigned long
dword
long
dword
float
dword
double
qword
long double
tbyte
near*
dword
Передача параметрів
У мові C функціям параметри передаються через стек. Перед викликом функції компілятор С спочатку заносить у стек передані цієї функції параметри, починаючи із самого правого параметра і закінчуючи лівим. У С виклик функції:
...
Test(i, j, 1);
...
компілюється в інструкції:
mov eax,1 push eax
push dword ptr _j push dword ptr _i call _Test
add esp,12
де видно, що правий параметр (значення 1), заноситься в стек першим, потім туди заноситься параметр j та, нарешті, і.
При поверненні з функції занесені в стек параметри усе ще знаходяться там, але вони більше не використовуються. Тому безпосередньо після кожного виклику функції C прокручує вказівник стеку назад у відповідності зі значенням, що він мав перед занесенням у стек параметрів (параметри, таким чином, відкидаються). У попередньому прикладі три параметри (по два байти кожен) займають у стеку разом 12 байт, тому компілятор C додає значення 12 до вказівника стеку, щоб відкинути параметри після звертання до функції Test. Важливий момент полягає в тому, що відповідно до використовуваних за замовчуванням угод С/C++ за видалення параметрів зі стеку відповідає викликаюча програма.
Функції Асемблера можуть звертатися до параметрів, переданих через стек, з використанням регістру EBP. Наприклад, функція Test може мати таку реалізацію на мові Асемблера (prmstack.asm):
.586
.MODEL FLAT
.CODE PUBLIC _Test _Test PROC
push ebp mov ebp,esp
mov eax,[ebp+8] ; одержати параметр 1
add eax,[ebp+12] ; додати параметр 2 до параметра 1 sub eax,[ebp+16] ; відняти від суми параметр 3
pop ebp
ret
_Test ENDP
Функція Test одержує доступ до переданих з програми мовою С параметрів через стек, з використанням регістру EBP. (EBP, це регістр для базової адресації за замовчуванням).
На рис. 2.1 показано, як виглядає стек перед виконанням першої інструкції
функції Test.
= 25;
= 4; Test(1, j, 1);
ESP
Адреса повернення
ESP + 4
25 (_i)
ESP + 8
4 (_j)
ESP + 12
1
Рис. 2.1. Стан стеку перед виконанням першої інструкції функції Test
Параметри функції Test мають фіксовані адреси відносно ESP, починаючи з комірки, на 4 байти старшої від адреси, за якою зберігається адреса повернення, що занесена туди при виклику. Завантаження регістра EBP значенням ESP дає можливість звертатися до цих параметрів відносно EBP. Однак, спочатку необхідно зберегти у стеку значення EBP для того, щоб забезпечити його відновлення при поверненні з функції, тому що викликаюча програма передбачає, що при поверненні EBP змінений не буде. Занесення в стек EBP змінює всі зміщення в стеку. На рис. 2.2 показано стан стеку після виконання наступних рядків коду:
...
push ebp
mov ebp, esp
...
Організація передачі параметрів функції через стек і використання його для динамічних локальних змінних - це стандартний прийом для мови С. Як можна помітити, неважливо, скільки параметрів має програма мовою С: крайній лівий параметр завжди зберігається в стеку за адресою, що безпосередньо слідує за збереженою у стеку адресою повернення, наступний параметр, що передається, зберігається безпосередньо після крайнього лівого параметра і т.д. Оскільки порядок і тип переданих параметрів відомі, їх завжди можна знайти в стеку.
Простір для локальних змінних можна зарезервувати, віднімаючи від ESP необхідну кількість байт. Наприклад, простір для локального масиву розміром у 100 байт можна зарезервувати, якщо почати функцію Test з інструкцій:
push ebp
mov ebp,esp
sub esp,100
ESP
EBP
EBP викликаючої програми
ESP + 4
Адреса повернення
EBP + 4
ESP + 8
25 (_i)
EBP + 8
ESP + 12
4 (_j)
EBP + 12
ESP + 16
1
EBP + 16
Рис. 2.2 Стан стеку після інструкцій PUSH і MOVE
Тоді, до локальних даних у зарезервованому адресному провторі в стеку можна і далі звертатися чарез регістр EBP, але уже з від’ємним зміщення. При виході з підпрограми також необхідно відкоректувати вказівник стеку.
Повернення значень
Програми, які викликаються з С і написані на Асемблері можуть повертати значення. Значення функцій повертаються через напередвизначені регістри, що відображені у табл. 2.2.
Розташування резутратів виконання функцій
Тип значения,
Розміщення значення,
що повертається функцією
що повертається функцією
unsigned char
EAX
char
EAX
enum
EAX
unsigned short
EAX
short
EAX
unsigned int
EAX
int
EAX
unsigned long
EAX
long
EAX
Виконання лабораторної роботи:
Завдання
1.Створити дві програми. Перша програма реалізує взаємовиклики С – ASM та здійснює обчислення, заданого виразу, згідно варіанту (Табл. 2.3). Програма повинна складатися з кількох модулів, передача параметрів між якими здійснюється через стек. Константа передається через спільну пам’ять.
Основний модуль – створюється мовою С. Він повинен забезпечувати:
ввід даних з клавіатури;
виклик підпрограми обчислення виразу;
вивід на екран результату обчислення виразу.
Модуль безпосередніх обчислень – здійснює всі обчислення виразу.
Створюється мовою Assembler
2.Друга програма реалізує взаємовиклики С – ASM – С та здійснює обчислення, заданого виразу, згідно варіанту. Програма повинна складатися з
кількох модулів, передача параметрів між якими здійснюється через стек.
Основний модуль – створюється мовою С. Він повинен забезпечувати:
ввід даних з клавіатури;
виклик підпрограми обчислення виразу;
Модуль безпосередніх обчислень – здійснює всі обчислення і вивід на екран результату обчислення виразу викликом стандартної функції printf() . Створюється мовою Assembler.
3.Відлагодити та протестувати програми. Результати роботи програм продемонструвати викладачу.
Варіант №3
X = K-B2 *5+C2-E1; K = 37788663
Завдання №1(C-ASM):
Фото виконання програми:
/
Код програми(C-ASM):
Source.cpp
#include<stdio.h>
#include<conio.h>
extern "C" int pruklad(int,int,char);
extern "C"{
short B = 0, C = 0;
char E = 0;
}
int main(){
int K = 0x37788663;
printf("X = K - B2 * 5 + C2 - E1 where K = 37788663");
printf("\n");
printf("Char(E) from 0 to 255\n");
printf("Enter numbers:\n");
printf("Enter B = ");
scanf("%d", &B);
printf("Enter C = ");
scanf("%d", &C);
printf("Enter E = ");
scanf("%d", &E);
printf("Result in C(from_C): %d ",K-B*5+C-E );
printf("\n");
int X = pruklad(B,C,E);
printf("Result in C from ASM(from_C) %d", X);
printf("\n");
getch();
return 0;
}
Assembler.asm
.386
.model flat,c
option casemap:none
PUBLIC pruklad
.data
_T1 dword 0
_T2 dword 0
_K EQU 37788663h
.code
pruklad PROC
push ebp
mov ebp,esp
;T1 = B2*5
mov eax, dword ptr[ebp+8]
shl eax,2
add eax, dword ptr[ebp+8]
mov _T1,eax
;T1 = K-T1
mov eax,_K
sub eax,_T1
mov _T1,eax
;T2 = C2-E1
mov eax, dword ptr[ebp+12]
sub eax, dword ptr[ebp+16]
mov _T2,eax
;T1 = T1+T2
mov eax,_T1
add eax,_T2
mov _T1,eax
pop ebp
ret
pruklad ENDP
END
Завдання№2(C-ASM-C):
Фото роботи програми:/
Код програми(C-ASM-С):
Source.cpp
#include<stdio.h>
#include<conio.h>
extern "C" int pruklad(int,int,char);
extern "C"{
short B = 0, C = 0;
char E = 0;
}
int main(){
int K = 0x37788663;
printf("X = K - B2 * 5 + C2 - E1 where K = 37788663");
printf("\n");
printf("Char(E) from 0 to 255\n");
printf("Enter numbers:\n");
printf("Enter B = ");
scanf("%d", &B);
printf("Enter C = ");
scanf("%d", &C);
printf("Enter E = ");
scanf("%d", &E);
printf("Result in C(from_C): %d ",K-B*5+C-E );
printf("\n");
pruklad(B,C,E);
getch();
return 0;
}
Assembler.asm
.386
.model flat,c
option casemap:none
EXTRN printf:near
PUBLIC pruklad
.data
_T1 dword 0
_T2 dword 0
_K EQU 37788663h
prnt db "Result in ASM(from_ASM): %d",10,13,0
.code
pruklad PROC
push ebp
mov ebp,esp
;T1 = B2*5
mov eax, dword ptr[ebp+8]
shl eax,2
add eax, dword ptr[ebp+8]
mov _T1,eax
;T1 = K-T1
mov eax,_K
sub eax,_T1
mov _T1,eax
;T2 = C2-E1
mov eax, dword ptr[ebp+12]
sub eax, dword ptr[ebp+16]
mov _T2,eax
;T1 = T1+T2
mov eax,_T1
add eax,_T2
mov _T1,eax
;cout result
mov edx,offset prnt
push eax
push edx
call near ptr printf
pop edx
pop eax
pop ebp
ret
pruklad ENDP
END
Словесний опис алгоритму роботи програми:
Створюємо прототип функції pruklad дописуємо до неї ключове слово extern аби доступ до неї міг здійснюватись з інших файлів, у нашому випадку файл Assembler.asm. У блоці extern ”C” описуємо змінні, які будуть використовуватись у асемблерному файлі, доступ до них буде здійснюватись через стек. За допомггою scanf здійснюємо зчитування даних з клавіатури і записуємо їх у відповідні змінні, які в подальшому будуть викорситовуватись у файлі програми асемблера. Слід звернути увагу на те як здійснюється операція множення на 5:
mov eax, dword ptr[ebp+8]
shl eax,2
add eax, dword ptr[ebp+8]
mov _T1,eax
В регістр eax ми заносимо число зі стеку(В), потім робимо зсув вліво на два(множення на 4), після чого до результату зсуву додаємо значення змінної яка зберігається в стеку (add eax,dword ptr [ebp+8]), після множення числа на 5 записуємо результат у зміну _Т1. Даний фрагмент коду використовується для виведення даних з асемблерівського файлу.
mov edx,offset prnt
push eax
push edx
call near ptr printfa
pop edx
pop eax
mov eax,_K
sub eax,_T1
mov _T1,eax
Фрагмент коду під номером 2 використовується для віднімання чисел К та тимчасової змінної Т1 через регістр eax(акумулятор).
Особливість завдання №1:
Завдання №1 відрізняється від завдання №2 тим, що у першому ми викликаємо функцію pruklad, яка повертає значення у змінну Х, і потім здійснюємо вивід результату.
Особливість завдання №2:
Завдання №2 відрізняється від завдання №1 тим що вивід результату відбувається з асемблерного файлу за допомогою команди printf() мови С.
Висновок: Після виконання даної лабораторної роботи я оволоділа навиками створення програм, частини яких написані різними мовами програмування. Засвоїла правила взаємодії між програними модулями.