системне програмування
Практична робота № 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 автоматично враховує різні розміри повернень ближнього і далекого типу.
Повернення значень
Програми, які викликаються з СІ++ і написані на Асемблері можуть повертати значення. Значення функцій повертаються в такий спосіб:
тип що повертає значения
Де перебуває значення, що повертається

unsigned char
AX

char
AX

enum
AX

unsigned short
AX

short
AX

unsigned int
AX

int
AX

unsigned long
DX: AX

long
DX: AX

float
регістр вершини стека співпроцесора
8087 (ST(0))

double
регістр вершини стека співпроцесора
8087 (ST(0))

long double
регістр вершини стека співпроцесора
8087 (ST(0))

near*
AX

far*
DX: AX


У загальному випадку 8- і 16-бітові значення вертаються в регістрі AX, а 32-бітові значення - в AX:DX (при цьому старші 16 біт значення перебувають у регістрі DX). Значення із плаваючою крапкою які вертаються в регістрі ST(0), що являє собою регістр вершини стека співпроцесора 8087 або емулятора співпроцесора 8087, якщо використається емулятор операцій із плаваючою крапкою
Виклик Borland C++ з Турбо Асемблера
Хоча більше прийнято для виконання спеціальних завдань викликати із СІ++ функції, написані на Асемблері, іноді вам може знадобитися викликати з Асемблера функції, написані мовою СІ++. Виявляється, насправді легше викликати функцію Borland C++ з функції Турбо Асемблера, ніж навпаки, оскільки з боку Асемблера не потрібно відслідковувати границі стеку. Розглянемо вимоги для виклику функцій Borland C++ з Турбо Асемблера.
Компонування з кодом ініціалізації СІ++ . Добрим правилом є виклик бібліотечних функцій Borland C++ тільки з Асемблера в програмах, які компонуються з модулем ініціалізації СІ++ (використовуючи його в якості першого модуля при компонуванні). Цей "надійний" клас містить у собі всі програми, які компонуються за допомогою командного рядка TC.EXE або TCC.EXE, і програми, у якості першого файлу компонування яких використається файл C0T, C0S, C0C, C0M, C0L або C0H.
У загальному випадку не слід викликати бібліотечні функції Borland C++ із програм, які не компонуються з модулем ініціалізації Borland C++, оскільки деякі бібліотечні функції Borland C++ не будуть правильно працювати. Виклик обумовлених користувачем функцій СІ++, які в свою чергу викликають бібліотечні функції мови СІ++, попадають в ту ж категорію, що й безпосередній виклик бібліотечних функцій СІ++. Відсутність коду ініціалізації СІ++ може викликати помилки у будь-якій програмі Асемблера, що прямо або побічно звертається до бібліотечних функцій СІ++.
Задання сегменту Необхідно забезпечувати, щоб Borland C++ і Турбо Асемблер використовували ту саму модель пам'яті, і щоб сегменти, які ви використовуються в Турбо Асемблері, збігалися з тими сегментами, які використовує Borland C++. У Турбо Асемблері є модель пам'яті tchuge,що підтримує модель huge Borland C++. Треба не засувати про використання директиви EXTRN для зовнішніх ідентифікаторів.
Виконання виклику
Усе, що потрібно для передачі параметрів у функцію C++, це занесення в стек самого правого параметра першим, потім наступного один за одним, поки в стеку не буде самий лівий параметр. Після цього потрібно просто викликати функцію. Наприклад, при програмуванні на Borland C++ для виклику бібліотечної функції Borland C++ strcpy для копіювання рядка SourceString у рядок DestString можна ввести:
strcpy(DestString, SourceString);
Для виконання того ж виклику на Асемблері потрібно використати інструкції:
lea ax,SourceString ; правий параметр
push ax
lea ax,DestString ; лівий параметр
push ax
call _strcpy ; скопіювати рядок
add sp,4 ; відкинути параметри
При настроюванні SP після виклику не забувайте очищати стек від параметрів.
Можна спростити ваш код і зробити його незалежним від мови, скориставшись розширенням команди Турбо Асемблера CALL:
call призначення [мова [,аргумент_1] ...]
де "мова" - це C, PASCAL, BASIC, FORTRAN, PROLOG або NOLANGUAGE, а "аргумент_n" це будь-який припустимий аргумент програми, що може бути прямо поміщений у стек процесора.
Використовуючи даний засіб, можна записати:
lea ax,SourceString
lea bx,DestString
call strcpy c,bx,ax
Турбо Асемблер автоматично вставить команди занесення аргументів у стек у послідовності, прийнятій в СІ++ (спочатку AX, потім BX), виконає виклик _strcopy (перед іменами СІ++ Турбо Асемблер автоматично вставляє символ підкреслення), і очищає стек після виклику.
Функції СІ++ зберігають наступні регістри (і тільки їх): SI, DI, BP, DS, SS, SP і CS. Регістри AX, BX, CX, DX, ES і прапори можуть довільно змінюватися.
Виклик з Турбо Асемблера функції Borland C++
Одним з випадків, коли може знадобитися викликати з Турбо Асемблера функцію Borland C++, є необхідність виконання складних обчислень, оскільки обчислення набагато простіше виконувати на СІ++, аніж на Асемблері. Особливо це відноситься до випадку мішаних обчислень, де використаються і значення із плаваючою комою і цілі числа. Краще покласти відповідальність за виконання перетворення типів і реалізації арифметики із плаваючою комою на СІ++.
Розглянемо приклад програми на Асемблері, що викликає функцію Borland C++, щоб виконати обчислення із плаваючою комою. Фактично в даному прикладі функція Borland C++ передає послідовність цілих чисел іншої функції Турбо Асемблера, що додає числа й у свою чергу викликає іншу функцію Borland C++ для виконання обчислень із плаваючої комою (обчислення середнього значення).
Частина програми CALCAVG.CPP, реалізована на СІ++ (CALCAVG.CPP), виглядає так:
#include <stdio.h>
extern float Average(int far * ValuePtr, int
NumberOfValues);
#define NUMBER_OF_TEST_VALUES 10
int TestValues(NUMBER_OF_TEST_VALUES) =
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
main()
{
printf("Середнє арифметичне дорівнює: %f\n",
Average(TestValues, NUMBER_OF_TEST_VALUES));
}
float IntDivide(int Divedent, int Divisor)
}
return( (float) Divident / (float) Divisor );
}

