пʼятницю, 27 травня 2011 р.

Приклади використання асемблера разом з OpenWatcom

Що таке Open Watcom

Open Watcom -- "Open source" версія славного компілятора Watcom. В 90-х йому фактично не було рівний, особливо під платформи DOS та Windows. Багато програм, зокрема канонічних ігор тих (і всіх взагалі) часів, таких як Doom, Hexen, Descent чи Duke Nukem 3D компілювалися саме ним, а заставка DOS/4GW пам'ятна багатьом дотепер. Історія його розпочалася в 80-х роках минулого століття на ниві FORTRAN, компілятор C вийшов у 1984. При тому, ходять чутки що історія цього сімейства компіляторів розпочалася ще в 1965, хоча яка саме природа приємственності між розробками 60-х та 80-х, мені невідомо. В кінці минулого тисячоліття компанія-розробник, SyBase, прийшла до висновку що ринок компіляторів під DOS фактично вимирає, а тягатися з Microsoft під Windows нереально. Тому на початку 2000-х, в кілька етапів, код компілятора було відкрито. (Лише той, що належав SyBase, тому Windows SDK, MFC, тести компілятора, підтримка QNX і т.д. не були відкриті.) Тепер він підтримується та розробляється спів-товариством добровільних розробників. 

Що я про нього думаю
Про сучасний стан фахово говорити мені складно, тому далі виключно моє IMHO. 
Оптимізатор коду С, най-най в 90х, зараз помітно поступається сучасним компіляторам. Підтримка C++ доволі слабенька, хоча певна тенденція прямувати до повної підтримки стандарту C++98 є. Перші версії OpenWatcom використовували STLport, однак потім від неї відмовилися на користь власної Open Watcom STL (OWSTL). Вона все ще неповна і працювати з нею людині, яка звикла до повноцінної (не обов'язково най-най-сучаснішої з найідеальнішою відповідністю стандарту) важко, пробував. Щоправда, спроби використати STLport все ще мають шанс на успіх. З іншого боку, багато важливих платформ, які підтримує OpenWatcom -- 16-бітні, і навряд чи є реальний шанс подружити з ними STLport. Тому потреба у власній реалізації STL велика. (Буду вдячний за вказівку на більш точну інформацію про розвиток STL для OpenWatcom, бажано з посиланнями, тому що не маю впевненості чи я не перебрехав усе).

Ходять крім того по Інтернету чутки що розробники не особливо чуйні, навіть якщо приходити до них із своїми "патчами". Особливо бояться чіпати кодогенератор. Наскільки вони обґрунтовані не знаю.

Вище говориться про недоліки. Але, при тому, це один з найкращих доступних інструментів для розробки під 16-бітні операційні системи, зокрема 16-бітного коду (djgpp, порт мого улюбленого gcc під DOS генерує 32-бітний код та покладається на сервіс DPMI, якщо немає системного -- власний CWSDPMI). При цьому OpenWatcom -- не тільки компілятор/лінкер/асемблер/дизасемблер, що і так було б немало, але й IDE, засоби збірки програм, зневадження, та інші інструменти. 

Взагалі, в ролі host-платформи він може використовувати (тобто -- працює на):  DOS, OS/2, Linux, Windows 16/32-бітний (на 64-бітному він працюватиме як 32-бітна аплікація), RDOS; target-платформи (для яких може генерувати код): всі перераховані + Nowell NetWare (інформація взята з інсталятора OW-1.9). Мови програмування -- FORTRAN, C/C++.

Використання асемблера разом з OpenWatcom

В уроках захищеного режиму Open Watcom використовується якраз завдяки тому, що підтримує генерацю 16-бітного коду. Главу про його взаємодію із зовнішніми функціями, зокрема написаними на асемблері, я вирішив винести в окремий пост. Інформація, подана тут/там -- лише ввідна. (Вона далеко не вичерпна - компілятор має надзвичайно розвинуті засоби для такої взаємодії. І це абсолютно природно, враховуючи зоопарк угод про виклики в тих OS, які він підтримує.)

Ці засоби менш гнучкі  та більш прямолінійні ніж аналогічні в gcc, але зате простіші у вивченні та використанні. (Ех, треба буде колись і про Inline Assembler в gcc написати...)


Правила передачi аргументiв у функцiї, повернення результатiв з них, манiпуляцiй стеком та регiстрами визначаються так-званими угодами про виклики (calling conventions). Вони вiдрiзняються у рiзних операцiйних системах, рiзних компiляторах, можуть навiть вiдрiзнятися якщо використовуються рiзнi опцiї компiлятора чи модифiкатори функцiй (наприклад __cdecl чи __fastcall).

Певна тенденцiя до стандартизацiї присутня, однак ”зоопарк” таких угод все ще великий. Ми не будемо зараз їх детально розглядати — це хоч i цiкава тема, але об’ємна i при тому вона не дуже важливою для розуміння подальшого матерiалу.

Для тих, кого цiкавлять подробицi, можна порекомендувати сайт Агнера Фога (Agner Fog) www.agner.org. Зокрема, у його практично вичерпнiй роботi "5. Calling conventions for different C++ compilers and operating systems" зiбрано угоди про виклики бiльшостi поширених компiляторiв та операцiйних систем.

Угоди про виклик OpenWatcom описанi в офiцiйних настановах користувачів ”OpenWatcom C/C++ User Guide”, якi ми надалi називатимемо скорочено cguide. Зокрема, роздiли ”Calling Conventions for Non-80x87 Applications” i ”Calling Conventions for 80x87-based Applications” 16-бiтної та 32-бiтної частин.

Нижче трiшки про них розповiдається, однак для простоти по можливостi на них не покладаємося, в бiльшостi випадкiв прямо вказуватимемо, якi регiстри використовувати.

Явне вказання регiстрiв при виклику функцiй
При оголошеннi зовнiшнiх (extern) функцiй, OpenWatcom дозволяє вказувати, яким чином їм будуть передаватися параметри i як вони будуть повертати результати своєї роботи.

extern unsigned long read_cr0();
#pragma aux read_cr0 \
value [dx ax];
//32-бiтний результат повертається в парi DX:AX
extern unsigned char inportb (unsigned short _port);
#pragma aux inportb \
parm [dx] \
value [al];

Для цього використовується pragma aux. Пiсля неї iде iм’я функцiї, та будь-якi з parm <список регiстрiв>, modify <список регiстрiв> i value <список регiстрiв>. Детальнiше – див. офiцiйнi iнструкцiї по користуванню OpenWatcom cguide.

modify [al dx] повiдомляє компiлятору, що функцiя змiнює регiстри al та dx. Варто зауважити, що регiстр AX змiнюють настiльки часто, що вiн по замовчуванню вважається таким що буде змiненим пiсля виклику функцiї. Якщо в цьому буде потреба (змiнюватимуться важливi для компiлятора чи нормальної роботи програми регiстри), компiлятор створить код для збереження i вiдновлення вказаних регiстрiв. Також можна вказати точний список регiстрiв, що пiдлягають модифiкацiї. Тодi всi решта будуть вважатися незмiнними: modify exact [cx dx]. Ще одна додаткова можливiсть — повiдомити компiлятору, що функцiя не змiнює пам’ять: modify nomemory.

Запис value [dx ax] вказує що для повернення можна використати регiстри з множини [DX, AX]. Якщо повертався б 16-бiтний тип int (нагадаємо, що ця програма в основному працює в реальному режимi), то компiлятор на свiй розсуд мiг вибрати або DX або AX. Функцiя read_cr0 повертає 32 бiти – long int, тому (без варiантiв, принаймi у OpenWatcom 1.5/1.9) буде використано пару DX:AX, старша половина в DX, молодша в AX. Аналогiчно, parm [dx ax] означало б що для передачi першого аргумента можна використати регiстри з множини [DX, AX]. parm [dx] просить використовувати лише DX.

Важливим є те, що якщо параметри не можна передати за допомогою вказаних регiстрiв, компiлятор мовчки, без жодних попереджень, передає аргументи (чи повертає результати) через стек. Тобто, може трапитися ситуацiя, коли пiдпрограма на асемблерi розраховує отримати параметри в певних регiстрах, а код на С iгнорує запит на цi регiстри... Все ускладнюється ще й тим, що однобайтовi величини (char наприклад), при передачi спочатку розширюютьчя до двох байт (16 бiт). Повертаються однобайтовi величини без всяких розширеннь. Тому, якщо написати

extern unsigned char inportb (unsigned short _port);
#pragma aux inportb \
parm [dx] \
value [al];

результат буде взято з AL, але якщо написати:

extern void outportb (unsigned short _port, unsigned char _data);
#pragma aux outportb \
parm [dx] [al];

то компiлятор розширить "unsigned char"до 16-ти бiтiв, виявить що 16 бiтiв у AL не помiстяться i передасть їх через стек, згенерувавши при виклику outportb (0x70,0x80) наступний код:

push 0x0080 ;<=== Увага
mov dx,0x0070
call outportb_

що безперечно засмутить (вона очiкувала дещо iншого) нашу функцiю outportb, реалiзовану ось так:

public _outportb as ’outportb_’
_outportb:
out dx,al
retn

Однак, якщо (асемблерна) функцiя задана безпосередньо в pragma aux, то в цьому випадку компiлятор строго дотримується заданих регiстрiв, i якщо параметр в прототипi не спiвпадає по розмiру з видiленим для нього регiстром, то цей параметр згiдно правил, близьких до звичайних правил приведення, "вкорочується"чи "розтягується". Наприклад, якщо для параметра типу (16-бiтного) int видiлено 8-бiтний регiстр, то спочатку вiдбудеться приведення int до char. Якщо ж для типу int видiлено пару DX:AX, то спочатку вiдбудеться приведення до long int.

Короткi асемблернi вставки

Невеликi пiдпрограми на асемблерi можна задавати зразу пiсля оголошення функцiї, в лапках, наприклад:

extern void sti();
#pragma aux sti = \
"sti";

Створюємо функцiю, яка складається з однiєї команди — sti. При цьому дозволяється використовувати i описанi вище директиви parm, modify та value, однак в такому випадку компiлятор зi значно бiльшою увагою буде вiдноситися до вказаних регiстрiв, адже усвiдомлює що ця inline-функцiя очiкує їх саме в такiй формi.
Коментарi в асемблерних вставках можуть бути як звичайнi, типу С чи С++ – ”\* ... *\”, ”\\ ...”, так i асемблернi – ”;”.

Регiстри, якi не повиннi змiнюватися у функцiях
 Асемблернi вставки чи функцiї, написанi на асемблерi, часто працюють з регiстрами. Однак компiлятор очiкує що в деяких регiстрах буде знаходитися саме те, що вiн туди поклав. Якщо функцiї потрiбен один з таких регiстрiв, ї ї оголошення повинне про це повiдомити компiлятор (див. вище про директиву aux), або вона сама мусить зберегти його на початку та вiдновити по закiнченню роботи. В 16-бiтному режимi комiплятор очiкує наступне:
  •  Всi регiстри, за винятком AX, та тих що використовуються для передачi параметрiв i повернення результатiв, слiд зберiгати.
  • Прапорець напрямку DF з FLAGS має зберiгати своє значення.
  • Для малої моделi пам’ятi слiд використовувати ближнє повернення – retn, для великої – дальнє, retf.
  • Все що функцiя помiстила в стек, вона повинна i забрати звiдтiля.
В 32-бiтному є два варiанти способу виклику функцiй. В одному з них аргументи передаються через регiстри. При цьому слiд враховувати тi ж побажання що i вище (тiльки стосовно 32-бiтних регiстрiв). В iншому аргументи передаються через стек i дозволяється змiнювати окрiм (E)AX ще ECX i EDX. В моделях пам’ятi, вiдмiнних вiд малої, також можна змiнювати ES.
Тому 16-бiтну функцiю з ближнiм типом виклику для очистки екрана засобами BIOS16
можна створити або так, роблячи все самостiйно:

//Оголошення
void clrscr_BIOS();
//Реалiзацiя на асемблернi
GLOBAL clrscr_BIOS_
clrscr_BIOS_:
push BX
push CX
push DX
mov AH, 06h ;Очистити вiкно
mov CX, 0 ; 0,0 лiвий верхнiй кут
mov DX, 184fh ; 24,79 - правий нижнiй
mov BH,7 ;атрибут по замовчуванню
int 10h
pop DX
pop CX
pop BX
retn
або користуючись засобами OpenWatcom:

//Оголошення
void clrscr_BIOS();
#pragma aux clrscr_BIOS \
modify [AX BX CX DX];
//Реалiзацiя на асемблернi
GLOBAL clrscr_BIOS_
clrscr_BIOS_:
mov AH, 06h ;Очистити вiкно
mov CX, 0 ; 0,0 лiвий верхнiй кут
mov DX, 184fh ; 24,79 - правий нижнiй
mov BH,7 ;атрибут по замовчуванню
int 10h
retn
або взагалi без використання окремого асемблерного коду:

//Оголошення та реалiзацiя
void clrscr_BIOS();
#pragma aux clrscr_BIOS \
modify [AX BX CX DX] = \
"mov AH, 06h ;Очистити вiкно" \
"mov CX, 0 ; 0,0 лiвий верхнiй кут" \
"mov DX, 184fh ; 24,79 - правий нижнiй" \
"mov BH,7 ;атрибут по замовчуванню" \
"int 10h" \
"retn";

Заплутування/прикрашання iмен (name mangling/decorating)

При написаннi функцiй на асемблерi часто використовувалися подвiйнi оголошення:

public _write_cr0
public _write_cr0 as ’write_cr0_’
 Директива public використовується для вказання асемблеру зробити функцiю достiпною ззовнi. Вона є доповнюючою до C-шної директиви extern. public X as Y – робить X доступною з iменем Y. Наведенi вище варiанти вiдрiзняються лише символом "_ який в одному випаду на початку а в другому — в кiнцi. Причина — С-компiлятори часто (в залежностi i вiд виду компiлятора i вiд вибраної для функцiї угод про виклик — calling convention) до назви функцiї (оголошеної як extern "C")  додають на початку або в кiнцi "_". При чому, якщо вже додають, то OpenWatcom додає "_" в кiнцi (хоча, тут є нюанси, див. главу ”Calling Conventions for Non-80x87 Applications” 32-бiтної частини офiцiйних настанов користувача cguide), а VisualC, mingw-gcc, intel, borland C, Digital Mars додають на початку. Наприклад, функцiя оголошена наступним чином (__cdecl — по замовчуванню):

extern "C" /*__cdecl*/ type some_function(<param list>) ;

для редактора зв’язiв (в об’єктному файлi) буде називатися:

_some_function

якщо компiлювалася за допомогою, скажiмо, mingw-gcc (пiд Windows)

some_function

якщо компiлювалася за допомогою gcc (пiд Linux, xBSD),

some_function_

якщо компiлювалася за допомогою OpenWatcom. Тому тут ми експортуємо два варiант (перший i третiй).

Детальнiше див. ”Calling conventions” Агнера Фога, секцiя ”Turning off name mangling with extern "C"" (8.10 у версiї вiд 2006-08-13). 

Керування вирiвнюванням структур
(Матеріал вже з другого уроку, але тематично пасує тут).

#pragma pack (push, 1) /* вирiвнювати структури по границi байта */

За допомогою цiєї директиви OpenWatcom дозволяє вказувати вирiвнювання для структур. Число 1 означає, що надалi буде використовуватися вирiвнювання по байтах, а push зберiгає те вирiвнювання яке було до виконання директиви. Його можна потiм вiдновити за допомогою:

#pragma pack (pop) /* повернутися до вирiвнювання по замовчуванню */

З нульовим та першим уроком закінчили, скоро перейдемо до другого :-)

Немає коментарів:

Дописати коментар