Міністерство освіти, науки, молоді та спорту України
Національний університет “Львівська політехніка”
Кафедра ЕОМ
Звіт
до лабораторної роботи №2
на тему: «Змішане програмування на мовах С та Асемблер»
з предмету: «Системне програмування»
Підготувала:
ст.гр. КІ-33
Надолинська Ольга
Перевірив:
Олексів М.В.
Львів 2012
Мета: оволодіти навиками створення програм, частини яких написані різними мовами програмування Засвоїти правила взаємодії різних модулів.
ТЕОРЕТИЧНІ ВІДОМОСТІ
Труднощі опису зв'язку програм мовою 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
Розпізнавання рядкових і прописних символів в ідентифікаторах
В іменах ідентифікаторів Макро асемблер звичайно не розрізняє рядкові і прописні букви (верхній і нижній регістр). Оскільки в С++ вони розрізняються, бажано задати таке розходження й у Макро Асемблері (принаймні для тих ідентифікаторів, що спільно використовуються Асемблером і С++). Це можна зробити за допомогою макрокоманди 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)
Між типами даних С++ та Макро асемблера існує наступне співвідношення:
Тип даних С++
Тип даних Макро Асемблера
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 адресується відносно сегмента стека). Але звідки вона знає, де знайти параметри відносно EBP?
На рис.1 показано, як виглядає стек перед виконанням першої інструкції у функції Test:
i = 25;
j = 4;
Test(1, j, 1);
Рис. 1. Стан стеку перед виконанням першої інструкції функції Test
Параметри функції Test являють собою фіксовані адреси відносно ESP, починаючи з комірки, на 4 байти старшої від адреси, за якою зберігається адреса повернення, занесена туди при виклику. Після завантаження регістра EBP значенням ESP ви можете звертатися до параметрів відносно EBP. Однак, ви повинні спочатку зберегти EBP тому, що у викликаючій програмі передбачається, що при поверненні EBP змінений не буде. Занесення в стек EBP змінює всі зміщення в стеку. На рис. 2 показано стан стеку після виконання наступних рядків коду:
...
push ebp
mov ebp,esp
...
Рис.2 Стан стеку після інструкцій PUSH і MOVE
Організація передачі параметрів функції через стек і використання його для динамічних локальних змінних - це стандартний прийом для мови С++. Як можна помітити, неважливо, скільки параметрів має програма мовою С++: Самий лівий параметр завжди зберігається в стеку за адресою, що безпосередньо слідує за збереженою у стеку адресою повернення, наступний параметр, що повертається, зберігається безпосередньо після самого лівого параметра і т.д. Оскільки порядок і тип переданих параметрів відомі, їх завжди можна знайти в стеку.
Простір для динамічних локальних змінних можна зарезервувати, віднімаючи від ESP необхідну кількість байт. Наприклад, простір для динамічного локального масиву розміром у 100 байт можна зарезервувати, якщо почати функцію Test з інструкцій:
. . .
push ebp
mov ebp,esp
sub esp,100
. . .
Повернення значень
Програми, які викликаються з С++ і написані на Асемблері можуть повертати значення. Значення функцій повертаються в такий спосіб:
тип що повертає значения
Де перебуває значення, що повертається
unsigned char
EAX
char
EAX
enum
EAX
unsigned short
EAX
short
EAX
unsigned int
EAX
int
EAX
unsigned long
EAX
long
EAX
float
регістр вершини стека співпроцесора 8087 (ST(0))
double
регістр вершини стека співпроцесора 8087 (ST(0))
long double
регістр вершини стека співпроцесора 8087 (ST(0))
near*
EAX
Варіант:
21
X=A4/8+2*(D2 – E1+K)
21
Код програми:
C-ASM
Calc.asm
.386
.model flat,c
EXTRN A:SDWORD, D:SDWORD, E:SDWORD, X:SDWORD
.data
K dd 21h;
format db "X = %d\n"
tmp1 dd 0
tmp2 dd 0
.code
calc PROC
mov ecx,8
mov edx,0
mov eax,A
idiv ecx
mov tmp1,eax
mov eax,D
sub eax,E
add eax,K
mov edx,2
imul edx
mov tmp2,eax
mov eax,tmp1
add eax,tmp2
mov X, eax
ret
calc ENDP
END
Main.cpp
//X=A4/8+2*(D2 – E1+K) K=21h
#include <stdio.h>
#include<conio.h>
extern "C" void calc(void);
extern "C"
{
int A=0;
short D=0;
short E=0;
int X=0;
};
int main()
{
printf("Please, enter your numbers:\n");
printf("A = ");
scanf("%d",&A);
printf("D = ");
scanf("%d",&D);
printf("E = ");
scanf("%d",&E);
calc();
printf("X = %d\n",X);
getch ();
return 0;
}
Результати виконання програми:
C-ASM-C
Main.cpp
#include <stdio.h>
#include<conio.h>
extern "C" void calc(void);
extern "C"
{
int A=0;
short D=0;
short E=0;
int X=0;
};
int main()
{
printf("Please, enter your numbers:\n");
printf("A = ");
scanf("%d",&A);
printf("D = ");
scanf("%d",&D);
printf("E = ");
scanf("%d",&E);
calc();
getch ();
return 0;
}
Calc.asm
.386
.model flat,c
EXTRN A:SDWORD, D:SDWORD, E:SDWORD, X:SDWORD
extern printf: proc
.data
K dd 21h;
format db "X = %d",10,0
tmp1 dd 0
tmp2 dd 0
outdata dd ?
.code
calc PROC
mov ecx,8
mov edx,0
mov eax,A
idiv ecx
mov tmp1,eax
mov eax,D
sub eax,E
add eax,K
mov edx,2
imul edx
mov tmp2,eax
mov eax,tmp1
add eax,tmp2
mov X, eax
mov outdata,eax
push outdata
push offset format
call printf
add esp,8
ret
calc ENDP
END
Результати виконання програми:
Висновок: під час виконання даної лабораторної роботи я оволоділа навиками створення програм, частини яких написані різними мовами програмування Засвоїла правила взаємодії різних модулів.