а частина програми на Асемблері (AVERAGE.ASM) має вигляд:
; Функція, що викликається з СІ++ з малою моделлю пам'яті,; яка повертає середнє арифметичне послідовності ; цілих чисел.; Для виконання останнього ділення викликає функцію СІ++ IntDivide().
; Прототип функції: ; extern float Average(int far * ValuePtr, ; int NumberOfValues);
; Ввід int far * ValuePtr ; масив значень для обчислення середнього
; int NumberOfValues ; кількість значень для обчислення середнього арифметичного
.MODEL SMALL
EXTRN _IntDivide:PROC
.CODE
PUBLIC _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] ; одержати знову кількість значень, переданих
; у функцію IntDivide у правому параметрі.
push ax ;передати суму в лівому параметрі
call _IntDivide ;обчислити середнє значення із плаваючою комою
add sp,4 ; відкинути параметри
pop bp
ret ; середнє значення в
; регістрі вершини стеку співпроцесора 8087
_Average ENDP
END
Основна функція (main) мовою СІ++ передає покажчик на масив цілих чисел TestValues і довжину масиву у функцію на Асемблері Average. Ця функція обчислює суму цілих чисел, і передає цю суму й число значень у функцію СІ++ IntDivide. Функція IntDivide приводить суму й число значень до типу із плаваючою комою і обчислює середнє значення (роблячи це за допомогою одного рядка на СІ++, у той час як на Асемблері для цього треба було б більше зусиль. Функція IntDivide повертає середнє значення (Average) у регістрі вершини стека співпроцесора 8087 і передає керування назад до основної функції.
Програми CALCAVG.CPP і AVERAGE.ASM можна скомпілювати і скомпонувати у виконавчу програму CALCAVG.EXE за допомогою команди: bcc calcavg.cpp average.asm
Зауважимо, що функція Average буде працювати як з малою, так і з великою моделлю пам’ятч без необхідності зміни її коду, тому що у всіх моделях передається покажчик далекого типу. Для підтримки більших моделей сегменту коду (надвеликий, великий і середньої) довелося б тільки змінити відповідну директиву .MODEL. Користуючись перевагами розширень, що забезпечують незалежність Турбо Асемблера від мови, асемблерний код із попередньо прикладу можна записати більш стисло (CONSISE.ASM):
.MODEL small,C
EXTRN C IntDivide:PROC
.CODE
PUBLIC 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 _IntDivide C,ax,NumberOfValues
ret
Average ENDP
END
Підтримувані Visual C/C++ узгодження по іменах
Узгодження, які використовує лінкер для мови С передбачають:
аргументи функціям передаються через стек;
викликаюча функція очищає стек від параметрів;
аргументи завантажуються в стек перед викликом функції справа на ліво;
імена ідентифікаторів мають префікс "підкреслення" (_);
імена ідентифікаторів є регістрозалежними (є різниця між великими і малими буквами);
функції повертають значення в регістропару eax:edx.
Для того, щоб в програмі доступатися до С функцій і даних з асемблерного чи будь-якого іншого модуля вони мають бути оголошені як такі, що звязані по правилам мови С за наступними правилами (детальніше див. статтю в MSDN “Using extern to Specify Linkage”):
extern string-literal { declaration-list }
extern string-literal declaration.
Інші узгодження по іменам (детальніше див. статтю в MSDN “Argument Passing and Naming Conventions”):
Ключове слово
Очистка стеку
Передача параметрів

__cdecl
Викликаюча функція
Заштовхує параметри в стек в зворотному порядку (справа на ліво).

__clrcall
Відсутня
Завантаження параметрів в стек CLR виразу в прямому порядку (зліва на право). Використовується для віртуальних функцій і використовує динамічне зв’язування.

__stdcall
Викликана функція
Заштовхує параметри в стек в зворотному порядку (справа на ліво).

__fastcall
Викликана функція
Зберігаються в регістрах, потім заштовхуються в стек.

__thiscall
Викликана функція
Заштовхуються в стек; вказівник this зберігається в ECX.


Приклад програми зі змішаним програмуванням C-Asm (лр1)
// Модуль main.cpp
// Програма реалізує обчислення: ((3 + nNumber2) + _nNumber1)* Weight
// і вивід результату на екран
#include <stdio.h>
// для того, щоб в програмі доступатися до С функцій і даних з асемблерного модуля
// вони мають бути оголошені як такі, що звязані по правилам мови С за допомогою
// extern "C"
extern "C" int weightedsum(int num1, int num2);
extern "C"
{
int nNumber1;
};
void main(void)
{
int nNumber2 , nRes;

nNumber1 = 1;
nNumber2 = 2;
nRes = weightedsum(nNumber2,3);
printf("Result is: %d", nRes);
}
----------------------------------------------------------------------------------
// Модуль func.asm
; використовується процесор з ядром і586 і вище
.586
.model flat
; flat - вказує на те, що всі сегменти розміщені фізично в одному сегменті
; Узгодження, які використовує лінкер для мови С передбачають:
; 1. аргументи функціям передаються через стек;
; 2. викликаюча функція очищає стек від параметрів;
; 3. аргументи завантажуються в стек перед викликом функції справа на ліво;
; 4. імена ідентифікаторів мають префікс "підкреслення" (_);
; 5. імена ідентифікаторів є регістрозалежними (є різниця між великими і малими буквами);
; 6. функції повертають значення в регістропару eax:edx.
EXTRN _nNumber1:dword
PUBLIC _weightedsum
.data
; оскільки Weight це локальна для func.asm змінна,
; то вона може не відповідати узгодженням для мови С
Weight dd 5
.code

; Процедура _weightedsum має 2 параметри і використовує узгодження мови С
_weightedsum PROC ; ближня процедура

; в стеку на даний момент є 32-ох розрядна ближня адреса пвернення з процедури
; збережемо в стек 32-ох розрядне значення ebp
push ebp
mov ebp,esp

; доступаємося до першого параметру який має відносно esp зміщення +8 байт
mov eax,[ebp+12]

; доступаємося до другого параметру який має відносно esp зміщення +12 байт
mov edx,[ebp+8]
add eax,edx
add eax,_nNumber1
mul Weight
; результат виконання занесено в eax

pop ebp
ret

_weightedsum endp
end
Програма після дизасемблювання
Щоб отримати доступ до дисасемблера необхідно в процесі відлагодження програми зайти в меню Debug->Windows->Disassembly. Щоб отримати доступ до поточного вмісту регістрів необхідно в процесі відлагодження програми зайти в меню Debug->Windows ->Registers.
// Модуль main.cpp
#include <stdio.h>
extern "C" int weightedsum(int num1, int num2);
extern "C"
{
int nNumber1;
};
void main(void)
{
004113A0 push ebp
004113A1 mov ebp,esp
004113A3 sub esp,0D8h
004113A9 push ebx
004113AA push esi
004113AB push edi
004113AC lea edi,[ebp-0D8h]
004113B2 mov ecx,36h
004113B7 mov eax,0CCCCCCCCh
004113BC rep stos dword ptr es:[edi]
int nNumber2 , nRes;
nNumber1 = 1;
; 417178h – адреса глобальної змінної в пам’яті
004113BE mov dword ptr [_nNumber1 (417178h)],1
nNumber2 = 2;
004113C8 mov dword ptr [nNumber2],2
nRes = weightedsum(nNumber2,3);
; виклик функції weightedsum починається з занесення в стек параметрів в порядку зліва на право
004113CF push 3
004113D1 mov eax,dword ptr [nNumber2]
004113D4 push eax
; власне виклик функції weightedsum адреса якої розміщена в таблиці функцій за адресою 411005h
004113D5 call @ILT+0(_weightedsum) (411005h)
; викликаюча функція відповідає за видалення даних зі стеку після завершення виконання
; викликаної функції. Дані зі стеку видаляються додаванням до esp числа 8 (2 змінні по 4 байти)
004113DA add esp,8
; Результат виконання знаходиться в регістрі eax звідки його треба скопіювати в nRes
004113DD mov dword ptr [nRes],eax
printf("Result is: %d", nRes);
004113E0 mov esi,esp
004113E2 mov eax,dword ptr [nRes]
004113E5 push eax
004113E6 push offset string "Result is: %d" (41563Ch)
004113EB call dword ptr [__imp__printf (4182B8h)]
; видалити дані зі стеку додаванням до esp числа 8 (2 змінні по 4 байти)
004113F1 add esp,8
004113F4 cmp esi,esp
004113F6 call @ILT+305(__RTC_CheckEsp) (411136h)
}
----------------------------------------------------------------------------------
//Фрагмент таблиці функцій
00411004 int 3
;адреса функції weightedsum рівна 4114F0h
00411005 jmp _weightedsum (4114F0h)
0041100A jmp _setdefaultprecision (412670h)
0041100F jmp _setargv (412710h)
00411014 jmp DebugBreak (41349Eh)
00411019 jmp _RTC_GetErrDesc (412590h)
0041101E jmp __p__fmode (412784h)
00411023 jmp __security_check_cookie (4132F0h)
00411028 jmp IsDebuggerPresent (4134AAh)
0041102D jmp _RTC_Terminate (412750h)
00411032 jmp WideCharToMultiByte (4134A4h)
00411037 jmp _RTC_AllocaHelper (411510h)
0041103C jmp _RTC_GetErrorFuncW (412650h)
00411041 jmp _RTC_NumErrors (412580h)
00411046 jmp __setusermatherr (4126F4h)
0041104B jmp Sleep (41348Ch)
00411050 jmp GetModuleFileNameW (4134FEh)
00411055 jmp __security_init_cookie (412910h)
0041105A jmp SetUnhandledExceptionFilter (413522h)
0041105F jmp _cexit (412A7Ch)
…………………
----------------------------------------------------------------------------------
// Модуль func.asm
.586
.model flat
EXTRN _nNumber1:dword
PUBLIC _weightedsum
.data
Weight dd 5
.code

; Процедура _weightedsum має 2 параметри і використовує узгодження мови С
_weightedsum PROC ; ближня процедура

; в стеку на даний момент є 32-ох розрядна ближня адреса пвернення з процедури
; збережемо в стек 32-ох розрядне значення ebp
push ebp
004114F0 push ebp
mov ebp,esp
004114F1 mov ebp,esp
; доступаємося до першого параметру який має відносно esp зміщення +8 байт
mov eax,[ebp+8]
004114F3 mov eax,dword ptr [ebp+8]
; доступаємося до другого параметру який має відносно esp зміщення +12 байт
mov edx,[ebp+12]
004114F6 mov edx,dword ptr [ebp+0Ch]
add eax,edx
004114F9 add eax,edx
add eax,_nNumber1
004114FB add eax,dword ptr [_nNumber1 (417178h)]
mul Weight
00411501 mul eax,dword ptr [Weight (417030h)]
; результат виконання занесено в eax
pop ebp
00411507 pop ebp
ret
00411508 ret
Приклад програми зі змішаним програмуванням C-Asm-C (лр2)
// Модуль main.cpp
// Програма реалізує обчислення: (((3 + nNumber2) + nNumber1)* Weight) / nDivisor,
// виклик програми ділення результату обчислень на задане другим аргументом функції
// divide число (функція визначена на мові С), результат роботи виводить на екран
#include <stdio.h>
// для того, щоб в програмі доступатися до С функцій і даних з асемблерного модуля
// вони мають бути оголошені як такі, що звязані по правилам мови С за допомогою
// extern "C"
extern "C" int weightedsum(int num1, int num2, char divisor);
extern "C" int divide(int arg1, int arg2)
{
return arg1/arg2;
}
extern "C"
{
int nNumber1;
};
void main(void)
{
int nNumber2 , nRes;
char nDivisor;

nNumber1 = 10000;
nNumber2 = 20000;
nDivisor = 10;
nRes = weightedsum(nNumber2, 3, nDivisor);
printf("Result is: %d", nRes);
}
----------------------------------------------------------------------------------
// Модуль func.asm
; використовується процесор з ядром і586 і вище
.586
.model flat
;flat - вказує на те, що всі сегменти розміщені фізично в одному сегменті
; Узгодження, які використовує лінкер для мови С передбачають:
; 1. аргументи функціям передаються через стек;
; 2. викликаюча функція очищає стек від параметрів;
; 3. аргументи завантажуються в стек перед викликом функції справа на ліво;
; 4. імена ідентифікаторів мають префікс "підкреслення" (_);
; 5. імена ідентифікаторів є регістрозалежними (є різниця між великими і малими буквами);
; 6. функції повертають значення в регістропару eax:edx.
EXTRN _nNumber1:dword
EXTRN _divide:PROC
PUBLIC _weightedsum
.data
Weight dd 5

.code
; Процедура _weightedsum має 2 параметри і використовує узгодження мови С
_weightedsum PROC ; ближня процедура

; в стеку на даний момент є 32-ох розрядна ближня адреса пвернення з процедури
; збережемо в стек 32-ох розрядне значення ebp
push ebp
mov ebp,esp

; завантажити перший параметр який має відносно esp зміщення +8 байт
mov eax,[ebp+8]

; завантажити другий параметр який має відносно esp зміщення +12 байт
mov edx,[ebp+12]
add eax,edx
add eax,_nNumber1
mul Weight
; результат виконання занесено в eax

mov edx,eax

; завантажити третій параметр який має відносно esp зміщення +16 байт
mov al,[ebp+16]
cbw
cwde

; занести параметри функції в стек справа на ліво (arg2 потім arg1)
push eax
push edx

; викликати С функцію divide
call _divide

; видалити параметри divide зі стеку додаванням до esp числа 8 (2 змінні по 4 байти)
add esp,8

pop ebp
ret

_weightedsum endp
end
----------------------------------------------------------------------------------
// Модуль func1.asm
// Альтернативно замість модуля func.asm можна використати модуль func1.asm
.586
.model flat
;flat - вказує на те, що всі сегменти розміщені фізично в одному сегменті
; Узгодження, які використовує лінкер для мови С передбачають:
; 1. аргументи функціям передаються через стек;
; 2. викликаюча функція очищає стек від параметрів;
; 3. аргументи завантажуються в стек перед викликом функції справа на ліво;
; 4. імена ідентифікаторів мають префікс "підкреслення" (_);
; 5. імена ідентифікаторів є регістрозалежними (є різниця між великими і малими буквами);
; 6. функції повертають значення в регістропару eax:edx.
EXTRN _nNumber1:dword
; структура директиви PROTO: label PROTO [distance] [langtype] [,[parameter]:tag]
; детальніше http://support.microsoft.com/kb/73407
; директива використовується сумісно з директивою invoke
divide PROTO near C :dword, :dword
PUBLIC _weightedsum
.data
Weight dd 5

.code

; Процедура _weightedsum має 2 параметри і використовує узгодження мови С
_weightedsum PROC ; ближня процедура

; в стеку на даний момент є 32-ох розрядна ближня адреса пвернення з процедури
; збережемо в стек 32-ох розрядне значення ebp
push ebp
mov ebp,esp

; доступаємося до першого параметру який має відносно esp зміщення +8 байт
mov eax,[ebp+8]

; доступаємося до другого параметру який має відносно esp зміщення +12 байт
mov edx,[ebp+12]
add eax,edx
add eax,_nNumber1
mul Weight
; результат виконання занесено в eax

mov edx,eax
; доступаємося до третьго параметру який має відносно esp зміщення +16 байт
mov al,[ebp+16]
cbw

; cwde розширює слово в АХ до подвійного слова в ЕАХ
cwde

; Директива invoke здійснює виклик операцій за вказаною адресою (наприклад, функцію)
; передаючи параметри в прямому порядку
; структура директиви invoke: INVOKE expression [[, arguments]]. Адреси передаються
; за допомогою оператора addr, наприклад, INVOKE proc1, _Val1, addr _Val2, eax
; детальніше http://support.microsoft.com/kb/73407
invoke divide, edx, eax

pop ebp
ret

_weightedsum endp
end
Де взяти макроасемблер для MS VS 2005 (ml.exe)?
Якщо при встановленні MS VS 2005 файл ml.exe не встановився, тоді необхідно відкрити на дистрибутивному диску MS VS 2005 файл vs_setup.msi, зайти в каталог \vs_setup.msi\SourceDir\Program Files\Microsoft Visual Studio 8\VC\bin\ і скопіювати файл ml.exe в C:\Program Files\Microsoft Visual Studio 8\VC\bin\