
Теоретичні відомості
Основні відомості про роботу з Visual Studio
Далі використовується Visual Studio 2010, проте всі дії в інших версіях лишаються аналогічними.
Середовище розробки VS призначене для розробки програмного забезпечення на різних мовах, на кшталт C++, C#, Visual Basic чи F#. Серед підтримуваних мов нас цікавить C++. Програми, написані на ній у VS, можуть використовувати так звані «асемблерні вставки». Передусім вони використовуються в тих місцях коду C++, де необхідна оптимізація через те, що компілятор C++ не в змозі згенерувати достатньо ефективну за швидкістю виконання або використанням ресурсів послідовність машинних команд. Це не завжди необхідно, бо сучасні компілятори виконують значну оптимізацію.
Робота в VS базується на Рішеннях (Solution), котрі можуть складатися з кількох проектів, які в свою чергу містять кілька файлів різного призначення. Серед них: вихідні коди (у мові С++ мають розширення *.cpp), файли заголовків (*.h), ресурси (*.res). Чим відрізняються рішення від проектів? Всі файли в проекті, образно кажучи, «перетворюються» в одну програму з однією точкою входу чи в бібліотеку. Рішення ж можуть складатися з багатьох таких програм, які можуть працювати одночасно чи послідовно, залежати одна від іншої або складати певний комплекс програм. В принципі такий спосіб поділу введений хіба що для зручності, тож на цьому зараз не варто зосереджувати увагу: головне- в VS програміст працює не з одним вихідним файлом, а з декількома зв’язаними між собою: той самий принцип, що і в IDE типу Eclipse, NetBeans, IntelliJ IDEA тощо, лише трохи інша реалізація.
Щоб почати написання програми необхідно створити новий проект – а рішення створиться разом із ним автоматично. Це можна зробити як через стартову сторінку VS, так і через головне меню (або взагалі Ctrl + Shift +N). Для цієї лабораторної роботи необхідним типом проекту є консольний проект, точніше Win32 Console Application - очевидно, що мовою має бути С++. Щоб створити проект треба ще й задати ім’я проекту та місце його розташування, якщо стандартне не подобається. Після цього всі налаштування у майстрі створення проекту можна лишити стандартними – одразу натиснути Finish.
Основні файли, що при цьому створюються:
<project_name>.cpp – «головний» вихідний файл, який містить точку входу – функцію _tmain.
// test.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
return 0;
}
stdafx.h – файл заголовка, який під’єднує системні файли заголовків та інші нечасто змінювані файли заголовків. Ці умови не обов’язково виконувати, це лише «хороший тон».
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//
#pragma once
#include "targetver.h"
#include <stdio.h>
#include <tchar.h>
// TODO: reference additional headers your program requires here
Програма запускається для відлагодження за командою F5. При цьому середовище змінює розташування деяких панелей – тож якщо потрібні закриються, можна їх повернути через меню View… Що й без пояснень очевидно. Суттєвих відмінностей від інших – в тому числі й Java – IDE для виконання цієї лабораторної роботи немає. Хіба що інші клавіатурні комбінації і назви меню. Тому перейдемо до власне асемблерних вставок.
Використання асемблерних вставок (inline-асемблер) С (С++).
Перед вставкою пишемо зарезервоване слово __asm. Його можна використовувати для вставленняя як однієї команди:
__asm mov eax, 1
Так і кількох підряд, об’єднавши їх фігурними дужками:
__asm
{
mov ecx, count;
mov ebx, arr;
sumall:
add eax, [ebx + 4*ecx - 4];
loop sumall;
}
Як можна помітити, основний синтаксис зовсім не відрізняється від звичайних програм, написаних на асемблері, за винятком хіба того, що можна використовувати змінні та параметри, передані в функцію, описані за межами самої вставки. Також користуватися можна
-
константами та членами множин (enum)
-
макросами та директивами препроцесора
-
коментарями
-
іменами типів та typedef
Але при цьому не вийде скористатися, наприклад, директивами “db”, “dw”, ”dd” і т.д., асемблерними процедурами “name PROC … name ENDP”, а також тими директивами TASM , що не мають ніякого сенсу, знаходячись у вставках, на кшталт “.386” або “.data”.
Використання змінних та вказівників в inline-асемблері
Змінні, як сказано вище, можна використовувати одразу, без будь-яких додаткових конструкцій:
mov ecx, count;
З вказівниками ситуація трохи інша, використати операцію розіменування не вийде. Тому вони обрамляються в таку конструкцію:
mov ebx, arr;
add eax, [ebx + 4*ecx];
де arr – вказівник (масив), а в ecx знаходиться індекс елемента, до якого треба звернутися.
В С можуть використовуватися 16-бітні near вказівники (лише зміщення в межах поточного сегменту), 32-бітні far-вказівники (містять пару сегмент:зміщення, як у Pascal) та 32-бітні huge-вказівники (містять 20-бітну суму, до якої можна застосувати арифметичні операції). Це стосується як вказівників, так і посилань (які на низькому рівні ідентичні до вказівників).
Передача параметрів у функції та повернення значень із неї
У більшості випадків параметри – будь то значення, посилання чи вказівники – передаються через стек. Винятком є конвенція FASTCALL: параметри передаються в регістрах загального призначення, якщо в них достатньо місця. Проте цю конвенцію розглядати не будемо, зосередившись на C-конвенції (також PASCAL, яка вважається застарілою, і STDCALL – для порівняння).
Для початку слід зауважити, що всі параметри з розміром меншим за 4 байти розширюються до 32 біт при передачі. Так само розширюються значення, що повертаються з функції. Ці значення мають бути розміщенні в регістрі eax, за виключенням, наприклад, 64-бітного типу long, що повертається у парі edx:eax.
Конвенція pascal
Ця конвенція використовується у мові Pascal, а також BASIC, FORTRAN, ADA тощо. В цій конвенції аргументи перед викликом функції поміщаються в стек у прямій послідовності, а очищує стек сама функція. Дані при цьому розташовані в пам’яті в зворотньому порядку.
Таким чином такий код на паскалі:
procedure proced(a, b, c : integer);
begin
…
end;
…
proced(x, y, z);
перетвориться на:
proced proc
push ebp ;стандартний пролог
mov ebp, esp
a equ [ebp + 16] ;аргументи розташовані в зворотньому порядку
b equ [ebp + 12]
c equ [ebp + 8]
…
pop ebp ;стандартний епілог
ret 12 ;зі звільненням 12 байтів стеку зайнятих аргументами
proced endp
…
push x ;прямий порядок занесення аргументів
push y
push z
call proced
Недоліком
є велика складність при написанні
процедур та функцій зі змінною кількістю
параметрів, оскільки функції не так
просто визначити
адресу першого аргументу, що розташований
в самому кінці.
Конвенції c та stdcall
В цих конвенціях параметри заносяться до стеку в зворотньому порядку. Різниця між C та STDCALL полягає в тому, що в останній підпрограми мають власноруч звільняти стек.
Код, написаний на C++
void func(a, b, c)
{
…
}
…
func(x, y, z)
Транслюється в
func proc
push ebp ;стандартний пролог
mov ebp, esp
a equ [ebp +8] ; в пам’яті аргументи розміщені в прямій послідовності
b equ [ebp + 12] ; ми точно знаємо адресу першого з них
c equ [ebp + 16]
…
pop ebp ;стандартний епілог
ret ;повернення без очищення стеку
func endp
…
push z ;зворотній порядок занесення
push y
push x
call func ;виклик функції
add esp, 12 ;очистка стеку – її виконує той, хто викликає функцію (конвенція С)
В конвенції С дуже легко викликати різні функції з одними й тими ж параметрами. Функція не звільняє стек власноруч, тобто параметри лишаються в стеку і їх можна використати знову.
Як видно із текстів на асемблері, компілятор додає так звані «пролог» та «епілог» всередину кожної функції. Вони зберігають значення вказівника стеку, щоб звернення до параметрів (як [ebp + 12]) не залежали від змін у самому стеку, наприклад, через створення локальних змінних. Якщо необхідно написати власний пролог та епілог, перед функцією треба додати __declspec(naked):
__declspec(naked) int func(int i)
{
__asm
{
… ;власний пролог
}
…
__asm
{
… ;власний епілог
}
}
Використання return в naked-функціях не дозволяється через зрозумілі причини.